internal/embed/skills/buy-x402/SKILL.md
Buy from any x402-gated endpoint. Two flows: `pay` for one-shot HTTP services (single authorization, no sidecar), and `buy` for long-running paid inference (pre-authorized batch via PurchaseRequest, exposed as `paid/<remote-model>`). Supports USDC (EIP-3009) and OBOL (Permit2). Zero signer access at runtime — spending is capped by design and nothing moves on-chain until a voucher is spent.
npx skillsauth add obolnetwork/obol-stack buy-x402Install this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Purchase access to remote x402-gated services. There are two flows, picked by usage shape:
pay <url> — single-shot. Probe the URL, sign one payment authorization, attach X-PAYMENT, send the request, return the response. Stateless. Use for type:http services and any one-off purchase. Max loss = price of one request. Settlement normally lands only after the request succeeds — but a facilitator can submit the settle tx on-chain and then fail the request. When that happens the failure report prints ⚠️ SETTLEMENT MAY HAVE COMPLETED ON-CHAIN with the tx hash: verify with balance --chain <X> before retrying (mechanism: docs/observability.md, "Verify settlement against the chain"). Applies to pay-agent too.pay-agent <url> --model <id> — single-shot paid streaming agent call. Same payment shape as pay (one auth, X-PAYMENT, max-loss = price), but POSTs to <url>/v1/chat/completions with stream: true and forwards every SSE event verbatim to stdout as it arrives. Use this for type:agent ServiceOffers when the calling agent wants to consume the response itself (memory, tool-call traces, partial results) instead of routing it through LiteLLM as a paid alias. Default HTTP read timeout is 1 hour — agent calls can legitimately run for many minutes; override with --timeout <seconds>.buy <name> — pre-authorize a budget. Sign N authorizations up front (the buyer pays nothing yet), declare them in a PurchaseRequest CR, let the x402-buyer sidecar redeem them transparently as the agent calls the model through LiteLLM at paid/<remote-model>. Use for long-running paid inference. Max loss = N × price (only as vouchers are spent); runtime path holds zero signer access.buy <name> --model <id> --set-default — same as buy above, then adopt paid/<remote-model> as the agent's own primary model, in-pod, by itself: an atomic hermes config set model.default that Hermes re-reads per request (effective next chat turn, no restart, no host-side obol model prefer/obol model sync). Refuses if the model isn't selectable in LiteLLM. Pair with --auto-refill so the primary model doesn't brick when the pre-authorized budget runs out.Both flows auto-detect the token + transfer method from the seller's 402 response. Currently supported: USDC via EIP-3009 (Base Sepolia, Base Mainnet, Ethereum Mainnet) and OBOL via Permit2 (Ethereum Mainnet).
Auth expiry (OBOL_X402_AUTH_TTL / --auth-ttl). A pre-signed pool is spent over time, so each auth carries a spendability deadline — distinct from the per-request settle window (maxTimeoutSeconds). One knob controls both payment methods (Permit2 deadline and ERC-3009 validBefore): default 30 days (1 month); pass a number of seconds (floored at 600s = the verifier's default settle window, so an auth cannot expire between request acceptance and settlement); or pass never (also 0/none) for a non-expiring pool (mapped to the uint sentinel 4294967295, ~year 2106, which both contracts accept). Set per-buy with --auth-ttl <seconds|never> or globally via the OBOL_X402_AUTH_TTL env. A too-short value silently expires the pool minutes after buy.
Chain names follow the eRPC project aliases: mainnet, base, base-sepolia. CAIP-2 strings (eip155:1, eip155:8453, eip155:84532) and the alias ethereum are accepted on input and normalized internally. Unknown chains fail loudly with the supported list — buy.py will not silently sign against base-sepolia when the seller is on mainnet.
x402 payments do NOT require ETH for gas. The agent signs an EIP-3009
TransferWithAuthorization (or Permit2 witness) off-chain. The seller's
facilitator submits the on-chain settlement transaction and pays gas.
The agent only needs a balance of the settlement token (USDC or OBOL). Zero
ETH is fine.
The facilitator is the seller-side service that settles payments on-chain.
The agent does not call it directly — there is no facilitator URI flag
in any of these commands. The seller's x402-verifier middleware
coordinates with the facilitator after verifying your X-PAYMENT header.
The default Obol-operated facilitator at https://x402.gcp.obol.tech
covers eip155:1, eip155:8453, and eip155:84532.
Permit2-based x402 payments (e.g. OBOL, USDC on chains where the seller selects assetTransferMethod=permit2) require the agent's wallet to have approved the Permit2 router on the token before any payment can settle. Without it, buy.py pay/buy pre-signs a valid voucher, the seller's facilitator submits the on-chain transferFrom, and it reverts with no clear error — usually surfacing as an opaque HTTP 503 from the seller.
buy.py now pre-flights this check and aborts with the exact remediation command. If you see the error, run:
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/ethereum-local-wallet/scripts/signer.py send-tx \
--from <agent-wallet> --to <token-address> \
--data <approve-calldata-printed-by-buy.py> --network <chain>
This is one tx, ~46k gas, valid forever (unless the user later revokes). EIP-3009 flows (USDC TransferWithAuthorization) do not need this approval. Sellers that advertise eip2612GasSponsoring in their 402 extensions also bypass it (per-request signed permits).
extra.name is NOT the EIP-712 signing domain name. The 402 response
echoes the token contract's on-chain name() getter as extra.name. For
USDC the EIP-712 signing domain depends on the deployment:
name is "USD Coin" (matches name()).name is "USDC" (differs from name() →
"USD Coin").
buy.py resolves the right domain automatically via an in-script
USDC_EIP712_DOMAIN table; sellers can also override per-request via
extra.eip712Domain (Obol convention). Treat extra.name/extra.version
as human-readable display only.POST /v1/chat/completions; HTTP services typically expect GET / (or
a service-specific path). Pass --type http to probe for HTTP services
so the CLI does not append /v1/chat/completions to the URL. pay
defaults to --type http and --method GET.pay is stateless; buy is persistent. Do not use buy for a
type:http endpoint — its pipeline is inference-shaped (creates a
PurchaseRequest, expects a model name, publishes a paid/<model>
route). Use pay instead.pay-agent vs buy for agents. buy pushes a model behind LiteLLM
as paid/<remote-model> — great for inference, wrong for agents.
type:agent services return first-class responses (knowledge to save
to memory, tool-call traces, partial results to use mid-task), so the
calling agent wants to consume the stream itself. pay-agent streams
SSE events straight to stdout; the calling Hermes/OpenClaw re-emits
them through its normal user channel (telegram, obol hermes chat,
REST clients). No LiteLLM wire-up./api/services.json over parsing markdown. The seller's
storefront publishes machine-readable metadata at
<base>/api/services.json with full asset, EIP-712 signing domain,
transfer method, and atomic-unit price for every offered service.pay timeout defaults to ~100 s. This is the Cloudflare free-tier
tunnel cap — longer requests get killed by the edge before our client
ever sees a response. Reasoning models, long generations, or large
batches need --timeout <seconds> set explicitly, and the seller's own
upstream/edge limit still applies./ in remote model identifiers. LiteLLM's paid/* wildcard
route only matches a single segment; a remote vendor/model would
resolve to paid/vendor/model and miss the wildcard, so the request
falls through to the buyer sidecar and 404s. Sellers should use a non-
slash separator (e.g. vendor--model); buyers signing against a
legacy slashed name need the controller to insert an explicit LiteLLM
entry for the alias (addLiteLLMModelEntry).probedemo-hello, sponsored API endpoints) — paypay-agentbuybuy <same-name>listbalancestatusselldiscoveryethereum-local-walletobol-stack# Probe an inference endpoint to see its pricing (default --type inference)
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py probe https://seller.example.com/services/my-model/v1/chat/completions
# Probe an HTTP service (no /v1/chat/completions append, GET by default)
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py probe https://seller.example.com/services/demo-hello --type http
# One-shot paid HTTP request (sign 1 auth, attach X-PAYMENT, send GET, print response)
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py pay https://seller.example.com/services/demo-hello
# One-shot paid POST with a JSON body
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py pay https://seller.example.com/services/echo --method POST --data '{"hello":"world"}'
# One-shot paid STREAMING agent call (SSE events flushed to stdout as they arrive)
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py pay-agent \
https://seller.example.com/services/demo-quant \
--model qwen3.5:9b --message 'summarize the latest research on staking'
# Pay-agent with a full OpenAI-compatible body (stream:true is forced on)
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py pay-agent \
https://seller.example.com/services/demo-quant \
--model qwen3.5:9b \
--data '{"messages":[{"role":"user","content":"hello"}]}'
# Probe with the concrete remote model when the seller validates model IDs
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py probe https://seller.example.com/services/my-model/v1/chat/completions --model qwen3.5:35b
# Buy access (probes, pre-signs auths, creates/updates a PurchaseRequest)
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py buy remote-qwen \
--endpoint https://seller.example.com/services/my-model \
--model qwen3.5:35b
# Buy with agent-managed auto-refill intent
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py buy remote-qwen \
--endpoint https://seller.example.com/services/my-model \
--model qwen3.5:35b \
--count 100 \
--auto-refill \
--refill-threshold 20 \
--refill-count 50
# Manual top-up on the same purchase name
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py buy remote-qwen \
--endpoint https://seller.example.com/services/my-model \
--model qwen3.5:35b \
--count 25
# List purchased providers + remaining auths
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py list
# Check sidecar health + remaining auths
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py status remote-qwen
# Reconcile auto-refill policies (heartbeat / cron entrypoint)
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py process --all
# Check your USDC balance
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py balance
# Compatibility alias for the same reconcile loop
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py maintain
| Command | Description |
|---------|-------------|
| probe <url> [--model <id>] [--type http\|inference\|agent] [--method GET\|POST] | Send request without payment, parse 402 response for pricing |
| pay <url> [--type http\|inference] [--method GET\|POST] [--data <body>] | Single-shot paid request: sign 1 auth, attach X-PAYMENT, send |
| pay-agent <url> --model <id> [--message <text> \| --data <json>] [--timeout <s>] | Single-shot paid streaming agent call: SSE events flush to stdout as they arrive (default timeout 1h) |
| buy <name> --endpoint <url> --model <id> [--budget N] [--count N] | Pre-sign auths, create/update PurchaseRequest, expose paid/<model> |
| buy <name> --endpoint <url> --model <id> --set-default [--auto-refill] | As above, then set paid/<model> as the agent's own primary model in-pod (no restart, no host CLI) |
| process <name> \| --all | Reconcile autoRefill policies against live x402-buyer status |
| list | List purchased providers + remaining auth counts |
| status <name> | Check sidecar pod status + remaining auths |
| balance [--chain <network>] | Check agent's USDC balance via eRPC |
Use these when a human operator or foreground agent is actively deciding what to buy:
probe — inspect seller pricingbuy <new-name> — acquire a new purchasebuy <same-name> — manual top-up for that purchasestatus, list, balance — inspect live runtime stateUse this when Hermes or OpenClaw is maintaining existing purchases in the background:
process --all — maintenance reconcile loop for autoRefillCurrent controller-mode limitation:
process --all is the intended heartbeat / cron entrypoint for Hermes or OpenClaw.buy <same-name> is the manual top-up path.paid/<remote-model> alias at a time.refill is still not a first-class command.remove is still not a first-class command; deleting the PurchaseRequest
directly now enters a drain-first lifecycle instead of tearing the route down
immediately.Use the absolute script path inside the pod. Do not rely on cd ... && ...
shell wrapping.
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py process --all
Tell the agent to schedule that as its maintenance loop only when at least one
PurchaseRequest.spec.autoRefill.enabled=true purchase exists.
Hermes already has a cron scheduler. The maintenance job should load the buy-x402 skill and run the same reconcile primitive on a schedule.
CLI example:
hermes cron create "every 5m" \
"Reconcile existing x402 PurchaseRequests. Use the buy-x402 skill and run python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py process --all. Report only errors or state changes." \
--name "x402 buy reconcile" \
--skill buy-x402
Python API example:
from cron.jobs import create_job
create_job(
prompt="Reconcile existing x402 PurchaseRequests. Use the buy-x402 skill and run python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py process --all. Report only errors or state changes.",
schedule="every 5m",
name="x402 buy reconcile",
skills=["buy-x402"],
)
flowchart LR
subgraph Human["Human / Foreground"]
H1["probe"]
H2["buy <new-name>"]
H3["buy <same-name>"]
H4["status / list / balance"]
end
subgraph Agent["Agent / Background"]
A1["Hermes cron or OpenClaw heartbeat"]
A2["process --all"]
end
subgraph Control["Control Plane"]
PR["PurchaseRequest"]
RS["remote-signer"]
end
subgraph Runtime["Runtime Plane"]
C["serviceoffer-controller"]
X["x402-buyer /status"]
L["LiteLLM paid/<model>"]
end
S["Seller"]
H1 --> S
H2 --> RS
H2 --> PR
H3 --> RS
H3 --> PR
H4 --> X
A1 --> A2
A2 --> X
A2 --> RS
A2 --> PR
PR --> C
C --> X
C --> L
L --> X
X --> S
Probe: Sends a request without payment. The x402 gate returns 402 Payment Required with pricing info (payTo, network, amount; legacy sellers may still use maxAmountRequired).
Pre-sign: The agent signs N ERC-3009 TransferWithAuthorization vouchers via the remote-signer. Each voucher has a random nonce and is single-use (consumed on-chain when the facilitator settles).
Delete / drain behavior: deleting a PurchaseRequest does not
immediately remove the paid route if there are still authorizations remaining. The
controller marks the purchase as draining, keeps paid/<model> live, and
only tears the route down after the pre-authorized budget reaches zero. While
draining:
buy.py list and buy.py status <name> still show the purchasepaid/<model>buy <other-name> --model <same-model> is still rejectedFinal cleanup: once remaining == 0, the controller removes the buyer
config/auth material, removes paid/<model> if there is no other owner,
reloads the buyer sidecar, and clears the finalizer so the CR can disappear.
Declare: buy.py creates or updates a PurchaseRequest in the agent namespace with the pre-signed authorizations embedded in spec.preSignedAuths. When requested, it also stores spec.autoRefill intent on the CR.
Reconcile: The controller validates pricing, writes per-upstream buyer config/auth files into the x402-buyer-config and x402-buyer-auths ConfigMaps in llm, and keeps the paid model route available in LiteLLM.
Runtime mount: A lean Go sidecar (x402-buyer) already runs inside the existing litellm pod in the llm namespace. It mounts both ConfigMaps and serves as an OpenAI-compatible reverse proxy on 127.0.0.1:8402.
Wire: LiteLLM keeps one static wildcard route: paid/* -> openai/* -> 127.0.0.1:8402/v1. The controller also adds explicit paid-model entries when required so models with colons resolve reliably. The public model name is always paid/<remote-model>.
Runtime: On each request through the sidecar:
Heartbeat: buy.py process --all reads live x402-buyer /status,
checks each PurchaseRequest.spec.autoRefill policy, signs a fresh batch
when remaining auths are at or below the configured threshold, trims spent
history from spec.preSignedAuths, and patches the CR. The controller then
republishes the refreshed pool into llm.
flowchart LR
subgraph Agent["Agent Namespace"]
B["buy.py"]
RS["remote-signer"]
PR["PurchaseRequest"]
end
subgraph Runtime["llm Namespace"]
C["serviceoffer-controller"]
L["LiteLLM"]
X["x402-buyer"]
end
S["Seller Endpoint"]
B -->|"probe"| S
B -->|"sign auths"| RS
B -->|"create/update"| PR
B -->|"process --all"| PR
PR --> C
C -->|"write config/auth pool"| X
C -->|"publish paid/<model>"| L
L -->|"paid/<model>"| X
X -->|"402 retry with X-PAYMENT"| S
| Variable | Default | Description |
|----------|---------|-------------|
| REMOTE_SIGNER_URL | http://remote-signer:9000 | Remote-signer REST API |
| ERPC_URL | http://erpc.erpc.svc.cluster.local/rpc | eRPC gateway base URL |
| ERPC_NETWORK | base | Default chain for balance queries |
obol openclaw onboardghcr.io/obolnetwork/x402-buyer:latest must be available in clusterpaid/<remote-model>buy and each refill batch driven by process --allx402-buyer /status via status, list, or process --all, not only PurchaseRequest.statusautoRefill on the CR and run process --all from a scheduler to keep it topped upThis is the complete journey from discovering a seller to using purchased inference:
discovery skill)# Search the ERC-8004 registry for recently registered agents
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/discovery/scripts/discovery.py search --chain base-sepolia
# Fetch a candidate's registration JSON to check x402Support and services
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/discovery/scripts/discovery.py uri <agent-id> --chain base-sepolia
Look for agents with "x402Support": true and a "web" service endpoint.
# Send an unauthenticated request to get 402 pricing
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py probe <service-endpoint> --model <model-name>
This returns the seller's pricing: payTo, network, price, and asset (USDC contract).
# Check USDC balance
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py balance --chain base-sepolia
# Buy access (pre-sign auths, create PurchaseRequest, wait for controller reconciliation)
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py buy <friendly-name> \
--endpoint <service-endpoint> \
--model <model-name> \
--count 20
After buying, the model is available through LiteLLM as paid/<model-name>:
curl -X POST http://litellm.llm.svc.cluster.local:4000/v1/chat/completions \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "paid/<model-name>", "messages": [{"role": "user", "content": "hello"}]}'
The paid/ prefix routes through the x402-buyer sidecar, which transparently attaches payment headers.
# Check remaining auths
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py list
# Check one purchased upstream in detail
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py status <friendly-name>
# Reconcile auto-refill intent (what the heartbeat should run)
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py process --all
Manual refill and remove commands are still not available in the current
controller-based path. maintain is now only a compatibility alias for
process --all.
references/purchase-request-spec.md — Full PurchaseRequest CRD field referencereferences/x402-buyer-api.md — Wire formats for 402 responses, X-PAYMENT headers, and sidecar configdiscovery skill for finding sellers on the ERC-8004 registrydata-ai
Spawn durable child Hermes agents from inside Obol Stack. Creates child namespaces, optional profile/env Secrets, Agent CRDs, and optional ServiceOffers for x402-paid child services.
testing
Sell access to services via x402 payment gating. Create ServiceOffer CRDs that automatically health-check upstreams, create payment-gated routes, and optionally pull models and register on ERC-8004. Supports inference, HTTP, and fine-tuning service types.
testing
End-to-end guide for monetizing GPU resources or HTTP services through obol-stack. Covers pre-flight checks, model detection, pricing research, selling via x402, ERC-8004 registration, and verification. Use this skill when the user wants to monetize their machine.
databases
Query Ethereum networks through the local RPC gateway. Use when asked about blocks, balances, transactions, gas prices, token balances, or any eth_* JSON-RPC method. All queries are read-only and routed through the in-cluster eRPC load balancer.