# 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](/docs/building-with-pay/subscriptions/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](https://datatracker.ietf.org/doc/draft-solana-subscription/) — implemented against an [audited on-chain program](https://github.com/solana-program/subscriptions).

## 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 `subscriptionId` returned in `Payment-Receipt` is the base58 of the `SubscriptionDelegation` PDA.

## 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.

<PaymentFlow
  caption="Activation: one HTTP round-trip, one signed transaction, three on-chain instructions."
  steps={[
    { from: 'client', to: 'api', label: 'GET /api/pro/feed' },
    {
      from: 'api',
      to: 'client',
      label: '402 Payment Required',
      note: 'intent=subscription, externalId=<plan PDA>',
    },
    {
      from: 'client',
      to: 'api',
      label: 'Authorization: Payment',
      note: 'signed activation transaction',
    },
    {
      from: 'api',
      to: 'solana',
      label: 'co-sign + broadcast',
      note: 'subscribe + first transfer',
    },
    {
      from: 'solana',
      to: 'api',
      label: 'confirmed',
      note: 'SubscriptionDelegation created',
    },
    {
      from: 'api',
      to: 'client',
      label: '200 OK + Payment-Receipt',
      note: 'subscriptionId returned',
    },
  ]}
/>

### The 402 challenge

The challenge `WWW-Authenticate` header carries a base64url-encoded JSON `request` object. Decoded, it looks like:

```json
{
  "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:

```json
{
  "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.

<PaymentFlow
  caption="Renewal: server-side pull. The client lifeline is intentionally idle."
  steps={[
    { from: 'api', to: 'solana', label: 'transfer_subscription', note: 'one instruction, signed by the puller' },
    { from: 'solana', to: 'api', label: 'confirmed', note: 'current_period_start_ts advanced' },
  ]}
/>

The on-chain program enforces the rules:

- At most one period's worth of value can be pulled per billing period.
- `current_period_start_ts` advances 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_subscription` collapses 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_subscription` starts failing with `SubscriptionCancelled`.
- 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](https://datatracker.ietf.org/doc/draft-solana-subscription/). Companion documents:

- [The Payment HTTP Authentication Scheme](https://datatracker.ietf.org/doc/draft-ietf-httpauth-payment/)
- [Subscription Intent for HTTP Payment Authentication](https://datatracker.ietf.org/doc/draft-payment-intent-subscription/)
- [Subscriptions Solana Program](https://github.com/solana-program/subscriptions)
