SAP Explorer Docs
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.");