Solana Reference Implementation
The SPX protocol is chain-agnostic. This is one implementation — an Anchor program on Solana that proves the protocol works. It's deployed on devnet with 77 passing tests. Anyone could write their own implementation on any chain that supports Ed25519.
Program ID (devnet): GX6RbhyuZnWC3CRDzrVreZR76dbstsS2VcpttdAWVMpQ
Account Types
EscrowAccount (206 bytes)
PDA seed: ["escrow", owner_pubkey, label]
pub struct EscrowAccount {
pub owner: Pubkey, // Can withdraw, freeze, close
pub agent: Pubkey, // Signs vouchers off-chain
pub deposited: u64, // Total ever deposited (append-only)
pub settled: u64, // Total settled to services (append-only)
pub total_withdrawn: u64, // Total withdrawn by owner (append-only)
pub created_at: i64, // Cross-session replay protection
pub expires_at: i64, // 0 = no expiry
pub state: EscrowState, // Active | Frozen | Closed
pub vault: Pubkey, // ATA holding escrowed tokens
pub label: String, // Max 16 bytes, for multi-escrow
pub mint: Pubkey, // SPL token mint (e.g. USDC)
pub rent_subsidy_per_service: u64,
}
Available balance: deposited - settled - total_withdrawn
An owner can create multiple escrows with different labels — one per agent, per budget, or per use case.
SettlementRecord (97 bytes)
PDA seed: ["settlement", escrow_pubkey, service_pubkey]
pub struct SettlementRecord {
pub escrow: Pubkey,
pub service: Pubkey,
pub settled_amount: u64, // Cumulative settled to this service
pub last_nonce: u64, // Replay protection
pub escrow_created_at: i64, // Cross-session binding
}
Created automatically on first settlement. The vendor pays rent, but the escrow reimburses the SOL cost via rent_subsidy_per_service.
ConfigAccount (107 bytes)
PDA seed: ["config"]
pub struct ConfigAccount {
pub authority: Pubkey, // Cold key — can transfer operator
pub operator: Pubkey, // Hot key — can update fee/treasury
pub treasury: Pubkey, // Receives protocol fees
pub fee_bps: u16, // Max 1000 (10%)
}
Instructions
Configuration
| Instruction | Signer | Description |
|---|---|---|
initialize_config | Authority | Create the global config (once) |
update_config | Operator | Update fee rate or treasury address |
transfer_operator | Authority | Rotate the operator key |
Escrow Management
| Instruction | Signer | Description |
|---|---|---|
create_escrow | Owner | Create a new escrow with label, mint, agent, expiry |
deposit | Owner | Fund the escrow vault (works when Active or Frozen) |
withdraw | Owner | Withdraw unspent funds |
freeze | Owner | Block all settlements |
unfreeze | Owner | Resume settlements |
update_agent | Owner | Rotate the agent signing key |
extend_expiry | Owner | Push the expiry date forward |
close_escrow | Owner | Close escrow and reclaim rent (must be fully settled) |
Settlement
| Instruction | Signer | Description |
|---|---|---|
settle | Service | Submit a voucher and collect payment |
close_settlement_record | Service | Reclaim settlement record rent |
Settlement Flow
The settle instruction is the core of the program (337 lines). Here's what it does:
1. Read Ed25519 precompile instruction from sysvar
2. Verify signature matches escrow.agent key
3. Verify escrow_created_at matches the escrow
4. Verify nonce > settlement_record.last_nonce
5. Compute delta = voucher.cumulative - settlement_record.settled_amount
6. Compute fee = (delta as u128 * fee_bps as u128) / 10_000
7. Transfer (delta - fee) tokens → service
8. Transfer fee tokens → treasury (if fee > 0)
9. Update escrow.settled += delta
10. Update settlement_record (new cumulative, new nonce)
11. Emit Settled event
The Ed25519 verify instruction must be at transaction index 0. The settle instruction reads it from the sysvar — it doesn't re-verify the signature itself.
Fee Model
- Rate: 50 basis points (0.5%) on the settlement delta
- Basis: Applied to the incremental amount, not the cumulative total
- Overflow protection: u128 intermediate for
delta * fee_bps - Dust handling: If fee rounds to 0, skip the treasury transfer
- Cap: Maximum 1,000 bps (10%), enforced on-chain
| Settlement Delta | Fee (0.5%) | Service Receives |
|---|---|---|
| $500.00 | $2.50 | $497.50 |
| $1.00 | $0.005 | $0.995 |
| $0.0001 | $0 (dust) | $0.0001 |
Events
The program emits events on every state change for off-chain indexing:
ConfigInitialized, ConfigUpdated, EscrowCreated, Deposited, Settled, Withdrawn, AgentUpdated, EscrowFrozen, EscrowClosed
Error Codes
| Code | Name | Meaning |
|---|---|---|
| 6000 | InvalidEd25519Program | Wrong program for signature verification |
| 6001 | InvalidEd25519Instruction | Malformed Ed25519 instruction |
| 6002 | SignatureMismatch | Voucher signature doesn't match agent key |
| 6003 | InvalidNonce | Nonce ≤ last settled nonce |
| 6004 | InsufficientFunds | Escrow balance can't cover the delta |
| 6005 | Overflow | Arithmetic overflow in fee calculation |
| 6006 | EscrowNotActive | Escrow is frozen or closed |
| 6007 | InvalidAmount | Zero or negative amount |
| 6008 | LabelTooLong | Label exceeds 16 bytes |
| 6009 | ExpiryInPast | Expiry timestamp is in the past |
| 6010 | EscrowExpired | Escrow has expired |
| 6011 | FeeTooHigh | Fee exceeds 1,000 bps cap |
| 6012 | SessionMismatch | Voucher's created_at doesn't match escrow |
| 6013 | EscrowNotSettled | Can't close — unsettled balance remains |
| 6014 | Unauthorized | Signer is not the owner/operator/authority |
| 6015 | InvalidMint | Token mint doesn't match escrow |
| 6016 | AlreadyFrozen | Escrow is already frozen |
| 6017 | NotFrozen | Escrow is not frozen (can't unfreeze) |
| 6018 | InvalidTreasury | Treasury account doesn't match config |
| 6019 | ExpiryNotExtended | New expiry is not later than current |
| 6020 | InvalidServiceKey | Voucher service key doesn't match signer |
| 6021 | InvalidEscrowKey | Voucher escrow key doesn't match account |
Deployment
The program is managed through Squads multisig on devnet:
- Upgrade authority → Squads vault (requires multisig approval)
- Config authority → Cold key in Squads
- Operator → Hot key for fee/treasury updates
- Treasury → Separate wallet for fee collection
The deployer keypair is ephemeral — created for initial deployment, then authority is transferred to Squads.
Next: SDK & integration →