Protocol & internals
The on-chain accounts, the 402 handshake, the activation transaction, and how renewals work under the hood.
The plain-English version lives on the Concept page. This one zooms in on what actually happens on the wire and on-chain.
The protocol is the Solana profile of the MPP subscription intent — draft-solana-subscription-00 — implemented against an audited on-chain program.
Agent summary
- Activation is one HTTP round-trip with one signed transaction. Three on-chain instructions execute atomically.
- Renewal is server-driven; no 402, no client signature.
- Cancellation is purely on-chain. The server stops pulling at the next period boundary.
- The
subscriptionIdreturned inPayment-Receiptis the base58 of theSubscriptionDelegationPDA.
The three on-chain accounts
A subscription is held by an audited on-chain program. The program defines three PDAs that together encode the recurring authorization:
| PDA | Purpose | Created by |
|---|---|---|
| Plan | The merchant's published terms: mint, per-period amount, period length, authorized recipients, authorized pullers. Core fields are immutable once published. | Merchant, off the critical path of any 402 challenge. |
| Subscription Delegation | Per-subscriber snapshot of the plan terms plus the current billing-period accounting (start timestamp, amount already pulled in this period). The base58 of this PDA is the subscriptionId. | Subscriber, atomically with the first-period charge at activation time. |
| Subscription Authority | Per-(subscriber, mint) account holding the SPL Token delegate authority over the subscriber's token account. Reusable across multiple subscriptions on the same mint. | Subscriber, the first time they subscribe to anything on that mint. |
The Plan is the source of truth. The challenge the server emits at 402 time MUST match the plan's snapshotted terms exactly; clients verify the match before signing.
Activation
When an unauthenticated client hits a subscription endpoint, the server returns 402 Payment Required with intent="subscription". The client signs a single activation transaction that creates the delegation and collects the first-period charge atomically.
The 402 challenge
The challenge WWW-Authenticate header carries a base64url-encoded JSON request object. Decoded, it looks like:
{
"amount": "10000000",
"currency": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"periodUnit": "day",
"periodCount": "30",
"subscriptionExpires": "2027-01-15T12:00:00Z",
"recipient": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
"externalId": "8tWbqLkUJoYy7zXc5h2EvCRoaQEv2xnQjUuYhc3rzCgT",
"description": "Pro feed — monthly access",
"methodDetails": {
"programId": "De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44",
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"tokenProgram": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"decimals": 6,
"puller": "5fKb5cF22cFybZB1H4hLDydFhwoQy9JzKzRWaSbMkB6h",
"network": "mainnet",
"feePayer": true,
"feePayerKey": "5fKb5cF22cFybZB1H4hLDydFhwoQy9JzKzRWaSbMkB6h"
}
}| Field | Meaning |
|---|---|
amount | Per-period charge in mint base units (10000000 = 10 USDC). |
currency | Mint address. Must equal methodDetails.mint. |
periodUnit / periodCount | Together specify the billing period. month is not accepted. |
recipient | The wallet authorized to receive the charge. Must appear in plan.destinations. |
externalId | Base58 of the on-chain Plan PDA. Required for this intent (elevated from the shared spec). |
methodDetails.programId | Canonical subscriptions program. The client must pin this before signing. |
methodDetails.puller | The server wallet authorized to pull. Must be plan.owner or listed in plan.pullers. |
methodDetails.feePayer | When true, the server sponsors fees and co-signs the activation transaction. |
subscriptionExpires | Optional HTTP-layer cap. After this point the server stops renewing and serves fresh 402s. |
The client MUST fetch the on-chain Plan at externalId and verify that every snapshotted term (mint, per-period amount, mapped period) matches the challenge before signing.
The activation transaction
The signed transaction contains three subscriptions-program instructions in this order:
Instruction 0 (optional) SetComputeUnitLimit, SetComputeUnitPrice
Instruction 1 (conditional) subscriptions.initialize_subscription_authority
— only if the (subscriber, mint) authority doesn't yet exist on-chain
Instruction 2 subscriptions.subscribe
— creates SubscriptionDelegation snapshotting plan terms
Instruction 3 subscriptions.transfer_subscription
— pulls first-period charge from subscriber ATA to recipient ATA
Fee payer: methodDetails.feePayerKey when feePayer=true, otherwise the subscriber
Signers: subscriber (always), puller (added by the server at co-sign time)Any deviation — a stray SPL Approve, a transfer to an unauthorized recipient, instructions from other programs — and the server rejects the credential before broadcasting.
Pull mode vs push mode
The activation credential can take two shapes:
| Mode | payload.type | What the client submits | Who broadcasts |
|---|---|---|---|
| Pull (default) | transaction | Standard-base64 of the signed tx | Server |
| Push | signature | Base58 of the confirmed signature | Client |
Pull mode is the default and the only mode compatible with server-sponsored fees. Push mode requires feePayer: false; the server has no opportunity to co-sign a transaction the client already broadcast.
The receipt
On success the server returns 200 OK with a Payment-Receipt header. The receipt payload decodes to:
{
"method": "solana",
"intent": "subscription",
"status": "success",
"reference": "5J8...base58 transaction signature...Kt",
"subscriptionId": "BXQGmO5VwTrl5RfFr6Y8XQZ4nPj9QqMOiKkRn3pZ4ZE",
"externalId": "8tWbqLkUJoYy7zXc5h2EvCRoaQEv2xnQjUuYhc3rzCgT",
"periodIndex": "0",
"periodStartTs": "2026-01-15T12:03:10Z",
"periodEndTs": "2026-02-14T12:03:10Z",
"expiresAt": "2027-01-15T12:00:00Z",
"timestamp": "2026-01-15T12:03:10Z"
}The subscriptionId is stable across renewals. The client retains it locally; the server uses it as the lookup key for renewal accounting.
Renewal
Renewals are server-driven. There is no 402, no client signature, no HTTP traffic from the subscriber. When the renewal worker detects a billing-period boundary, it submits a single instruction to the subscriptions program.
The on-chain program enforces the rules:
- At most one period's worth of value can be pulled per billing period.
current_period_start_tsadvances by whole multiples of the period length when a transfer occurs after the period has elapsed.- Missed periods do not accumulate. If periods 3 and 4 elapse without a charge, the next successful
transfer_subscriptioncollapses to period 4 — the skipped period 3 is forfeit, not bankable.
The server SHOULD submit at most one transfer_subscription per billing period per subscription. If the transaction fails (insufficient balance, on-chain error), the server records the failure and serves 402 Payment Required on the next gated request.
Cancellation
Cancellation is out-of-band — the subscriber submits cancel_subscription directly to the program; no HTTP credential is involved. The program sets delegation.expires_at_ts to the end of the current billing period.
- Until that timestamp, the server continues to honor access.
- At that timestamp,
transfer_subscriptionstarts failing withSubscriptionCancelled. - Subsequent gated requests receive a fresh 402.
Subscribers can additionally revoke every subscription on a given mint at once by closing their SubscriptionAuthority — the authority is shared across all subscriptions on that mint, so closing it invalidates them simultaneously.
When the server returns 402 again
A previously-active subscription stops working under any of these conditions. In every case the server returns 402 Payment Required with a fresh challenge:
| Condition | Cause |
|---|---|
subscriptionExpires reached | HTTP-layer cap hit, server stops renewing. |
| On-chain cancellation effective | delegation.expires_at_ts ≤ now. |
SubscriptionAuthority closed | Subscriber revoked all subscriptions on that mint. |
Current billing period unpaid and transfer_subscription failed | Insufficient balance or other on-chain error. |
| Activation credential invalid | Replay, mismatched plan, scope outside allowed instrs. |
Clients receiving a 402 after a previously-valid subscription SHOULD treat the old subscription as no longer usable and start a new activation flow.
Specification
Full normative text: draft-solana-subscription-00. Companion documents: