Concepts
Escrow System
Trustless USDC-SPL escrow on Solana for guaranteed-delivery payments
The escrow system provides trustless payment guarantees via a deployed Anchor program on Solana mainnet. Instead of paying the gateway directly, the agent deposits USDC into a PDA vault. The gateway claims only the actual cost after the request succeeds. If the gateway fails, the agent reclaims their deposit.
Deployed program: 9neDHouXgEgHZDde5SpmqqEZ9Uv35hFcjtFEPxomtHLU
Escrow vs direct payment
| Exact (direct) | Escrow | |
|---|---|---|
| Payment method | USDC-SPL transfer to gateway wallet | Deposit to PDA vault |
| Gateway receives | Full amount upfront | Actual cost after completion |
| Agent protection | None — gateway gets funds before serving | Refundable if gateway fails |
| Complexity | Simple — one transaction | Two transactions (deposit + claim) |
| Use case | Trusted gateway, simple integrations | Trustless agent-to-gateway flows |
How it works
Agent gets a 402 response
The 402 response includes both exact and escrow payment options in the accepts array. The escrow option includes escrow_program_id.
Agent builds a deposit transaction
The agent builds a signed Solana transaction that deposits USDC into the escrow PDA vault. The PDA is deterministically derived from the agent's pubkey and a service ID.
Agent sends the deposit payload
The agent wraps the deposit transaction in an EscrowPayload and sends it in the payment-signature header.
Gateway verifies the deposit on-chain
The gateway checks that the deposit transaction is confirmed and the deposited amount covers the request cost.
Gateway proxies to the LLM provider
The request is forwarded to the provider and the response is returned to the agent.
Gateway claims the actual cost
After the response is sent, the gateway fires a fire-and-forget claim transaction to collect the actual token cost from the PDA vault. Any excess remains in the vault.
PDA account structure
The escrow vault PDA is derived using:
seeds = [b"escrow", agent_pubkey_bytes, service_id_bytes]
program_id = 9neDHouXgEgHZDde5SpmqqEZ9Uv35hFcjtFEPxomtHLUThe service_id is a 32-byte request correlation ID (base64-encoded) that the agent generates per request. It is used both as a PDA seed and to correlate the deposit with the gateway request.
The PDA holds a USDC-SPL token account (Associated Token Account). The vault is owned by the program — neither the agent nor the gateway can withdraw unilaterally.
Escrow payment payload
When choosing the escrow scheme, the payment-signature header contains a PaymentPayload with an EscrowPayload:
{
"x402_version": 2,
"resource": { "url": "/v1/chat/completions", "method": "POST" },
"accepted": {
"scheme": "escrow",
"network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"amount": "2625",
"asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"pay_to": "<gateway-wallet>",
"max_timeout_seconds": 300,
"escrow_program_id": "9neDHouXgEgHZDde5SpmqqEZ9Uv35hFcjtFEPxomtHLU"
},
"payload": {
"deposit_tx": "<base64-encoded-signed-deposit-tx>",
"service_id": "<base64-encoded-32-byte-service-id>",
"agent_pubkey": "<base58-agent-wallet-pubkey>"
}
}Client-initiated settle (F4)
By default the gateway claims escrow at request end. For streaming responses, clients can drop reconciliation latency from minutes (auto-timeout) to milliseconds by calling POST /v1/escrow/settle once the stream completes — the gateway computes the actual cost from the reported token counts and fires the claim immediately.
This endpoint is what @solvela/openclaw-provider's wrapStreamForF4 wrapper calls after each wrapStreamFn invocation finishes.
{
"service_id": "<base64 32-byte service id, same value used in the deposit>",
"agent_pubkey": "<base58 Solana pubkey of the depositor>",
"model": "openai/gpt-4o",
"status": "completed",
"actual_prompt_tokens": 128,
"actual_completion_tokens": 256
}Behavior:
status: "completed"with bothactual_*_tokensfields → gateway looks up the model in the registry, computes the actual atomic-USDC cost, and enqueues a claim. The escrow program rejects over-claims on-chain, so the cost is bounded by the deposit regardless of what the client reports.status: "error"→ gateway logs the error and fires no claim. The deposit is reconciled by the escrow program's on-chain auto-timeout refund path (same as the pre-F4 flow). Noactual_*_tokensfields are needed.- Missing tokens with
status: "completed", unknownmodel, or non-finite pricing →400.
Idempotency: when PostgreSQL is configured, the durable claim queue dedupes by (service_id, agent). Without a database, a duplicate settle call can produce a second on-chain claim attempt; the escrow program rejects the second because the deposit account is already drained.
Auth (v1): the endpoint is fire-and-forget. The on-chain escrow program is the source of truth — a misrouted settle (wrong agent_pubkey or service_id) fails at the chain layer, logged but not surfaced to the client. Future hardening (HMAC over the body, or an agent signature over (service_id, model, status, tokens)) is tracked but not yet shipped.
Note
As of 2026-05-14, the F4 wrapper in @solvela/openclaw-provider is unit-tested but not yet invoked from the plugin's hot path because @solvela/signer-core does not yet expose service_id from the encoded payment-header transaction. Until that lands, escrow accounting still relies on the on-chain auto-timeout refund. See STATUS.md Known follow-ups.
Refund behavior
If the gateway fails to claim within max_timeout_seconds, the agent can invoke the refund instruction on the escrow program to reclaim their deposit. The refund path is enforced on-chain by the Anchor program — no trust in the gateway is required.
Warning
The escrow program is deployed to mainnet with upgrade authority retained by the deployer. Treat it as trusted-but-upgradeable software.
When to use escrow
Use escrow when:
- You are building autonomous agents that require trustless guarantees
- You want protection against gateway downtime after payment
- You are handling large request batches and want per-request accountability
Use exact (direct) payment when:
- You want the simplest possible integration
- You are running in a trusted environment
- Your SDK handles payment automatically and you don't need custom logic
See x402 Protocol for the full payment flow.