Skip to content

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.

1 · ONCE Subscriber signs once subscribe() once per (user, mint) creates Two program accounts: SubscriptionAuthority PDA — the delegate Subscription PDA — caps · destination · expiry EVERY BILLING PERIOD ↻ 2 · EACH PULL Merchant / puller pays the tx fee submits transferSubscription() Program validates — at transfer time cap · destination · expiry · not-cancelled pass SubscriptionAuthority PDA signs the token CPI → tokens move · SubscriptionTransferEvent emitted any check fails The whole transaction reverts. No partial pulls — nothing moves, whoever the caller is.

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:

  1. 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.
  2. 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.

interactive — lifecycle of a $5 pull

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.