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:
- Prefix is
SPX_VOUCHER_V1 - Signature is valid for the known agent public key
- Service key matches your address
- Cumulative ≥ your last received cumulative (monotonically increasing)
- 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:
- Reads the Ed25519 precompile instruction from the transaction's sysvar to verify the signature was checked natively
- Confirms the signer matches the escrow's agent key
- Confirms
escrow_created_atmatches the escrow account - Confirms
nonce > settlement_record.last_nonce - Computes
delta = cumulative - settlement_record.settled_amount - Verifies
escrow.settled + delta ≤ escrow.deposited - escrow.total_withdrawn - Transfers
delta - feeto the vendor,feeto the protocol treasury - 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 Requiredwith 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 →