SDK & Integration
The TypeScript SDK handles voucher signing, verification, and on-chain settlement. Two entry points: @spx/sdk/agent for paying, @spx/sdk/server for getting paid.
Install
npm install @spx/sdk
Agent Side: Pay for Services
import { SpxClient } from "@spx/sdk/agent";
import { PublicKey } from "@solana/web3.js";
const client = new SpxClient({
agentSecretKey: myKeypair.secretKey,
escrowKey: new PublicKey("MyEscrowPDA"),
escrowCreatedAt: 1700000000n,
});
// Sign a voucher — off-chain, instant, free
const { header } = client.pay(
new PublicKey("VendorPubkey"),
1000n // amount in token base units
);
// Attach to any HTTP request
const res = await fetch("https://api.vendor.com/data", {
headers: { "X-SPX-Voucher": header },
});
The client tracks nonces and cumulative amounts per vendor automatically. Each .pay() call increments both.
How it works under the hood
- Client increments cumulative total for this vendor
- Client increments nonce
- Client builds the 110-byte voucher message
- Client signs with Ed25519 (
nacl.sign.detached) - Client base64-encodes
message + signatureinto the header
Vendor Side: Accept Payments
Add middleware to your API. Every request without a valid voucher gets 402 Payment Required.
import express from "express";
import { spxMiddleware } from "@spx/sdk/server";
const app = express();
app.use("/api", spxMiddleware({
agentPublicKeys: {
"EscrowPubkeyBase58": agentPublicKeyBytes,
},
onVoucher: (voucher, amount) => {
console.log(`Earned ${amount} tokens`);
},
}));
app.get("/api/data", (req, res) => {
res.json({ result: "your paid content here" });
});
Verification is a local Ed25519 signature check. No RPC calls. No blockchain latency. Microseconds.
Settlement: Collect Payment
When you're ready to collect, submit your latest voucher on-chain.
import { Ed25519Program, Transaction, SYSVAR_INSTRUCTIONS_PUBKEY } from "@solana/web3.js";
import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } from "@solana/spl-token";
// Build the Ed25519 verify instruction (must be first)
const ed25519Ix = Ed25519Program.createInstructionWithPublicKey({
publicKey: agentPublicKey,
message: voucherMessage,
signature: voucherSignature,
});
// Build the settle instruction
const settleIx = await program.methods
.settle(
new BN(voucher.amount.toString()),
new BN(voucher.cumulative.toString()),
new BN(voucher.nonce.toString()),
Array.from(voucherSignature)
)
.accounts({
service: serviceWallet,
escrow: escrowPda,
settlement: settlementPda,
vault: escrow.vault,
serviceTokenAccount: getAssociatedTokenAddressSync(escrow.mint, serviceWallet),
config: configPda,
treasuryTokenAccount: getAssociatedTokenAddressSync(escrow.mint, config.treasury),
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
ixSysvar: SYSVAR_INSTRUCTIONS_PUBKEY,
})
.instruction();
// Ed25519 verify MUST be instruction index 0
const tx = new Transaction();
tx.add(ed25519Ix);
tx.add(settleIx);
Settlement strategies
Batch (recommended for high-volume). Accumulate vouchers and settle periodically — hourly, daily, or at a threshold. Minimizes Solana transaction fees.
Immediate. Settle after every voucher or after reaching a dollar threshold. Best when you need instant liquidity.
You always submit only the latest voucher per escrow. Earlier vouchers are automatically superseded.
Building Vouchers Manually
If you're not using the SDK, here's how to build the 110-byte message:
import nacl from "tweetnacl";
import { PublicKey } from "@solana/web3.js";
const VOUCHER_PREFIX = Buffer.from("SPX_VOUCHER_V1");
function buildVoucherMessage(
escrowKey: PublicKey,
escrowCreatedAt: bigint,
serviceKey: PublicKey,
amount: bigint,
cumulative: bigint,
nonce: bigint
): Buffer {
const msg = Buffer.alloc(110);
let offset = 0;
VOUCHER_PREFIX.copy(msg, offset); offset += 14;
escrowKey.toBuffer().copy(msg, offset); offset += 32;
msg.writeBigInt64BE(escrowCreatedAt, offset); offset += 8;
serviceKey.toBuffer().copy(msg, offset); offset += 32;
msg.writeBigUInt64BE(amount, offset); offset += 8;
msg.writeBigUInt64BE(cumulative, offset); offset += 8;
msg.writeBigUInt64BE(nonce, offset);
return msg;
}
const message = buildVoucherMessage(escrowKey, createdAt, serviceKey, amount, cumulative, nonce);
const signature = nacl.sign.detached(message, agentSecretKey);
PDA Derivation
import { PublicKey } from "@solana/web3.js";
const PROGRAM_ID = new PublicKey("GX6RbhyuZnWC3CRDzrVreZR76dbstsS2VcpttdAWVMpQ");
// Escrow PDA
const [escrowPda] = PublicKey.findProgramAddressSync(
[Buffer.from("escrow"), ownerPubkey.toBuffer(), Buffer.from(label)],
PROGRAM_ID
);
// Settlement record PDA
const [settlementPda] = PublicKey.findProgramAddressSync(
[Buffer.from("settlement"), escrowPda.toBuffer(), servicePubkey.toBuffer()],
PROGRAM_ID
);
// Config PDA
const [configPda] = PublicKey.findProgramAddressSync(
[Buffer.from("config")],
PROGRAM_ID
);