Skip to content

feat: add allowances for automatic paykit payments - #799

Draft
ovitrif wants to merge 35 commits into
codex/paykit-shared-runtime-local-20260930from
feat/paykit-allowances
Draft

ovitrif wants to merge 35 commits into
codex/paykit-shared-runtime-local-20260930from
feat/paykit-allowances

Conversation

@ovitrif

@ovitrif ovitrif commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #806
Twin: synonymdev/bitkit-android#1340
Stacked on #856
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 builds on the published Paykit SDK 0.1.0-rc59 that #856 moves Bitkit to, where an Allowance binds the two Pubky identities of a contact's single Encrypted Link and covers every Paykit app they use. It is stacked on #856; GitHub retargets it to master when #856 merges.

  • 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 contact's 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
  • Pays an incoming request within the limits automatically: the app claims the request, the SDK's allowance accounting reserves it before the send and the outcome is reported back afterwards, so a kill and relaunch mid-payment never pays twice
  • Keeps this device the owner of an auto-accepted request, so a payment that never reached the node comes back as an ordinary Payment Request after a restart
  • Keeps a request an allowance covers off the Send sheet until its automatic payment is decided, and stops the request presentation from spinning on it
  • Keeps a started automatic payment running when the caller that triggered it is cancelled, so a request that was accepted is not left unpaid
  • 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; requests that arrived before the allowance stay manual
  • Attributes automatically paid activity to the contact, tags those rows "Auto-paid" in the Payments tab and shows the amount paid automatically on the allowance detail
  • Counts allowance months from the original anchor, so the monthly cap resets on the same day each month
  • 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
  • Leaves wallets without an allowance ledger untouched at launch

Out of Scope

  • Allowance detail on the payee: "Paid automatically" counts the payer's own journal, so the payee sees $0.00. The ledger is the payer's by design; the payee can sum the received proofs tagged with the allowance id, which Bitkit does not do yet
  • Payment method: the executor pays the payee's bolt11 invoice, or an on-chain address when there is none. It checks no Lightning capacity before choosing bolt11, and a failed Lightning send does not fall back to on-chain, so the request becomes a manual Payment Request
  • Failed automatic send: the request is already accepted, so it falls back to a manual Payment Request and is not retried automatically
  • Granting to a contact without a ready link: the sheet reports that the contact is not linked and nothing is proposed. A grant covers the contact's one link, so there is no later link to extend it to

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

Set Allowance Allowances Detail Payments
Recording
auto-pay-clip.mp4

QA Notes

Journeys

  • new set-and-accept.xml — the offer opens on the payee by itself and both rows turn Active; ran on two iOS 26.5 simulators
  • new auto-pay-under-limit.xml — a $2 request pays itself, the Payments row reads Auto-paid and the detail shows $2.00 paid automatically; ran on two iOS 26.5 simulators
  • new above-limit-asks.xml — a $20 request arrives as an ordinary Payment Request, pays by swipe without the Auto-paid tag and stays out of the amount paid automatically; ran on two iOS 26.5 simulators
  • new monthly-cap-reached.xml — the third $4 request on a $10 cap stays an ordinary Payment Request and the detail reads $8.00 paid automatically; ran on two iOS 26.5 simulators, the Limit Reached notification itself was not observed
  • new end-stops-auto-pay.xml — after either side ends it, the next request asks; both halves ran on two iOS 26.5 simulators: the ender's row and the other side's row read Ended, the payer's with the amount paid automatically, and the next request waited as an ordinary Payment Request
  • new restart-never-pays-twice.xml — the payer was killed 0.4 s after the hand-off and relaunched: one payment left the wallet and the payee received one; ran on two iOS 26.5 simulators

Manual Tests

N/A

Automated Checks

  • added PaykitAllowanceTests.swift — limits, terms, capacity, month anchoring, grouping and admission decisions
  • added PaykitAllowanceExecutorTests.swift — reserve, hand-off, claim, payment list outcomes, cancellation of the caller, outcome recording, deferral, channel readiness, granting over a ready link and restart recovery of automatic payments
  • updated PaykitPaymentProofServiceTests.swift — payment proofs carry the allowance id
  • ran BitkitTests on an iOS 26.5 simulator against Paykit 0.1.0-rc59 at the current head — 1,626 tests pass, 12 skipped

