Authorization format

An SPX payment authorization is a 110-byte Ed25519-signed message. This is the entire payment primitive — the thing that makes thousands of payments collapse into one transaction.

Message Layout

Offset   Size   Field              Type        Description
───────────────────────────────────────────────────────────
[0..14)    14   prefix             ASCII       "SPX_VOUCHER_V1"
[14..46)   32   escrow_key         Pubkey      Escrow account address
[46..54)    8   escrow_created_at  i64 BE      Escrow creation timestamp
[54..86)   32   service_key        Pubkey      Intended recipient
[86..94)    8   amount             u64 BE      Per-call payment amount
[94..102)   8   cumulative         u64 BE      Running total owed to service
[102..110)  8   nonce              u64 BE      Monotonically increasing

Total message: 110 bytes. Signature: 64 bytes (Ed25519 detached).

Field Semantics

prefix

Fixed string SPX_VOUCHER_V1. Prevents cross-protocol signature confusion. Future protocol versions use a different prefix, making old signatures invalid.

escrow_key

The on-chain address of the escrow account this voucher draws from. Binds the voucher to a specific pool of funds.

escrow_created_at

The created_at timestamp of the escrow account. This is cross-session replay protection: if an escrow is closed and re-created at the same PDA, old vouchers are invalid because this timestamp won't match.

service_key

The vendor's public key. The escrow will only pay this address. Prevents a vendor from submitting another vendor's voucher.

amount

The per-call payment amount in token base units. Used by the vendor for off-chain accounting (tracking revenue per call). Not used in on-chain settlement — only cumulative matters on-chain.

cumulative

The total amount authorized for this vendor across all vouchers from this escrow. This is the settlement basis. On-chain, the delta is: cumulative - previously_settled_amount.

Each voucher supersedes all previous vouchers. Only the highest cumulative value can extract funds.

nonce

Strictly increasing counter per (escrow, service) pair. The on-chain program rejects vouchers with a nonce ≤ the last settled nonce. Prevents replay.

Signing

Vouchers are signed with Ed25519 using the agent's keypair. The signature is detached (not wrapping the message).

import nacl from 'tweetnacl';

const message = buildVoucherMessage(
  escrowKey, escrowCreatedAt, serviceKey,
  amount, cumulativeAmount, nonce
);

const signature = nacl.sign.detached(message, agentSecretKey);
// signature: 64 bytes

The agent key is a raw Ed25519 keypair — not a Solana wallet. It's a hot key that can sign vouchers but has no on-chain authority to withdraw or modify the escrow.

Verification

Vendors verify vouchers locally. No RPC call. No blockchain.

import nacl from 'tweetnacl';

function verify(message: Uint8Array, signature: Uint8Array, agentPubkey: Uint8Array): boolean {
  if (message.length !== 110) return false;
  if (signature.length !== 64) return false;

  // Check prefix
  const prefix = Buffer.from(message.subarray(0, 14)).toString();
  if (prefix !== 'SPX_VOUCHER_V1') return false;

  // Check recipient is us
  const serviceKey = message.subarray(54, 86);
  if (!Buffer.from(serviceKey).equals(ourPublicKey)) return false;

  // Verify Ed25519 signature
  return nacl.sign.detached.verify(message, signature, agentPubkey);
}

Vendor validation checklist

Before accepting a voucher:

  1. Prefix is SPX_VOUCHER_V1
  2. Signature is valid for the known agent public key
  3. Service key matches your address
  4. Cumulative ≥ your last received cumulative (monotonically increasing)
  5. Nonce > your last received nonce

Store only the latest voucher per (escrow, service) pair. All prior vouchers are superseded.

On-Chain Settlement

When a vendor submits a voucher for settlement, the on-chain program:

  1. Reads the Ed25519 precompile instruction from the transaction's sysvar to verify the signature was checked natively
  2. Confirms the signer matches the escrow's agent key
  3. Confirms escrow_created_at matches the escrow account
  4. Confirms nonce > settlement_record.last_nonce
  5. Computes delta = cumulative - settlement_record.settled_amount
  6. Verifies escrow.settled + delta ≤ escrow.deposited - escrow.total_withdrawn
  7. Transfers delta - fee to the vendor, fee to the protocol treasury
  8. Updates the settlement record with new cumulative and nonce

Transport

The protocol doesn't prescribe a transport layer. Common patterns:

  • HTTP header: X-SPX-Voucher: <base64(message + signature)> — simplest, works with any HTTP API
  • HTTP 402 flow: Vendor returns 402 Payment Required with pricing info, agent retries with voucher header
  • WebSocket: Stream vouchers in real-time for low-latency services
  • Message queue: NATS, Redis Streams, etc. for high-throughput pipelines

Next: Security model →