x402 paywall

HTTP 402 → on-chain escrow → release.

@aap/x402-adapter is an Express middleware that turns any route into a pay-per-call endpoint. Without payment it returns HTTP 402 with an x402-spec-compatible challenge JSON; with payment it validates the on-chain escrow, runs your handler, then synchronously settles before flushing the response.

Wire it into Express

import express from "express";
import { aapPaywall } from "@aap/x402-adapter";
import { AapClient } from "@aap/sdk-ts";

const client = new AapClient({ provider });
const app = express();

app.get(
  "/api/scrape",
  aapPaywall({
    client,
    merchantPubkey: merchant.publicKey,
    attestorKeypair: attestor,            // never expose this
    mint,                                  // USDC (or mock)
    priceStroops: 1_000_000n,             // 1 USDC per call
    resource: "/api/scrape",
    description: "Pay-per-call scraper",
    onPayment: ({ escrowPda, agentOwner, txSig }) => {
      console.log("settled", { escrowPda, agentOwner, txSig });
    },
  }),
  async (req, res) => {
    const html = await scrapeUrl(req.query.url);
    res.json({ html });   // adapter intercepts and settles
  },
);

Request lifecycle

┌──────────────────────────────────────────────────────────────┐
│ 1. agent → GET /api/scrape (no X-Payment)                    │
│    ← HTTP 402 + challenge JSON                               │
│       expectedTaskHash, mint, payTo, amount                  │
├──────────────────────────────────────────────────────────────┤
│ 2. agent → on-chain createEscrow(amount, expectedTaskHash)   │
│    ← escrow PDA created, USDC custodied                      │
├──────────────────────────────────────────────────────────────┤
│ 3. agent → GET /api/scrape with X-Payment: <base64 envelope> │
│    middleware: validate escrow on-chain                      │
│      ✓ state == Pending                                      │
│      ✓ merchant == config.merchantPubkey                     │
│      ✓ agent_owner == envelope.agent_owner                   │
│      ✓ amount >= priceStroops                                │
│      ✓ deterministic task_hash matches request               │
│    handler runs → res.json({ html, ... })                    │
│    middleware intercepts:                                    │
│      proof_hash = SHA-256(body)                              │
│      attestor.signEd25519(escrow ‖ proof ‖ ts)               │
│      tx = [Ed25519Ix, releaseEscrowIx]                       │
│      await sendAndConfirm(tx)            ← SYNC settle       │
│    ← HTTP 200                                                │
│       x-payment-settled: true                                │
│       x-aap-tx-sig: <base58>                                 │
│       x-aap-proof-hash: <hex>                                │
└──────────────────────────────────────────────────────────────┘

Demo endpoints

The demo merchant exposes several paywalled resources so the protocol feels less abstract than a single scrape endpoint:

GET  /api/scrape?url=<url>        mock web scrape response
GET  /api/report                   agent commerce risk report
GET  /api/inventory-check?sku=<id> inventory availability snapshot
POST /api/book-hotel-mock          mock hotel booking receipt

Hosted demo: demo.settleproof.xyz. Headless API console: api.settleproof.xyz.

402 challenge format

{
  "x402Version": 1,
  "accepts": [
    {
      "scheme": "aap-solana",
      "network": "solana-devnet",
      "maxAmountRequired": "1000000",
      "asset": "<usdc mint base58>",
      "payTo": "<merchant pubkey base58>",
      "resource": "/api/scrape",
      "description": "Pay-per-call scraper",
      "expectedTaskHash": "<hex 32B SHA-256 of canonical request>",
      "extra": {
        "custodyProgramId": "DbpCDp…nFp",
        "attestorProgramId": "DcbEtM…Xaa",
        "attestorPubkey": "<base58>"
      }
    }
  ]
}

X-Payment header (base64 JSON envelope)

{
  "version": 1,
  "scheme": "aap-solana",
  "payload": {
    "escrow_pda": "<base58>",
    "agent_owner": "<base58>",
    "task_hash": "<hex>"
  }
}

The agent computes task_hash deterministically from resource ‖ body ‖ agent_owner using SHA-256 — the server re-derives the same hash and rejects mismatches.

Response headers (200 with payment)

HTTP/1.1 200 OK
content-type: application/json
x-payment-settled: true
x-aap-tx-sig: 4DbqF4hG1sXmUKpPnrX3frPSMkB7ZpCdJ7VhvVQ78gPEbe7…
x-aap-proof-hash: a8a40b252bb74302…

Failure mode

If the on-chain settle fails after the handler ran (network blip, stale attestation, etc.) the response still flushes but with x-payment-settled: false and x-aap-error headers. The escrow stays in Pending and the agent can claim a refund after TTL.

Reference: spec compatibility

The challenge envelope follows x402.org conventions with a custom aap-solana scheme. Standard x402Version, accepts, maxAmountRequired, asset, payTo fields are preserved so existing x402 client libraries can reach the challenge surface without modification.