ovitrif added 15 commits October 1, 2026 19:05
An Allowance now binds two identities, so a grant is one SDK Allowance on the
contact's single Encrypted Link and coverage no longer depends on a receiver
folder. Automatic payments claim the request before the automatic Acceptance,
pass the payee's payment app on the proof and settle the private payment list
with the same outcomes the manual flow uses.
A cancelled caller no longer stops an automatic payment after the request was
accepted: admission runs in its own task once the ledger is reconciled.
The shared request flow only shows an accepted request again while this device
owns its acceptance. Automatic payments now record that ownership before the
automatic Acceptance, so a payment that never reached the node returns as an
ordinary Payment Request.
@ovitrif
ovitrif force-pushed the feat/paykit-allowances branch from 1a0051f to 8f0449c Compare October 1, 2026 19:58
@ovitrif
ovitrif changed the base branch from feat/demo-clock-subscriptions to codex/paykit-shared-runtime-local-20260930 October 1, 2026 19:59
@ovitrif

ovitrif commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

I restacked this PR on #856 and pushed 8f0449c (draft, base codex/paykit-shared-runtime-local-20260930).

  • Rebased onto feat: share paykit state across apps #856 and dropped the demo clock commit, so the diff over feat: share paykit state across apps #856 is allowances only; the three tests that asserted allowances ignore the demo clock are gone
  • Removed the local ../paykit-rs reference and the rc55 state-layout shim; the build uses the Paykit 0.1.0-rc59 that feat: share paykit state across apps #856 resolves
  • Adapted allowances to the shared identity: one SDK Allowance per grant on the contact's single link, no receiver folders
  • Automatic payments now claim the request first, pass the payment app on the proof, settle the private payment list like the manual flow and keep this device as the request's owner
  • A cancelled caller no longer stops a payment after the request was accepted

Local verification: BitkitTests on an iOS 26.5 simulator at a593f0e: 1,611 tests pass, 12 skipped; the later merge of #856's head 61203c8 builds, with its test rerun pending. The device journeys and the Preview media wait for free disk on the simulator host.

…yment is decided

A request an allowance covers belongs to the automatic flow from the moment it
arrives. Before, a slow first pass let the incoming-request presentation open a
manual sheet for a request that was then paid automatically.
…owance flow owns

Skipping an owned request left it in the presentable list, so the presentation
re-entered itself on the main thread until the allowance pass finished. The pass
now logs how each covered request ended.
@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

I pushed a19ea87 with the device findings and the Preview media.

  • Fixed the request presentation re-entering itself on the main thread whenever the allowance flow owned a request, which froze the payer's UI until the automatic payment finished; found on the simulators
  • A covered request now stays off the Send sheet from the moment it arrives, until the automatic pass finds it manual or pays it; a manual sheet had opened for a request that was then paid automatically
  • Automatic payments record this device as the request's owner before the automatic Acceptance, so a payment that never reached the node comes back as an ordinary Payment Request
  • Merged the latest feat: share paykit state across apps #856 head (05c3234)

Device runs on two iOS 26.5 simulators against Paykit 0.1.0-rc59: set-and-accept.xml, auto-pay-under-limit.xml, restart-never-pays-twice.xml (killed 0.4 s after the hand-off, one payment out and one received) and the payee half of end-stops-auto-pay.xml pass. Local verification: BitkitTests 1,622 tests pass, 12 skipped, before the last #856 merge, which builds. The description has the screenshots and the recording.

@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

I ran the open checks on the current head (a19ea87), on two iOS 26.5 simulators against Paykit 0.1.0-rc59; no code changed.

  • BitkitTests: 1,626 tests pass, 12 skipped
  • above-limit-asks.xml: a $20 request arrived as an ordinary Payment Request, was paid by swipe, shows no Auto-paid tag and did not change the amount paid automatically
  • monthly-cap-reached.xml: on a $5 and $10 allowance, "Cap1" and "Cap2" ($4 each) paid themselves, "Cap3" stayed an ordinary Payment Request and the detail reads $8.00 paid automatically; the Limit Reached notification itself was not observable in the simulator
  • end-stops-auto-pay.xml, payer half: the payer's row reads "Ended · $8.01 paid automatically", the payee's reads Ended, and the next request waited as an ordinary Payment Request

The journeys are ticked in QA Notes; the manual test list is now N/A.

The clock offset from the demo clock PR is in the base now, so the three tests
that assert allowance admission, coverage and status keep real time return.
@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

I merged the latest #856 head (225ec3e, which brings in #798 and master) and pushed the result.

  • The clean merge brought the subscription clock offset into the base, so the three tests that assert allowance admission, coverage and status keep real time are back, now against SubscriptionClock
  • No other change to the allowance code

