| Internet-Draft | Solana Subscription | October 2026 |
| Galabru | Expires 29 April 2027 | [Page] |
This document defines the Solana profile of the "subscription" payment intent for use with the Payment HTTP Authentication Scheme. It specifies how clients grant servers permission to collect a fixed SPL token payment once per billing period using a subscription delegation held by an audited on-chain program. This profile also defines a reusable payer-signed bearer proof for access under an active subscription. It intentionally models the recurring transfer authorization itself, not a richer billing object.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 29 April 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document.¶
This document may not be modified, and derivative works of it may not be created, except to format it for publication as an RFC or to translate it into languages other than English.¶
The "subscription" intent on Solana represents a recurring fixed-amount SPL token payment. The client grants the server a recipient-scoped subscription delegation with a per-period spending limit. Activation creates the delegation and collects the first billing-period charge in the same transaction.¶
This intent is useful for recurring API plans, content subscriptions, and other Solana-priced services with a stable amount per billing period.¶
This profile is intentionally narrower than a general billing subscription. It standardizes a recurring token-transfer authorization, not price catalogs, quantities, prorations, deferred starts, or billing-anchor resets.¶
This document inherits the shared request semantics of the
"subscription" intent from [I-D.payment-intent-subscription]. It
defines only the Solana-specific methodDetails, payload,
activation transaction shape, on-chain lifecycle, and verification
procedures for the "solana" payment method.¶
Solana subscriptions support only subscription-delegation
fulfillment. Solana transactions containing standalone SPL Token
Approve instructions and push-mode hash credentials do not provide
the per-period enforcement required for this intent.¶
Solana also imposes an additional constraint that is not part of the
shared intent: the recurring authorization MUST be created against a
Plan account that the merchant has published on-chain prior to the
challenge. This method therefore requires the base58 address of that
on-chain plan in methodDetails.planAddress. The shared externalId
field retains its meaning as the merchant's reference for the
subscription.¶
Solana subscriptions also require the per-period spending limit, recipient scoping, and missed-period non-accumulation described in this document, including the Token-2022 mint constraints in Section 9.10. Servers MUST reject request objects on deployments that cannot enforce those restrictions.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
This document uses the terms Subscription, Billing Period,
Activation, Renewal, Cancellation, and Subscription Identifier
as defined by [I-D.payment-intent-subscription]. The following
additional terms are specific to the Solana profile.¶
Solana's standard token program. SPL Token-2022 is its extension- enabled successor and is supported subject to Section 9.10.¶
The audited on-chain program implementing the account model and instructions referenced by this specification [SUBSCRIPTIONS-PROGRAM].¶
An on-chain PDA published by the merchant that defines
subscription terms and lifecycle controls. Core billing terms --
token mint, amount per billing period, period length, creation
timestamp, and recipient destinations -- are immutable, while
status, end timestamp, allowed pullers, and metadata URI are mutable
until the plan is sunset. Derived from
["plan", owner, plan_id].¶
The plan owner or an address in the on-chain Plan.pullers array
that signs a transfer_subscription instruction. This profile uses
the program's puller name for the server-controlled signing
address authorized to collect subscription charges.¶
A per-subscriber on-chain PDA that snapshots the plan terms at
subscription time and tracks current-period accounting state.
Derived from ["subscription", plan_pda, subscriber].¶
A per-(subscriber, mint) on-chain PDA that holds the SPL Token
delegate authority over the associated token account for that
subscriber. The subscriptions program signs transfers as this PDA.
Derived from ["SubscriptionAuthority", subscriber, mint].¶
The "subscription" intent on Solana represents a request for a
recurring fixed-amount SPL token payment of amount, charged once
per billing period until explicit cancellation, until the on-chain
subscription delegation is otherwise invalidated, or until the
optional subscriptionExpires timestamp is reached.¶
On Solana, the recurring authorization is held by an audited on-chain
program [SUBSCRIPTIONS-PROGRAM]. Servers MUST pin its deployment
address in methodDetails.subscriptionProgram, and clients MUST validate that
address before signing. The program defines three on-chain accounts
referenced by this specification:¶
Plan, published off the critical path of the 402 challenge by the merchant. The plan's core terms (mint, amount per billing period, period length, destinations) are immutable once published.¶
SubscriptionDelegation, created when the subscriber activates a subscription. The delegation snapshots the plan's terms and tracks the current billing-period start time and the amount already pulled in the current period.¶
SubscriptionAuthority, created per (subscriber, mint) and
acting as the on-chain delegate over the subscriber's token
account.¶
The program enforces per-period spending limits, recipient scoping,
and missed-period non-accumulation. When a transfer occurs after one
or more billing periods have elapsed, renewals advance the current
billing-period start by whole multiples of the period length and reset
the in-period counter to zero. Although the on-chain primitive permits
partial transfers up to a per-period cap, this HTTP profile permits
only one transfer of exactly amount per billing period.¶
| Property | Value |
|---|---|
| Intent Identifier |
subscription
|
| Payment Timing | Recurring (activation charge atomic with delegation creation, then once per period via server-driven pulls) |
| Idempotency | Activation credential single-use; bearer proof reusable while the subscription remains usable; on-chain delegation reusable across billing periods |
| Reversibility | Cancellable on-chain; effective at end of currently-paid billing period |
The following diagram illustrates the Solana subscription flow:¶
Client Server Solana
│ │ │
│ (1) GET /api/resource │ │
│--------------------------> │ │
│ │ │
│ (2) 402 Payment Required │ │
│ intent="subscription" │ │
│<-------------------------- │ │
│ │ │
│ (3) Sign activation tx │ │
│ + bearer proof │ │
│ │ │
│ (4) Authorization: Payment │ │
│--------------------------> │ │
│ │ │
│ │ (5) Sign as puller and │
│ │ broadcast │
│ │ subscription delegation│
│ │ + first transfer │
│ │--------------------------> │
│ │ │
│ (6) 200 OK + Receipt │ │
│<-------------------------- │ │
│ │ │
│ (7) GET /api/resource │ │
│ + bearer proof │ │
│--------------------------> │ │
│ │ │
│ ... later period ... │ │
│ │ │
│ │ (8) transfer_subscription │
│ │ (server-driven pull) │
│ │--------------------------> │
│ │ │
│ (9) 200 OK + Receipt │ │
│<-------------------------- │ │
│ │ │
¶
The request parameter in the WWW-Authenticate challenge contains a
base64url-encoded JSON object. The request JSON MUST be serialized
using JSON Canonicalization Scheme (JCS) [RFC8785] and
base64url-encoded without padding per [I-D.httpauth-payment].¶
The currency field on Solana is the base58-encoded mint address of
an SPL Token or SPL Token-2022 mint:¶
| Format | Example | Description |
|---|---|---|
| Token mint |
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
Base58 address of an SPL Token or Token-2022 mint |
Implementations MUST treat currency and methodDetails.mint as the
same value; servers MUST reject request objects where they differ.
Native SOL is not a valid currency for the "subscription" intent on
Solana.¶
Base58 values in this profile use the standard Solana alphabet [BASE58]. Address comparisons are by decoded value, not raw string form.¶
All Solana-specific request parameters live in methodDetails:¶
| Field | Type | Required | Description |
|---|---|---|---|
methodDetails.mint
|
string | REQUIRED | Mint address echoed from currency. MUST equal the on-chain plan.mint
|
methodDetails.decimals
|
number | REQUIRED | Decimal precision of the mint |
methodDetails.tokenProgram
|
string | REQUIRED | Token program ID. The value MUST be the SPL Token program or the SPL Token-2022 program |
methodDetails.puller
|
string | REQUIRED | Base58 of the server's puller pubkey. MUST be plan.owner or appear in plan.pullers
|
methodDetails.planAddress
|
string | REQUIRED | Base58 address of the on-chain Plan account |
methodDetails.subscriptionProgram
|
string | REQUIRED | Base58 address of the subscriptions program deployment |
methodDetails.network
|
string | OPTIONAL |
"mainnet", "devnet", "testnet", or "localnet". If omitted, the default value is "mainnet"
|
methodDetails.feePayer
|
boolean | OPTIONAL | If true, the client constructs the activation transaction with the server as fee payer |
methodDetails.feePayerKey
|
string | OPTIONAL | Base58 of the server fee-payer pubkey. REQUIRED when feePayer is true
|
methodDetails.recentBlockhash
|
string | OPTIONAL | Pre-fetched blockhash to bind to the activation transaction |
Servers MUST reject request objects where the on-chain Plan at
methodDetails.planAddress does not exist, has been closed, is not
owned by methodDetails.subscriptionProgram, or has terms that diverge from
currency, amount, or the mapped per-billing-period interval.
Servers MUST also verify that the mint account is owned by
methodDetails.tokenProgram and that methodDetails.decimals matches
the decimal precision recorded in the mint account.¶
The non-zero entries in plan.destinations MUST consist of exactly
one address, and that address MUST equal recipient. Servers and
clients MUST reject request objects referencing a plan with an empty
destination whitelist, more than one non-zero destination, or a
destination different from recipient. The subscriptions program
compares this whitelist against the owner of the receiver token
account on every transfer, so this constraint binds activation and
all renewals to the advertised recipient.¶
This section is non-normative.¶
The Solana profile is a deliberately narrow projection of what an on-chain subscription system could express. Implementations should:¶
Treat the on-chain Plan as the authoritative source of recurring
terms. The challenge request MUST agree with the plan exactly;
servers should reject mismatched challenges before any client
signing.¶
Refuse to map periodUnit="month" rather than approximate it with
30-day or 31-day fixed periods. Clients receiving a month request
for the Solana method should treat it as a server bug.¶
Publish a separate Plan for each recipient. This profile requires
exactly one non-zero Plan.destinations entry so that the program
can enforce the challenge's recipient on every transfer.¶
Submit at most one transfer_subscription per billing period per
subscription, and never retry past the end of the period the
transaction was constructed against.¶
Maintain durable server state sufficient to prevent duplicate charges across retries, concurrent requests, and out-of-band on- chain events such as failed renewals or independent cancellations.¶
{
"amount": "10000000",
"currency": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"periodUnit": "day",
"periodCount": "30",
"subscriptionExpires": "2026-07-14T12:00:00Z",
"recipient": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
"description": "Monthly Pro plan",
"externalId": "merchant-subscription-270",
"methodDetails": {
"subscriptionProgram": "De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44",
"planAddress": "8tWbqLkUJoYy7zXc5h2EvCRoaQEv2xnQjUuYhc3rzCgT",
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"tokenProgram": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"decimals": 6,
"puller": "5fKb5cF22cFybZB1H4hLDydFhwoQy9JzKzRWaSbMkB6h",
"network": "mainnet",
"feePayer": true,
"feePayerKey": "5fKb5cF22cFybZB1H4hLDydFhwoQy9JzKzRWaSbMkB6h"
}
}
¶
The credential in the Authorization header contains a base64url-
encoded JSON object per [I-D.httpauth-payment].¶
The credential payload for a Solana "subscription" intent either
activates the subscription or authenticates later use of an active
subscription. Renewals are server-driven on-chain transactions and do
not produce HTTP credentials. Cancellations are out-of-band on-chain
operations and use no credential.¶
| Field | Type | Required | Description |
|---|---|---|---|
challenge
|
object | REQUIRED | Echo of the challenge from the server |
payload
|
object | REQUIRED | Solana-specific activation or access payload |
source
|
string | OPTIONAL | Subscriber identifier (e.g., did:pkh:solana:...) |
Solana subscription activation uses type="transaction". The client
signs the activation transaction as the subscriber and submits the
serialized bytes. The server verifies the transaction, signs it as
methodDetails.puller, adds the fee-payer signature when
methodDetails.feePayer is true, and broadcasts it.¶
| Field | Type | Required | Description |
|---|---|---|---|
type
|
string | REQUIRED | The string "transaction"
|
transaction
|
string | REQUIRED | Standard-base64 of the activation transaction signed by the subscriber. The server adds the puller signature and, when configured, its fee-payer signature |
authentication
|
object | REQUIRED | Reusable payer proof defined in Section 6.2 |
The client signs one reusable proof when it constructs the activation credential. The proof authorizes access under the resulting subscription; it does not authorize activation, a renewal transfer, or any other movement of funds.¶
The client derives the SubscriptionDelegation PDA from the plan and
subscriber, then constructs this JCS [RFC8785] object:¶
{
"domain": "mpp-subscription-auth-v1",
"payer": "<base58 subscriber public key>",
"subscriptionChallengeId": "<activation challenge id>",
"subscriptionDelegation": "<base58 delegation address>"
}
¶
The client signs the UTF-8 bytes of the JCS serialization with the
Ed25519 private key corresponding to payer. The resulting
authentication object has this shape:¶
| Field | Type | Required | Description |
|---|---|---|---|
type
|
string | REQUIRED | The string "proof"
|
challengeId
|
string | REQUIRED | Activation challenge ID signed as subscriptionChallengeId
|
payer
|
string | REQUIRED | Base58 Ed25519 public key that produced the proof |
signature
|
string | REQUIRED | Base58 encoding of the 64-byte Ed25519 signature |
During activation, the server MUST verify that:¶
authentication.challengeId equals the activation challenge ID;¶
the signed subscriptionDelegation equals the canonical PDA derived
from methodDetails.planAddress, the subscriber, and
methodDetails.subscriptionProgram;¶
authentication.payer equals the subscriber signer extracted from
the activation transaction; and¶
the Ed25519 signature verifies over the exact JCS object above.¶
The server MUST bind the verified authentication object to the
subscription only after the activation transaction confirms and the
resulting delegation passes the post-settlement checks in
Section 7.1. A signature over a derivable PDA whose account does not
exist is not evidence of an active subscription.¶
To use an active subscription on a later request, the client sends a
credential whose outer challenge echoes the activation challenge and
whose payload has this shape:¶
{
"type": "proof",
"subscriptionDelegation": "<base58 delegation address>",
"authentication": {
"type": "proof",
"challengeId": "<activation challenge id>",
"payer": "<base58 subscriber public key>",
"signature": "<base58 Ed25519 signature>"
}
}
¶
The server MUST verify the proof against the delegation and the
activation binding before granting access. It MUST require the same
authentication field values that it bound at activation. The
activation challenge's
expires auth-param limits creation of the binding; it does not expire
an already-bound proof. Repeated presentation is the expected bearer-
proof behavior and MUST NOT be rejected merely because the proof was
previously presented.¶
The proof remains usable only while the identified subscription is
usable under Section 7.4. It becomes invalid
when cancellation takes effect, subscriptionExpires is reached, the
delegation or its authority is invalidated, or the current billing
period is not paid. Servers MAY provide application-layer early
revocation or narrower audience and lifetime policies.¶
The proof is a bearer credential after signing. Clients and servers
MUST send it only over authenticated encrypted transports, MUST NOT log
it, and MUST prevent it from appearing in URLs, analytics, or error
reports. An Idempotency-Key prevents replay of an application request;
it does not constrain use of a leaked bearer proof.¶
The subscriber-signed activation transaction MUST:¶
contain a subscribe instruction creating the
SubscriptionDelegation PDA from the on-chain Plan snapshot;¶
contain a transfer_subscription instruction collecting the first-
period charge atomically with the subscription creation;¶
contain an initialize_subscription_authority instruction prepended
only when the (subscriber, mint) authority does not yet exist
on-chain;¶
target the subscriptions program identified by
methodDetails.subscriptionProgram;¶
use the SPL Token or Token-2022 program identified by
methodDetails.tokenProgram for all token-touching instructions;¶
pull funds from the subscriber's associated token account for
methodDetails.mint;¶
use methodDetails.puller as the required signer for the
transfer_subscription instruction;¶
direct the first-period charge to a receiver ATA whose token-account
owner equals recipient;¶
set the fee payer to methodDetails.feePayerKey when
methodDetails.feePayer is true, and to the subscriber otherwise;¶
contain no instructions other than those above plus optional compute-budget and memo instructions.¶
The subscriber-signed activation transaction MUST NOT contain SPL
Token Approve or any other non-subscriptions-program instruction
that could move the subscriber's tokens outside the per-period limit,
and MUST NOT reference writable accounts that could redirect funds
away from recipient.¶
Each "subscription" activation credential MUST be usable only once per challenge. Servers MUST reject replayed credentials.¶
This single-use requirement applies to type="transaction"
activation credentials. It does not apply to a type="proof" access
credential defined in
Section 6.2.¶
A successfully activated subscription may be reused for later billing periods until:¶
For intent="subscription", activation and the first billing-period
charge are a single atomic transaction:¶
Client Server Solana
| | |
| (1) Authorization: | |
| Payment <credential> | |
| (subscriber-signed tx) | |
|--------------------------> | |
| | |
| | (2) Sign as puller and, |
| | if configured, fee |
| | payer; sendTransaction |
| |--------------------------> |
| | |
| | (3) SubscriptionDelegation |
| | created + first |
| | transfer executed |
| |<-------------------------- |
| | |
| (4) 200 OK | |
| Payment-Receipt: ... | |
|<-------------------------- | |
| | |
¶
When the server receives a Solana "subscription" credential, it MUST:¶
Verify the activation transaction matches the challenge: program ID, plan address, token program, mint, puller, recipient, and per-period amount as described in Section 7.1.2.¶
Verify the subscriber identity per Section 7.1.1.¶
Verify and retain the subscription bearer proof per Section 6.2.¶
Sign the transaction as methodDetails.puller. When
methodDetails.feePayer is true, also sign as
methodDetails.feePayerKey, then broadcast.¶
Wait for confirmation and read the resulting on-chain state.¶
Initialize durable subscription state, including the exact bound proof, for later access and renewals.¶
Return 200 OK with a Payment-Receipt for the first charge,
including a subscriptionId as defined in
Section 7.3.¶
Servers MUST treat the subscription as active only after the
activation transaction settles successfully and the on-chain
SubscriptionDelegation account reflects
amount_pulled_in_period == amount_per_period for period 0.¶
Servers MUST NOT treat activation as successful if
subscriptionExpires is present and the activation transaction
settles at or after that timestamp.¶
If a credential includes the optional source field, servers MUST NOT trust this value without verification.¶
Servers MUST determine the payer identity from the activation
transaction itself, by extracting the subscriber signer (the signer
whose role corresponds to the delegator field of the resulting
SubscriptionDelegation).¶
If source is present, servers MUST verify that it uses the
did:pkh:solana: method and that its address matches the subscriber
signer extracted from the activation transaction.¶
For each later billing period, the server MAY submit one
transfer_subscription transaction using the registered subscription
delegation. The transaction MUST pull exactly amount to a receiver
ATA whose token-account owner equals the recipient bound at
activation.¶
If the server grants access for a later billing period, it MUST ensure that the renewal charge for that period has been collected before, or atomically with, delivering the corresponding service.¶
Servers MUST NOT submit more than one successful renewal charge for the same billing period and MUST NOT use the program's partial-transfer capability for this profile.¶
The on-chain transfer_subscription instruction advances
current_period_start_ts by whole multiples of the period length and
resets amount_pulled_in_period to zero when the period rolls over.
The program then accepts transfers up to the remaining per-period cap.
Conforming servers MUST use that broader primitive only to collect one
transfer of exactly amount in the current billing period. If one or
more billing periods elapse without a successful charge, a later
transaction authorizes only the then-current period's charge; missed
periods do not accumulate.¶
After successful activation, the server MUST return a
subscriptionId in the Payment-Receipt. The value MUST be a
server-issued opaque base64url string without padding as defined by
[I-D.payment-intent-subscription]. The server MUST also return the
base58 address of the on-chain SubscriptionDelegation account in the
Solana-specific subscriptionDelegation receipt field.¶
Servers MUST NOT include a Payment-Receipt header on error
responses. On renewal, servers MUST return the same subscriptionId
for the active subscription.¶
The receipt payload for a Solana subscription:¶
| Field | Type | Description |
|---|---|---|
method
|
string |
"solana"
|
reference
|
string | Base58 of the settlement transaction signature |
status
|
string |
"success"
|
subscriptionId
|
string | Server-issued opaque base64url identifier for the subscription |
subscriptionDelegation
|
string | Base58 address of the SubscriptionDelegation account |
periodIndex
|
number | Zero-based integer index of the billing period (0 on activation) |
periodStart
|
string | [RFC3339] start of the current period |
periodEnd
|
string | [RFC3339] end (exclusive) of the current period |
expiresAt
|
string | OPTIONAL. [RFC3339] effective subscription expiry |
timestamp
|
string | [RFC3339] settlement time |
externalId
|
string | OPTIONAL. Echoed from the challenge request |
Clients MAY retain the subscriptionId as application data when
referring to the active subscription in later interactions, but the
subscriptionId is only a receipt identifier unless an application
explicitly assigns it additional application-layer meaning.¶
Clients MUST use the bearer proof from
Section 6.2 to authenticate later access. Possession
or presentation of a subscriptionId alone is insufficient.¶
The billing anchor for a Solana subscription is the on-chain
current_period_start_ts written by subscribe when the activation
transaction settles. Servers MUST derive this anchor from chain
settlement data rather than local wall-clock time.¶
Billing periods are defined as:¶
Period 0: [anchor, anchor + mappedPeriodSeconds)¶
Period 1: [anchor + mappedPeriodSeconds, anchor + 2*mappedPeriodSeconds)¶
Period N: [anchor + N*mappedPeriodSeconds, anchor + (N+1)*mappedPeriodSeconds)¶
mappedPeriodSeconds is period_hours * 3600 for the on-chain
delegation.¶
Servers MUST maintain durable subscription state sufficient to enforce per-period charging rules across retries and concurrent requests. At minimum, servers MUST track:¶
server-issued subscription identifier¶
subscription delegation address¶
activation challenge and bound bearer proof¶
plan address¶
billing anchor¶
last successfully charged billing-period index¶
any in-flight billing-period index and renewal transaction signature¶
subscription expiry¶
cancellation timestamp (delegation.expires_at_ts; 0 means active,
non-zero means cancellation is scheduled, and cancellation is effective
once the current time reaches that timestamp)¶
When granting access in a later billing period, servers MUST:¶
Verify the payer signature, activation binding, and delegation binding defined in Section 6.2.¶
Verify the SubscriptionDelegation account exists, is owned by
methodDetails.subscriptionProgram, has the expected discriminator and version,
and still identifies the bound plan and payer.¶
Derive the payer's SubscriptionAuthority from the payer and mint, verify
that it exists and is owned by methodDetails.subscriptionProgram, and
require its current init_id to equal the delegation header's init_id.
A missing authority or mismatched incarnation invalidates the subscription.¶
Verify the subscription is still usable by reading
delegation.expires_at_ts on-chain; a non-zero timestamp only blocks
renewal once the current time is at or after that timestamp.¶
Determine the current billing-period index from the anchor and the mapped period in seconds.¶
Verify that delegation.amount_pulled_in_period is zero before
submitting a renewal for the current billing period.¶
Atomically record any renewal attempt for the current billing
period as in-flight before submitting transfer_subscription.¶
Mark the current billing period as charged only after the renewal
transaction settles successfully and the on-chain
delegation.amount_pulled_in_period equals amount.¶
Grant access only after, or atomically with, durably recording the successful renewal charge.¶
For non-idempotent requests, clients SHOULD send an Idempotency-Key
header per [I-D.ietf-httpapi-idempotency-key-header]. Servers MUST NOT charge the same activation or billing period more than once for
duplicate idempotent requests.¶
Subscribers cancel a subscription by submitting
cancel_subscription against their
SubscriptionDelegation. The normal cancellation path sets
delegation.expires_at_ts to the end of the current billing period,
after which transfer_subscription fails with
SubscriptionCancelled. Implementations should also handle
program-defined immediate-expiry cases, such as closed or mismatched
plan accounts.¶
Subscribers can additionally revoke every subscription tied to a
(subscriber, mint) by closing and reopening their
SubscriptionAuthority. Reopening creates a new authority instance,
causing existing subscriptions that reference the prior authority
instance to fail transfer validation.¶
Servers MUST NOT submit renewal charges for billing periods after cancellation takes effect. Servers SHOULD handle revocation gracefully by returning a fresh subscription challenge once the on-chain delegation has expired.¶
When a Solana subscription cannot be used to fulfill a request, the server MUST return an appropriate HTTP status code:¶
| Condition | Status Code | Behavior |
|---|---|---|
subscriptionExpires present and reached |
402 Payment Required | Issue new challenge |
On-chain cancellation effective or SubscriptionAuthority closed |
402 Payment Required | Issue new challenge |
Current billing period unpaid or transfer_subscription failed |
402 Payment Required | Issue new challenge |
| Activation transaction failed verification | 402 Payment Required | Issue new challenge |
| Invalid credential | 402 Payment Required | Issue new challenge |
For all 402 responses, the server MUST include a WWW-Authenticate
header with a fresh challenge. Clients receiving a 402 after a
previously valid subscription SHOULD treat the subscription as no
longer usable and initiate a new subscription flow.¶
This section is non-normative.¶
Challenge:¶
HTTP/1.1 402 Payment Required Cache-Control: no-store WWW-Authenticate: Payment id="qT8wErYuI3oPlKjH6gFdSa", realm="api.example.com", method="solana", intent="subscription", expires="2026-01-15T12:05:00Z", request="<base64url-encoded JSON below>"¶
The request decodes to:¶
{
"amount": "10000000",
"currency": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"periodUnit": "day",
"periodCount": "30",
"subscriptionExpires": "2026-07-14T12:00:00Z",
"recipient": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
"description": "Monthly Pro plan",
"externalId": "merchant-subscription-270",
"methodDetails": {
"subscriptionProgram": "De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44",
"planAddress": "8tWbqLkUJoYy7zXc5h2EvCRoaQEv2xnQjUuYhc3rzCgT",
"mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"tokenProgram": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"decimals": 6,
"puller": "5fKb5cF22cFybZB1H4hLDydFhwoQy9JzKzRWaSbMkB6h",
"network": "mainnet",
"feePayer": true,
"feePayerKey": "5fKb5cF22cFybZB1H4hLDydFhwoQy9JzKzRWaSbMkB6h"
}
}
¶
This requests a recurring payment of 10.00 USDC every 30 days until 2026-07-14T12:00:00Z, with the server (5fKb...kB6h) sponsoring transaction fees.¶
Activation transaction (composed by client):¶
Instruction 0: SetComputeUnitLimit(200000)
Instruction 1: SetComputeUnitPrice(1)
Instruction 2: subscriptions.initialize_subscription_authority
signer: subscriber
accounts: [authority_pda, subscriber, mint, ata, ...]
Instruction 3: subscriptions.subscribe
signer: subscriber
accounts: [plan_pda, subscription_pda, authority_pda,
subscriber, system_program]
Instruction 4: subscriptions.transfer_subscription
signer: puller
accounts: [subscription_pda, plan_pda, authority_pda,
subscriber_ata, destination_ata, mint,
token_program]
Fee payer: 5fKb5cF22cFybZB1H4hLDydFhwoQy9JzKzRWaSbMkB6h (server)
Signatures: subscriber (partial), puller (server, added at co-sign)
Destination ATA owner: 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin
¶
Credential:¶
{
"challenge": {
"id": "qT8wErYuI3oPlKjH6gFdSa",
"realm": "api.example.com",
"method": "solana",
"intent": "subscription",
"request": "eyJ...",
"expires": "2026-01-15T12:05:00Z"
},
"payload": {
"type": "transaction",
"transaction": "AQAAAA...base64 of partially signed tx...",
"authentication": {
"type": "proof",
"challengeId": "qT8wErYuI3oPlKjH6gFdSa",
"payer": "7YWHMfk9JZe0LMQDBiTkBXK9LmJRxB6Kuf7nBzvuN7tP",
"signature": "4vJ9...base58 Ed25519 signature...Qd"
}
}
}
¶
Later access credential:¶
{
"challenge": {
"id": "qT8wErYuI3oPlKjH6gFdSa",
"realm": "api.example.com",
"method": "solana",
"intent": "subscription",
"request": "eyJ...",
"expires": "2026-01-15T12:05:00Z"
},
"payload": {
"type": "proof",
"subscriptionDelegation": "BXQGmO5VwTrl5RfFr6Y8XQZ4nPj9QqMOiKkRn3pZ4ZE",
"authentication": {
"type": "proof",
"challengeId": "qT8wErYuI3oPlKjH6gFdSa",
"payer": "7YWHMfk9JZe0LMQDBiTkBXK9LmJRxB6Kuf7nBzvuN7tP",
"signature": "4vJ9...base58 Ed25519 signature...Qd"
}
}
}
¶
Receipt:¶
If activation settles at 2026-01-15T12:03:10Z, the
Payment-Receipt payload decodes to:¶
{
"method": "solana",
"intent": "subscription",
"status": "success",
"reference": "5J8...base58 transaction signature...Kt",
"subscriptionId": "c3ViX3NvbGFuYV8wMQ",
"subscriptionDelegation": "BXQGmO5VwTrl5RfFr6Y8XQZ4nPj9QqMOiKkRn3pZ4ZE",
"externalId": "merchant-subscription-270",
"periodIndex": 0,
"periodStart": "2026-01-15T12:03:10Z",
"periodEnd": "2026-02-14T12:03:10Z",
"expiresAt": "2026-07-14T12:00:00Z",
"timestamp": "2026-01-15T12:03:10Z"
}
¶
The resulting billing periods are:¶
Period 0: [2026-01-15T12:03:10Z, 2026-02-14T12:03:10Z)¶
Period 1: [2026-02-14T12:03:10Z, 2026-03-16T12:03:10Z)¶
Period 2: [2026-03-16T12:03:10Z, 2026-04-15T12:03:10Z)¶
Requests during Period 0 do not require another renewal charge. When
Period 1 begins, the server's renewal worker submits one
transfer_subscription instruction to the subscriptions program. On
success, the on-chain delegation advances to
current_period_start_ts = 2026-02-14T12:03:10Z and
amount_pulled_in_period = 10000000. After that renewal succeeds,
additional requests during Period 1 do not permit another charge for
Period 1.¶
Suppose Period 2 has been charged and the subscriber cancels by
submitting cancel_subscription on-chain at 2026-03-20T09:00:00Z.
The program sets delegation.expires_at_ts = 2026-04-15T12:03:10Z
(end of Period 2).¶
Cancellation takes effect at the end of the current paid billing
period. The server continues honoring access through that time. The
server's renewal worker MUST NOT submit a Period 3 charge: the
on-chain transfer_subscription would fail with
SubscriptionCancelled.¶
Requests at or after 2026-04-15T12:03:10Z receive
402 Payment Required with a fresh challenge.¶
If the subscriber's token balance is insufficient when the server
attempts a Period 3 renewal, the on-chain transaction fails. The
server records the failure but does not retry continuously; it
returns 402 Payment Required on subsequent gated requests.¶
If a later retry within Period 3 succeeds, the server may grant access for Period 3 and update its local state.¶
If Period 4 begins before any successful Period 3 charge, the next
successful transfer_subscription collapses to Period 4 (the program
advances current_period_start_ts to Period 4's start and resets
amount_pulled_in_period = 0). The skipped Period 3 charge does not
become additional on-chain spending authority.¶
When subscriptionExpires = 2026-07-14T12:00:00Z is reached, the
server stops submitting renewal transactions. A subsequent
transfer_subscription would still succeed on-chain (the program
itself does not enforce subscriptionExpires; it is a server-side
contract derived from the credential), so the server MUST honor the
expiry by ceasing renewal submissions and serving fresh challenges.¶
Requests after that time receive 402 Payment Required with a fresh
challenge.¶
If the server sponsored activation, it can reclaim rent only when
program revocation rules allow revoke_delegation, such as after
on-chain cancellation has taken effect or the referenced plan has
ended or been closed.¶
Clients MUST clearly communicate that a Solana subscription
authorizes future recurring on-chain transfers without requiring a
new user action for each billing period. Wallets and client UIs
SHOULD display the per-period amount, the period length, and the
subscriptionExpires value (when present) at activation time.¶
Clients MUST parse and verify the request payload before signing:¶
Verify amount is reasonable for the service¶
Verify currency is the expected mint¶
Verify periodUnit and periodCount match expectations¶
Verify recipient is controlled by the expected party¶
Verify subscriptionExpires is acceptable when present¶
Verify the on-chain Plan referenced by
methodDetails.planAddress carries matching mint, per-period
amount, and per-billing-period interval; authorizes the server's
puller as the plan owner or in plan.pullers; and has exactly one
non-zero destination equal to recipient¶
Verify the activation transfer_subscription instruction uses a
receiver ATA whose token-account owner equals recipient¶
Clients MUST NOT sign an activation transaction whose on-chain Plan
does not match the challenge. Clients MUST NOT rely on the
description field for payment verification.¶
On-chain per-period accounting prevents overspending within a billing period, but it does not by itself make HTTP service delivery idempotent. Servers MUST implement durable local state to prevent duplicate activation and renewal charges caused by retries, parallel requests, or races between charging and service delivery.¶
Servers SHOULD use a per-subscription lease (e.g., a database row lock with a short TTL) when submitting a renewal, releasing the lease only after the on-chain state has advanced.¶
Servers operating Solana subscriptions are responsible for:¶
Responses to subscription challenges (402 Payment Required) MUST
include Cache-Control: no-store to prevent sensitive payment data
from being cached by intermediaries.¶
Responses containing Payment-Receipt headers MUST include
Cache-Control: private to prevent shared caches from storing
payment receipts.¶
The subscription authentication proof is intentionally reusable. A party that obtains it can impersonate the subscriber within the server's protection space until the subscription or an additional application-layer authorization expires or is revoked. Domain separation and the activation challenge bind the proof to one subscription and server realm, but they do not make a copied proof non-transferable.¶
Servers MUST re-check subscription usability for every presentation; signature verification alone is insufficient. Servers SHOULD support early application-layer proof revocation when immediate logout or credential rotation is required.¶
Solana subscription delegations MUST be bound to a receiver
identified by recipient. Clients and servers MUST reject credentials
whose activation transaction uses a receiver ATA owned by any other
address. Servers MUST use a receiver ATA owned by the same recipient
for every renewal.¶
plan.destinations is a whitelist, not a payout split. The underlying
program permits an empty whitelist or several destinations, but this
profile does not. The non-zero destination set MUST contain only
recipient, making the recurring recipient restriction immutable and
program-enforced for the lifetime of the plan.¶
Implementations MUST validate Token-2022 mints and token accounts at activation and before each token-touching instruction according to the deployed validation policy of the subscriptions program.¶
The canonical subscriptions program rejects Token-2022 mints with a
configured TransferHook extension. An inert TransferHook extension
whose authority and program id are both unset is allowed, because it
cannot be activated later.¶
Implementations MUST NOT resolve transfer-hook extra accounts for configured hooks or otherwise bypass the mint and token account validation performed by the program.¶
Before deserializing or mutating any account, implementations MUST validate the expected program owner for:¶
the Plan, SubscriptionDelegation, and SubscriptionAuthority
PDAs (owned by the subscriptions program);¶
the subscriber and destination token accounts (owned by
methodDetails.tokenProgram);¶
the mint (owned by methodDetails.tokenProgram).¶
Implementations MUST NOT allow user-controlled program accounts to influence the activation, transfer, or revocation paths.¶
The subscription payment intent is registered by
[I-D.payment-intent-subscription]. This document does not register
it again.¶
The authors thank the MPP community and the Tempo team for their feedback on this specification, and the Solana Foundation subscriptions program team for the on-chain primitives this profile builds on.¶