The Authorization Model
BLUF: The program implements permissioned pulls. A user signs once to grant a scoped permission; after that, the counterparty (merchant, employer, agent operator) submits each transfer, and the program checks that transfer against the granted limits on-chain, at execution time. The user never signs individual payments — and the counterparty can never exceed what was granted.
One signature grants a scoped permission; the program re-checks it on every pull, on-chain, before a single token moves.
The mental model: a card pre-authorization, on-chain
The closest familiar analogy is a card pre-auth. When a hotel puts a hold on your credit card, you sign once at check-in; the hotel can then capture charges later — but only up to the authorized amount, only to the hotel's merchant account, and only within the authorization window. You don't co-sign each minibar charge, and the hotel can't drain your account.
This program is that, with the card network replaced by an on-chain program:
| Card world | This program |
|---|---|
| You sign the pre-auth slip once | Subscriber signs subscribe (or creates a delegation) once |
| Hotel captures charges later | Merchant or whitelisted puller submits transfer_subscription |
| Network enforces the auth limit | Program enforces per-period / cumulative caps at transfer time |
| Capture goes only to the hotel's account | Destination allowlist — funds can only land where the plan says |
| Auth expires | end_ts / expires_at_ts checked on every pull |
| You can cancel | cancel_subscription / revokeDelegation, signed by you |
Who signs what
This is the single most important table in the guide:
| Action | Who signs | Who pays the fee |
|---|---|---|
| Create / update / delete a plan | Merchant (plan owner) | Merchant |
| Subscribe, cancel, resume | Subscriber | Subscriber |
| Create / revoke a fixed or recurring delegation | Delegator (the payer) | Delegator |
Each pull (transfer_subscription, transferFixed, transferRecurring) |
Merchant or one of ≤4 whitelisted pullers | The puller (~5,000 lamports per pull) |
Two consequences worth internalizing:
- The payer is passive after setup. A subscriber's wallet does nothing on billing day. If the charge doesn't happen, it's because the merchant's infrastructure didn't fire — see Running a Puller.
- Pull costs sit with the pull side. The merchant (or its puller) pays the transaction fee for every collection. Factor it into your unit economics.
How the program gates each pull
Underneath, one SubscriptionAuthority PDA per (user, mint) (seeds ["SubscriptionAuthority", user, tokenMint] — note the literal CamelCase seed string) becomes the delegate on the user's token account for that mint, and every transfer it signs must first pass the program's full check chain — walk through it below.
Static view (enable JavaScript for the interactive stepper):
a pull transaction passes, in order — ① accounts are program-owned →
② mint matches the plan → ③ plan not expired → ④ caller is owner or whitelisted
puller → ⑤ destination is on the plan's allowlist → ⑥ subscription's snapshotted
terms fingerprint matches the live plan (PlanTermsMismatch otherwise) →
⑦ subscription not cancelled → ⑧ period rollover applied, then per-period cap
checked → ⑨ state updated and CPI transfer signed by the SubscriptionAuthority
PDA → ⑩ SubscriptionTransferEvent emitted via self-CPI. Any failed
gate aborts the whole transaction; no partial pulls.
Every gate aborts the entire transaction on failure — there are no partial pulls. The full chain, in program order: program-owned check → mint match → plan-expiry check → caller authorization (owner | puller) → destination allowlist → check_plan_terms() fingerprint → cancellation check → period rollover + cap → state update → CPI transfer signed by the SubscriptionAuthority PDA → SubscriptionTransferEvent via self-CPI.
The u64::MAX approval paradox, explained honestly
Here's the part that makes wallet users nervous, so let's not dance around it.
When a user sets up their first delegation for a given token, the SubscriptionAuthority PDA is approved as the delegate on their token account with an amount of u64::MAX — the maximum possible value. A wallet UI that naively renders token approvals will display this as "unlimited spending approval," which looks terrifying.
The honest breakdown:
- At the token-program layer, the approval really is
u64::MAX. That's not spin; it's what's on the account. - But the SubscriptionAuthority PDA has no private key. Nobody can sign as it directly. The only code path that can produce its signature is the subscriptions program itself, via CPI — and that code path runs the full gate chain above before signing anything.
- So the effective spending power is the sum of the caps in your active Delegation PDAs, not
u64::MAX. No delegation, no spend. Cancelled delegation, no spend. Cap exhausted this period, no spend.
Why design it this way? One blanket approval means the user signs the token-level approve once per (user, mint) instead of re-approving for every new subscription, and lets the program manage many concurrent delegations through a single delegate slot (SPL token accounts only have one).
The caveat that remains true
You are trusting the program — its checks, and the audit that covered them — rather than the token-level approval amount. That's the actual trust statement, and it's why this guide has a Security Model page instead of a shrug. Wallet UIs that display raw approval amounts without program context will overstate the risk; integrators should explain this to users rather than hide it.
Recap: one signature grants a scoped permission; the program re-validates every pull against caps, destinations, expiry, and cancellation; the scary-looking u64::MAX is a key the program holds, gated by locks the user controls.
Sources for every claim on this page: About → Sources.