spec

MPP.sol Wire Format

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.


1. Conventions

2. Settlement schemes

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.

3. Challenge — WWW-Authenticate: Payment

Server-side response when a resource requires payment. The server MUST return HTTP 402 Payment Required with this header.

3.1 Common parameters (both schemes)

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.

3.2 Example

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"

4. Authorization — Authorization: Payment

Client-side header carrying proof of payment. Sent on the retry request after the 402 challenge.

4.1 solana-direct

The 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:

  1. Contains a token transfer (or system_instruction::transfer if mint=native) from any account to solana-recipient with amount ≥ solana-amount of solana-mint.
  2. Contains the challenge nonce on-chain via one of:
    • Memo program (MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr) instruction with data exactly b64url(solana-nonce). Simplest. Adds ~100 CU.
    • MPP CPI: mppsol/cpi’s Pay instruction with the nonce in the instruction data. Preferred when the payer is a Solana program.
  3. Confirmed at the level requested by solana-min-confirmations before the deadline.
  4. Block time ≤ solana-deadline.

The nonce-binding requirement prevents a single payment from satisfying multiple unrelated requests.

4.2 solana-session

The 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).

4.3 Examples

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"

5. Receipt — Payment-Receipt

Server-side header on the successful (200/2xx) response, proving the payment satisfying the request.

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

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

5.3 Receipt verification by third parties

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.

6. Server-side verification rules

For solana-direct, the server MUST, in order:

  1. Reject if scheme is not advertised in its challenge.
  2. Reject if nonce is not a known recently-issued nonce, or has already been used.
  3. Fetch the transaction by signature from a trusted RPC.
  4. Reject if the cluster does not match.
  5. Reject if the transaction is not confirmed at solana-min-confirmations.
  6. Reject if block time > solana-deadline.
  7. Verify the transaction contains the required transfer to solana-recipient, of solana-mint, of amount ≥ solana-amount.
  8. Verify the nonce-binding instruction (Memo or MPP CPI) is present and matches.
  9. Mark the nonce as used, record the receipt, return the resource.

For solana-session, the server MUST, in order:

  1. Reject if scheme is not advertised.
  2. Verify the Ed25519 signature on the canonical debit message.
  3. Reject if debit.session does not match the session parameter.
  4. Reject if debit.nonce does not match a recently-issued challenge nonce.
  5. Fetch session account; reject if revoked, expired, or wrong cluster.
  6. Reject if debit.sequence ≤ session.last_seen_sequence (replay).
  7. Reject if debit.amount > session.remaining_cap.
  8. Reject if debit.expiry < now or debit.expiry > solana-deadline.
  9. Update local session accounting; record receipt; return the resource.

Servers MUST persist nonce/sequence state durably enough to survive crashes within the deadline window, or risk accepting replays.

7. Replay protection

8. Cluster identification

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:

9. Errors

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.

10. Full example exchange

Client request

GET /v1/joke HTTP/1.1
Host: api.example.com
Accept: text/plain

Server challenge

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 pays

Client builds a transaction:

Submits, waits for confirmed, gets signature 3vQY...UA.

Client retry

GET /v1/joke HTTP/1.1
Host: api.example.com
Authorization: Payment scheme="solana-direct",
  signature="3vQYZHkZ6f...UA",
  nonce="dGhpc2lzYXJhbmRvbW5vbmNlMzJieXRlcw"

Server response

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.

11. Open questions

12. References