Status: Draft v0.1 Last updated: 2026-05-05 Editors: psyto
This document specifies the Solana-specific encoding of the
MPP WWW-Authenticate: Payment,
Authorization: Payment, and Payment-Receipt headers. It is normative for
any MPP server or client that settles on Solana.
It does not redefine the base MPP protocol: the HTTP 402 challenge-response
flow, header envelope grammar, and Payment-Receipt semantics are inherited
from upstream MPP. This document defines only the Solana settlement method
and the parameters that travel inside it.
key="value" per the upstream MPP envelope.base64url),
per RFC 4648 §5.1000 means 0.001 USDC.MPP.sol defines two MPP settlement schemes:
| Scheme | Use case |
|---|---|
solana-direct |
One transaction per request. Simple, fully on-chain, ~400 ms confirm. |
solana-session |
Off-chain debits against a pre-authorized on-chain session. Many requests, one settlement. |
A server MAY advertise either or both. A client MUST pick one per request.
WWW-Authenticate: PaymentServer-side response when a resource requires payment. The server MUST return
HTTP 402 Payment Required with this header.
| Parameter | Required | Description |
|---|---|---|
realm |
yes | Logical resource scope (per upstream MPP). |
methods |
yes | Comma-separated supported schemes. MUST include solana-direct, solana-session, or both. |
solana-cluster |
yes | One of mainnet-beta, devnet, testnet. Cluster confusion is a security risk; clients MUST refuse mismatches. |
solana-recipient |
yes | Base58 public key receiving payment. For SPL tokens, this is the token account, not the wallet. |
solana-mint |
yes | Base58 mint of the SPL token used for payment. For native SOL settlement, use the literal string native. |
solana-amount |
yes | Decimal string, atomic units. |
solana-nonce |
yes | Server-generated, single-use, base64url-encoded 32 random bytes. |
solana-deadline |
yes | Unix seconds. Server MUST reject payments confirmed after this time. |
solana-min-confirmations |
no | One of processed, confirmed, finalized. Default: confirmed. |
HTTP/1.1 402 Payment Required
WWW-Authenticate: Payment realm="api.example.com",
methods="solana-direct, solana-session",
solana-cluster="mainnet-beta",
solana-recipient="9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
solana-mint="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
solana-amount="1000",
solana-nonce="dGhpc2lzYXJhbmRvbW5vbmNlMzJieXRlcw",
solana-deadline="1746489600",
solana-min-confirmations="confirmed"
Authorization: PaymentClient-side header carrying proof of payment. Sent on the retry request after
the 402 challenge.
solana-directThe client constructs and submits a Solana transaction satisfying the challenge, then sends:
| Parameter | Required | Description |
|---|---|---|
scheme |
yes | Literal "solana-direct". |
signature |
yes | Base64url of the transaction signature (64 bytes). |
nonce |
yes | The same solana-nonce from the challenge (echo for binding). |
The transaction MUST satisfy all of:
system_instruction::transfer if mint=native)
from any account to solana-recipient with amount ≥ solana-amount of solana-mint.MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr)
instruction with data exactly b64url(solana-nonce). Simplest. Adds ~100 CU.mppsol/cpi’s Pay instruction with the nonce in the
instruction data. Preferred when the payer is a Solana program.solana-min-confirmations before the deadline.solana-deadline.The nonce-binding requirement prevents a single payment from satisfying multiple unrelated requests.
solana-sessionThe client has a pre-authorized session PDA on-chain (see spec/session.md).
The session has an authorized signer key, a remaining cap, an expiry, and a
monotonic sequence counter.
For each request, the client signs an off-chain debit message and sends:
| Parameter | Required | Description |
|---|---|---|
scheme |
yes | Literal "solana-session". |
session |
yes | Base58 public key of the session PDA. |
debit |
yes | Base64url of the canonical debit message (see below). |
signature |
yes | Base64url Ed25519 signature over debit by the session’s authorized signer (64 bytes). |
The debit message is a canonical fixed-layout struct:
struct Debit {
session: [u8; 32], // session PDA pubkey
nonce: [u8; 32], // server's solana-nonce from the challenge
amount: u64, // little-endian, atomic units, ≤ challenge amount
expiry: i64, // little-endian Unix seconds, ≤ challenge deadline
sequence: u64, // little-endian, monotonically increasing per session
domain_sep: [u8; 16], // ASCII "MPP.SOL/DEBIT001"
}
Total: 104 bytes. The server MUST verify the signature, fetch the session
account, check amount ≤ session.remaining_cap, expiry ≥ now,
sequence > session.last_seen_sequence, and the cluster match. On success
the server records the new last_seen_sequence and reduces remaining_cap
in its local accounting; the on-chain settlement is batched (§5.2).
solana-direct:
GET /paid-resource HTTP/1.1
Authorization: Payment scheme="solana-direct",
signature="3vQYZHkZ6f...UA",
nonce="dGhpc2lzYXJhbmRvbW5vbmNlMzJieXRlcw"
solana-session:
GET /paid-resource HTTP/1.1
Authorization: Payment scheme="solana-session",
session="5JBp4...XJk",
debit="AAECAwQF...AAA",
signature="2bP9k...QQ"
Payment-ReceiptServer-side header on the successful (200/2xx) response, proving the
payment satisfying the request.
solana-direct| Parameter | Required | Description |
|---|---|---|
scheme |
yes | "solana-direct". |
tx |
yes | Base64url of the transaction signature. |
slot |
yes | Decimal slot at which the tx was confirmed. |
cluster |
yes | Same as challenge. |
recipient |
yes | Echo of solana-recipient. |
mint |
yes | Echo of solana-mint. |
amount |
yes | Atomic units actually credited. |
nonce |
yes | Echo of solana-nonce. |
solana-session| Parameter | Required | Description |
|---|---|---|
scheme |
yes | "solana-session". |
session |
yes | Echo of session pubkey. |
sequence |
yes | The accepted sequence number. |
amount |
yes | Atomic units debited. |
nonce |
yes | Echo of solana-nonce. |
settlement-tx |
no | Base64url of the on-chain batch settlement signature, once known. MAY be omitted on the immediate response and surfaced via a GET /receipts/{nonce} endpoint or polled later. |
Servers SHOULD batch session settlements (e.g. every N seconds or M debits)
and emit one on-chain settlement transaction that closes/reduces the session
PDA. The settlement-tx field, when present, is a globally-verifiable anchor
for the off-chain debit.
Receipts are intentionally self-describing: any third party can take a
Payment-Receipt header, fetch the referenced transaction (or session
state) from any Solana RPC, and independently verify the payment without
trusting the server. This is the basis for receipt portability across
agents, audit logs, and on-chain consumers.
For solana-direct, the server MUST, in order:
scheme is not advertised in its challenge.nonce is not a known recently-issued nonce, or has already been used.signature from a trusted RPC.solana-min-confirmations.solana-deadline.solana-recipient,
of solana-mint, of amount ≥ solana-amount.For solana-session, the server MUST, in order:
scheme is not advertised.debit.session does not match the session parameter.debit.nonce does not match a recently-issued challenge nonce.debit.sequence ≤ session.last_seen_sequence (replay).debit.amount > session.remaining_cap.debit.expiry < now or debit.expiry > solana-deadline.Servers MUST persist nonce/sequence state durably enough to survive crashes within the deadline window, or risk accepting replays.
solana-deadline,
and MUST be bound on-chain via Memo or MPP CPI. The server keeps a “seen”
set covering at least [now − max_deadline, now].last_seen_sequence per session. Out-of-order debits are rejected, not
reordered.A signed Solana transaction or debit message is structurally identical across
clusters. A malicious server could issue a mainnet-beta challenge while
relaying it to a devnet RPC and accepting a worthless devnet payment.
To prevent this:
solana-cluster.Pay instruction MAY include a cluster-genesis-hash field for
on-chain enforcement (see spec/cpi.md).On verification failure the server returns 402 again with an error parameter:
WWW-Authenticate: Payment error="nonce-reused"
Defined error codes:
| Code | Meaning |
|---|---|
invalid-signature |
Signature does not verify. |
nonce-unknown |
Nonce was never issued or expired from cache. |
nonce-reused |
Nonce was previously consumed. |
nonce-not-bound |
On-chain nonce-binding instruction missing. |
amount-insufficient |
Transferred amount < requested. |
mint-mismatch |
Wrong SPL mint. |
recipient-mismatch |
Transfer not addressed to solana-recipient. |
cluster-mismatch |
Cluster declared in authorization differs from challenge. |
tx-not-confirmed |
Transaction not at requested confirmation level. |
deadline-passed |
Block time or now exceeds deadline. |
session-not-found |
Session PDA does not exist on-chain. |
session-revoked |
Session PDA marked revoked. |
session-expired |
Session expiry passed. |
cap-exceeded |
Debit amount exceeds session remaining cap. |
sequence-reused |
Debit sequence ≤ last seen. |
GET /v1/joke HTTP/1.1
Host: api.example.com
Accept: text/plain
HTTP/1.1 402 Payment Required
WWW-Authenticate: Payment realm="api.example.com",
methods="solana-direct",
solana-cluster="mainnet-beta",
solana-recipient="9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
solana-mint="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
solana-amount="1000",
solana-nonce="dGhpc2lzYXJhbmRvbW5vbmNlMzJieXRlcw",
solana-deadline="1746489600"
Client builds a transaction:
spl-token transfer of 1000 atomic USDC to recipient.Memo instruction with data = dGhpc2lzYXJhbmRvbW5vbmNlMzJieXRlcw.Submits, waits for confirmed, gets signature 3vQY...UA.
GET /v1/joke HTTP/1.1
Host: api.example.com
Authorization: Payment scheme="solana-direct",
signature="3vQYZHkZ6f...UA",
nonce="dGhpc2lzYXJhbmRvbW5vbmNlMzJieXRlcw"
HTTP/1.1 200 OK
Content-Type: text/plain
Payment-Receipt: scheme="solana-direct",
tx="3vQYZHkZ6f...UA",
slot="290000000",
cluster="mainnet-beta",
recipient="9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
mint="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
amount="1000",
nonce="dGhpc2lzYXJhbmRvbW5vbmNlMzJieXRlcw"
Why don't scientists trust atoms? Because they make up everything.
mainnet-beta
is meaningless on devnet; should the receipt format include a
cluster-genesis-hash to make this machine-checkable?