You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This PR adds allowances: a payer sets a per-payment and a monthly limit for a Paykit contact, and that contact's requests within the limits are paid without asking.
Description
It is stacked on #1401, so it builds against the published Paykit 0.1.0-rc59 that #1401 resolves from GitHub Packages. That release includes the allowance stack (pubky/paykit-rs#158 to #161) and the shared identity runtime, where one Encrypted Link serves a contact's identity and the allowance is bound to the two identities instead of to a receiver folder. Merge #1401 first; this PR's diff shrinks to the allowance work once it lands.
Adds an Allowances tab to Subscriptions, with an empty state, the list and a Set Allowance sheet with per-payment and monthly USD limits, so a payer can let a contact's requests pay themselves
Sends the allowance over the existing private Paykit link and opens a review sheet on the receiver, who accepts or declines; either side can end an allowance from its detail sheet
Binds each allowance to the contact's identity, so one grant covers every Paykit app of that contact and there is one entry per grant, with no per-link proposals
Pays an incoming request within the limits automatically, over Lightning or on-chain: the request is reserved in the SDK's allowance accounting before the send and reported back afterwards, so a kill and relaunch mid-payment never pays twice
Claims the request's execution for Bitkit before accepting it automatically, and saves this install as the accepting install first, so an automatic payment that fails stays a payable manual request on this install under the shared-state ownership rules
Pays the endpoint the request is bound to and names the paying app in the payment proof, as the shared runtime requires
Leaves a request above the per-payment limit, over the monthly cap, or after an end as an ordinary Payment Request, with a Limit Reached notification for the payer and a Payment Executed notification for an automatic one
Tags automatically paid rows "Auto-paid" in the Payments tab and shows the amount paid automatically on the allowance detail
Waits for the payee's next private payment list instead of asking the payer when the previous list was used up, and holds automatic payments until the node has a usable channel, because both cases fell back to manual on regtest
Fixes automatic payments so an admitted payment finishes even when the refresh that started it is cancelled, because a cancelled send left the request accepted, unpaid and off the request sheet
Lets an explicit E2E homegate URL win on every backend, a dev-only change used to run the branch against a local Pubky homegate
Out of Scope
Allowance detail on the payee: "Paid automatically" shows $0.00, because the payer's wallet keeps the payment journal; Bitkit can sum the received payment proofs that carry the allowance id instead
Automatic payments: pays over bolt11 when the payee offers an eligible invoice and on-chain otherwise; there is no liquidity check before picking bolt11 and no fallback from a failed Lightning send to on-chain, so a payer without usable outbound capacity falls back to a manual request
PaykitAllowanceExecutor.kt recovery: a send stopped by a process kill stays at SENDING, so its request neither pays nor asks; follow-up
PaykitAllowanceExecutor.kt recovery: it settles every open attempt in the identity-wide ledger, which assumes Bitkit is the only app paying from it; follow-up before another app shares the ledger
LightningRepo.ktsync(): a cancellation is recorded as a sync error and starts the retry loop instead of being rethrown; follow-up
Failed automatic send: the request is already accepted, so it falls back to a manual Payment Request and is not retried automatically
Design
Figma "Bitkit - Experimental New": Allowances empty state, Set Allowance, Allowances list. The receiver's review sheet and the detail sheet have no frame and follow the drawn film. Deliberate differences: the first tab stays "Overview", the list row's right column reads the monthly limit, and the copy typos are fixed.
Preview
Allowances empty
Set Allowance
Review on the payee
Detail
Recording
side-by-side.mp4
QA Notes
Journeys
new set-and-accept.xml — the offer opens on the payee by itself and both rows turn Active
new auto-pay-under-limit.xml — a $2 request pays itself with only a Payment Executed notification
new above-limit-asks.xml — a $20 request arrives as an ordinary Payment Request
new monthly-cap-reached.xml — the third $4 request on a $10 cap raises Limit Reached and asks
new end-stops-auto-pay.xml — after either side ends it, the next request asks
new restart-never-pays-twice.xml — a kill right after the hand-off never leads to a second payment
Automated Checks
added PaykitAllowanceTest.kt — limits, terms, capacity and month anchoring of the allowance model, and status and capacity on real time while the subscription clock offset is set
added PaykitAllowanceExecutorTest.kt — reserve, hand-off, outcome recording and restart recovery of automatic payments, the execution claim before the automatic acceptance, and admission on real time while the subscription clock offset is set
added PaykitAllowanceRepoTest.kt — admission decisions: automatic, manual, deferred and limit reached, one grant per contact identity, and coverage on real time while the subscription clock offset is set
added PrivatePaykitAllowancePaymentTest.kt — resolving the payee's request-bound private endpoint for an automatic payment
added AllowancesViewModelTest.kt — list, set, review and detail state
updated PaykitPaymentRequestRepoTest.kt — an automatic acceptance saves this install as the owner and drops it on a definite failure
updated AppViewModelSendFlowTest.kt — the send flow with the allowance repo wired in
updated PaykitPaymentProofRepoTest.kt — payment proofs carry the allowance id and the paying app
ran ./gradlew --offline testDevDebugUnitTest detekt with com.synonym:paykit-android:0.1.0-rc59 from a local build of the v0.1.0-rc59 tag in place of GitHub Packages — 3,278 unit tests pass and detekt is clean
ran set-and-accept.xml, auto-pay-under-limit.xml, above-limit-asks.xml and end-stops-auto-pay.xml on two Android 15 emulators with fresh wallets against the Blocktank staging regtest LSP and the staging homegate, on 0b1f2664a before the last sync with feat: share paykit state across apps #1401 — all four pass; a $2 request paid itself over Lightning, a $20 request asked, and after an end the next request asked. Staging was slow: the automatic payment arrived about 5 minutes after the request, and two payment request sends timed out once before succeeding on retry. The review sheet opens on its own only when no intro sheet is on top, and the payer row of an ended allowance reads "Ended" where end-stops-auto-pay.xml expects "Ended · $0.00 paid automatically"; the journey text is not updated
not run: monthly-cap-reached.xml and restart-never-pays-twice.xml; the cap and the restart rules are covered by PaykitAllowanceExecutorTest.kt
Pushed 573123b: #1401 was rebased onto a newer master (new head 14d5383ff), which rewrote its commits, so a plain merge conflicted in nine files. I merged its new head with the old #1401 head (7026f8693) as the merge base, so only the rebase delta applied, with no conflicts, and this PR's history stays a fast-forward of the previous head. The diff against #1401 is again only the allowance work. 3,242 unit tests and detekt pass on the merge.
Pushed a2f33ad: #1401 was rebased again (new head 5da2ef235, which now includes the subscription clock offset from master). I merged it the same way as before, with the previous #1401 head as the merge base, so there were no textual conflicts. Two changes on top:
Its new allowUsedOnchainAddress flag broke the build in the allowance payment resolution; automatic payments are one-time and never reuse an on-chain address, so I pass false.
I added three guard tests, one each for admission, coverage and status with the offset set to 400 and 365 days, which assert that allowances keep real time. The admission test fails when the executor's trusted time reads the offset.
Pushed cda2de2: I merged the new head of #1401 (820335046, paykit recovery and payment details) into this PR, keeping history. One conflict in PaykitPaymentRequestRepo.kt: I kept the shared acceptance-ownership helper and added the new ConcurrentUpdate case to its list of uncertain failures. 3,265 unit tests and detekt pass on the merge.
Pushed 0e4e239: I merged the new head of #1401 (2458eaec1, redundant Paykit polling stopped) into this PR, keeping history and with no conflicts. 3,266 unit tests and detekt pass on the merge.
Pushed 3472b26: I merged the new head of #1401 (2ca29dc90, payment authorization and cleanup guards) into this PR, keeping history. One conflict in PaykitPaymentRequestRepo.kt: I kept the shared acceptance-ownership helper and added the new SharedStateBusy case to its list of uncertain failures. 3,268 unit tests and detekt pass on the merge.
Pushed 44af685: I merged the new head of #1401 (88231813c, a test formatting change only) into this PR, keeping history and with no conflicts. Detekt and the changed test pass; the full suite last ran on the previous head with 3,268 passing.
Pushed de33cd3: I merged the new head of #1401 (2f42424d8, yielding between Paykit handshake advances) into this PR, keeping history and with no conflicts. 3,269 unit tests and detekt pass on the merge.
Pushed 681f0fb: I merged the new head of #1401 (7d3c1967b, Paykit background retries consolidated) into this PR, keeping history and with no conflicts. 3,268 unit tests and detekt pass on the merge.
Pushed a63faa4: I merged the new head of #1401 (09c6426b9, Paykit messages received on each inbox poll) into this PR, keeping history and with no conflicts. 3,268 unit tests and detekt pass on the merge.
Pushed 9879897: I merged the new head of #1401 (10a3a647a, private endpoint withdrawal failures reported) into this PR, keeping history and with no conflicts. 3,268 unit tests and detekt pass on the merge.
Pushed 1962193: I merged the new head of #1401 (eaf6a092b, Paykit preparation coalesced and inbox polling reduced) into this PR, keeping history and with no conflicts. 3,273 unit tests and detekt pass on the merge.
Pushed c71b8df: I merged the new head of #1401 (0fdff066a, private contact preparation and request discovery coordinated) into this PR, keeping history and with no conflicts. 3,278 unit tests and detekt pass on the merge.
Pushed 07b5b6f: I merged the new head of #1401 (85f1963d3, no transport maintenance when opening subscription reminders) into this PR, keeping history and with no conflicts. 3,278 unit tests and detekt pass on the merge.
Pushed b7f4c8b: I merged the new head of #1401 (bb4637dd5, SDK queue waits excluded from the request discovery timeout) into this PR, keeping history and with no conflicts. I could not run the unit tests or detekt on this merge: my local Gradle dependency cache was wiped and the GitHub Packages dependencies cannot be re-resolved without credentials. The previous head 07b5b6f30 passed 3,278 tests and detekt; CI runs lint and detekt on this head.
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1352
Twin: synonymdev/bitkit-ios#799
Stacked on #1401
Refs:
This PR adds allowances: a payer sets a per-payment and a monthly limit for a Paykit contact, and that contact's requests within the limits are paid without asking.
Description
It is stacked on #1401, so it builds against the published Paykit
0.1.0-rc59that #1401 resolves from GitHub Packages. That release includes the allowance stack (pubky/paykit-rs#158 to #161) and the shared identity runtime, where one Encrypted Link serves a contact's identity and the allowance is bound to the two identities instead of to a receiver folder. Merge #1401 first; this PR's diff shrinks to the allowance work once it lands.Out of Scope
PaykitAllowanceExecutor.ktrecovery: a send stopped by a process kill stays at SENDING, so its request neither pays nor asks; follow-upPaykitAllowanceExecutor.ktrecovery: it settles every open attempt in the identity-wide ledger, which assumes Bitkit is the only app paying from it; follow-up before another app shares the ledgerLightningRepo.ktsync(): a cancellation is recorded as a sync error and starts the retry loop instead of being rethrown; follow-upDesign
Figma "Bitkit - Experimental New": Allowances empty state, Set Allowance, Allowances list. The receiver's review sheet and the detail sheet have no frame and follow the drawn film. Deliberate differences: the first tab stays "Overview", the list row's right column reads the monthly limit, and the copy typos are fixed.
Preview
side-by-side.mp4
QA Notes
Journeys
set-and-accept.xml— the offer opens on the payee by itself and both rows turn Activeauto-pay-under-limit.xml— a $2 request pays itself with only a Payment Executed notificationabove-limit-asks.xml— a $20 request arrives as an ordinary Payment Requestmonthly-cap-reached.xml— the third $4 request on a $10 cap raises Limit Reached and asksend-stops-auto-pay.xml— after either side ends it, the next request asksrestart-never-pays-twice.xml— a kill right after the hand-off never leads to a second paymentAutomated Checks
PaykitAllowanceTest.kt— limits, terms, capacity and month anchoring of the allowance model, and status and capacity on real time while the subscription clock offset is setPaykitAllowanceExecutorTest.kt— reserve, hand-off, outcome recording and restart recovery of automatic payments, the execution claim before the automatic acceptance, and admission on real time while the subscription clock offset is setPaykitAllowanceRepoTest.kt— admission decisions: automatic, manual, deferred and limit reached, one grant per contact identity, and coverage on real time while the subscription clock offset is setPrivatePaykitAllowancePaymentTest.kt— resolving the payee's request-bound private endpoint for an automatic paymentAllowancesViewModelTest.kt— list, set, review and detail statePaykitPaymentRequestRepoTest.kt— an automatic acceptance saves this install as the owner and drops it on a definite failureAppViewModelSendFlowTest.kt— the send flow with the allowance repo wired inPaykitPaymentProofRepoTest.kt— payment proofs carry the allowance id and the paying app./gradlew --offline testDevDebugUnitTest detektwithcom.synonym:paykit-android:0.1.0-rc59from a local build of thev0.1.0-rc59tag in place of GitHub Packages — 3,278 unit tests pass and detekt is cleanset-and-accept.xml,auto-pay-under-limit.xml,above-limit-asks.xmlandend-stops-auto-pay.xmlon two Android 15 emulators with fresh wallets against the Blocktank staging regtest LSP and the staging homegate, on0b1f2664abefore the last sync with feat: share paykit state across apps #1401 — all four pass; a $2 request paid itself over Lightning, a $20 request asked, and after an end the next request asked. Staging was slow: the automatic payment arrived about 5 minutes after the request, and two payment request sends timed out once before succeeding on retry. The review sheet opens on its own only when no intro sheet is on top, and the payer row of an ended allowance reads "Ended" whereend-stops-auto-pay.xmlexpects "Ended · $0.00 paid automatically"; the journey text is not updatedmonthly-cap-reached.xmlandrestart-never-pays-twice.xml; the cap and the restart rules are covered byPaykitAllowanceExecutorTest.kt