Working Examples
x402 Payment Flow
End-to-end example of the x402 payment lifecycle from escrow creation to settlement verification.
x402 Payment Flow
This example demonstrates the complete x402 payment lifecycle: cost estimation, escrow creation, making paid API calls, settlement, and verification.
Client Side
import { SapClient } from "@oobe-protocol-labs/synapse-sap-sdk";
import { AnchorProvider } from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";
const client = SapClient.from(AnchorProvider.env());
const agentWallet = new PublicKey("AgentWalletAddress..."); // the agent you want to pay
// 1. Estimate cost for 100 calls.
// Reads the agent's on-chain pricing and applies volume curve discounts.
const estimate = await client.x402.estimateCost(agentWallet, 100);
console.log("Estimated total:", estimate.totalCost.toString(), "lamports");
console.log("Per call:", estimate.effectivePricePerCall.toString());
if (estimate.hasVolumeCurve) {
console.log("Volume tiers:");
for (const tier of estimate.tiers) {
console.log(` ${tier.calls} calls at ${tier.pricePerCall} = ${tier.subtotal}`);
}
}
// 2. Create escrow and deposit funds.
// preparePayment sends one Solana TX that creates the escrow PDA
// and deposits the specified lamports.
const paymentCtx = await client.x402.preparePayment(agentWallet, {
pricePerCall: 10_000, // 10,000 lamports per call (~$0.0015)
maxCalls: 100, // max 100 calls (0 = unlimited)
deposit: 1_000_000, // deposit 1,000,000 lamports upfront
expiresAt: Math.floor(Date.now() / 1000) + 3600, // expires in 1 hour (0 = never)
volumeCurve: [ // optional: tiered pricing
{ afterCalls: 50, pricePerCall: 8_000 }, // 20% discount after 50 calls
],
});
console.log("Escrow:", paymentCtx.escrowPda.toBase58());
// 3. Build x402 HTTP headers and call the agent.
// Include these headers with every HTTP request to the agent's endpoint.
const headers = client.x402.buildPaymentHeaders(paymentCtx);
for (let i = 0; i < 5; i++) {
const response = await fetch("https://agent.example.com/api/v1/query", {
method: "POST",
headers: {
"Content-Type": "application/json",
...headers, // merges X-Payment-Protocol, X-Payment-Escrow, etc.
},
body: JSON.stringify({ prompt: `Query ${i + 1}` }),
});
const data = await response.json();
console.log(`Call ${i + 1}:`, data);
}
// 4. Check remaining balance.
const balance = await client.x402.getBalance(agentWallet);
console.log("Remaining:", balance?.callsRemaining, "calls");
console.log("Balance:", balance?.balance.toString(), "lamports");
console.log("Expired:", balance?.isExpired);
console.log("Affordable:", balance?.affordableCalls, "more calls");Agent Side
// 5. Agent settles after serving requests.
// settle() must be called by the agent's wallet (the escrow's agentWallet).
const receipt = await client.x402.settle(
depositorWallet, // PublicKey: the client who funded the escrow
5, // number of calls to settle (claim payment for)
"batch-service-data", // service data string (auto-hashed to SHA-256)
);
console.log("Settled:", receipt.callsSettled, "calls");
console.log("Amount:", receipt.amount.toString(), "lamports transferred to agent");
console.log("TX:", receipt.txSignature);
console.log("Service hash:", receipt.serviceHash); // number[] (32 bytes)Cleanup
// 6. Client withdraws remaining funds from the escrow.
const remaining = await client.x402.getBalance(agentWallet);
if (remaining && remaining.balance.gtn(0)) {
await client.x402.withdrawFunds(
agentWallet, // the agent's wallet (identifies the escrow)
remaining.balance, // BN: amount to withdraw
);
console.log("Withdrawn:", remaining.balance.toString(), "lamports");
}
// 7. Close the escrow PDA (balance must be 0). Reclaims ~0.004 SOL rent.
await client.x402.closeEscrow(agentWallet);
console.log("Escrow closed.");