Local verification: BitkitTests 1,648 tests pass, 12 skipped. One earlier run of the same build showed 5 failures (1 unexpected) that did not repeat; I did not keep that log, so I could not name the test.

@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

I merged the latest #856 head (ac90e65) and pushed the result; the merge is clean and the allowance code is unchanged.

Local verification: BitkitTests 1,650 tests pass, 12 skipped.

@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

#856 merged master again (48b6b8f, the contacts and profile fix); I merged it and pushed. The merge is clean and does not touch the allowance code; it builds, and I did not rerun the tests for it.

@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Correction to my last comment: the test target does not build after that merge (33dcf4b), so "it builds" was wrong.

The app target and the allowance code are unaffected; the last full BitkitTests run (1,650 pass) was on ac90e65 before this master merge.

@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

#856 fixed its test build and I merged its latest head (f2d69b3) and pushed 6658b0f; the merge is clean and the allowance code is unchanged.

Local verification: build and the full BitkitTests suite on the pushed head: 1,666 tests pass, 12 skipped.

@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

I merged the latest #856 head (0e7da00, "stop redundant paykit polling") and pushed 7eda1ec; the merge is clean and the allowance code is unchanged.

Local verification: build and the full BitkitTests suite on the pushed head: 1,667 tests pass, 12 skipped.

@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

I merged the latest #856 head (c5ae314, "guard paykit payment authorization and cleanup") and pushed 6a97572; the merge is clean and the allowance code is unchanged.

Local verification: build and the full BitkitTests suite on the pushed head: 1,668 tests pass, 12 skipped.

@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

I merged the latest #856 head (f8f45ca, "yield between paykit handshake advances") and pushed e53eaae; the merge is clean and the allowance code is unchanged.

Local verification: build and the full BitkitTests suite on the pushed head: 1,669 tests pass, 12 skipped.

@jvsena42
jvsena42 added this pull request to stack #866 October 2, 2026 16:44
…l-20260930' into feat/paykit-allowances

# Conflicts:
#	Bitkit/AppScene.swift
@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

I merged the latest #856 head (d78aaa3, "consolidate paykit background retries") and pushed 32d00d7.

  • One conflict in AppScene.swift: feat: share paykit state across apps #856 removed the initialPaykitSyncGeneration state next to the allowance manager's, so I kept only paykitAllowanceManager; nothing else uses the removed state

Local verification: build and the full BitkitTests suite on the pushed head: 1,668 tests pass, 12 skipped.

…l-20260930' into feat/paykit-allowances

# Conflicts:
#	Bitkit/AppScene.swift
@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

I merged the latest #856 head (dc785b4, "receive Paykit messages on each inbox poll") and pushed bb6b941.

  • One conflict in AppScene.swift: feat: share paykit state across apps #856 renamed the refresh parameter to processOutgoingMessages; the incoming-request refresh keeps the allowance pass and passes processOutgoingMessages: refreshMaintenance, and the refresh after an automatic payment passes false

Local verification: build and the full BitkitTests suite on the pushed head: 1,668 tests pass, 12 skipped.

@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

I merged the latest #856 head (89c4fd8, "report private endpoint withdrawal failures") and pushed 8eac259; the merge is clean and the allowance code is unchanged.

Local verification: build and the full BitkitTests suite on the pushed head: 1,668 tests pass, 12 skipped.

…l-20260930' into feat/paykit-allowances

# Conflicts:
#	Bitkit/AppScene.swift
@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

I merged the latest #856 head (27e921a, "coalesce paykit preparation and reduce inbox polling") and pushed fe1ef54.

  • One conflict in AppScene.swift: feat: share paykit state across apps #856 changed the refresh parameter name again; the incoming-request refresh keeps the allowance pass and passes syncPrivateMessages: refreshMaintenance, with the refresh after an automatic payment passing false

Local verification: build and the full BitkitTests suite on the pushed head: 1,675 tests pass, 12 skipped.

…l-20260930' into feat/paykit-allowances

# Conflicts:
#	Bitkit/AppScene.swift
@ovitrif

ovitrif commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

I merged the latest #856 head (f5ca801, "avoid contact preparation waits for subscription notifications") and pushed 2073233.

  • One conflict in AppScene.swift: feat: share paykit state across apps #856 moved the subscription notification handling to the start of the sign-in task, where the allowance manager's activation also sits; I kept both, with the activation first

Local verification: build and the full BitkitTests suite on the pushed head: 1,681 tests pass, 12 skipped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add allowances for automatic paykit payments

1 participant