@selat-ai/selat-pay
v0.5.0
Published
Standalone CLI that pays SELAT Router endpoints over x402 Gateway-batched. Keeps the open-source discovery skill free of heavy deps.
Readme
selat-pay
Standalone CLI that pays SELAT Router endpoints over x402 Gateway-batched, with MPC custody preserved via Circle Agent Wallets.
This tool is deliberately separate from the open-source runtime-integration-and-discovery skill. The skill stays lean (only @circle-fin/cli as an optional peer dep); selat-pay carries the heavier Gateway-batching SDK + viem so users who don't need routing don't pay the install cost.
Why this exists
circle services pay cannot complete the SELAT Router round-trip — it does its own internal probe and doesn't forward the router's x-selat-quote-id response header on the signed retry. This CLI does the probe + retry manually, using:
@circle-fin/x402-batching/client'sBatchEvmSchemeto construct the Gateway-batched EIP-712 payload (protocol-spec correctness).circle wallet sign typed-datafor the actual signature (MPC custody preserved; no raw private key in the skill).
A --raw-key mode is provided for development that bypasses Circle CLI and signs locally with SELAT_PRIVATE_KEY. Do not use --raw-key in production.
Install
cd "$(dirname "$0")"
npm installOr run directly without install (once published):
npx @selat-ai/selat-pay GET https://upstream/api --chain base --max-amount 0.05Setup
.env in this folder (or process environment):
# Demo router — plain HTTP at a raw IP, intermittent. Replace with an
# https:// router URL when one is available.
SELAT_ROUTER_URL=https://router.selat.ai
SELAT_AGENT_WALLET_ADDRESS=0xb71105c418b671cd8e6b983611c1fa142d22f51bPlus Circle CLI authenticated:
circle wallet login <your-email> --type agentFor dev / non-MPC signing only:
SELAT_PRIVATE_KEY=0x<32-byte hex>Usage
# Probe only — see the 402 terms without paying
selat-pay GET "https://pro-api.coinmarketcap.com/x402/v3/cryptocurrency/quotes/latest?symbol=eth" \
--chain base --probe-only
# Paid call (GET)
selat-pay GET "https://upstream/api" \
--chain base --max-amount 0.05
# Paid call (POST with JSON body)
selat-pay POST "https://upstream/api" \
--body '{"foo":"bar"}' \
--chain base --max-amount 0.05
# Local-key signing (dev only)
SELAT_PRIVATE_KEY=0x... \
selat-pay GET "https://upstream/api" --chain base --max-amount 0.05 --raw-key
# List chains the SDK knows about
selat-pay --list-chainsFlow
selat-pay probes the upstream directly first to detect its scheme, then picks one of two paths:
Direct mode (Gateway-batched upstream — no router hop, no markup):
selat-pay Upstream Agent Wallet (MPC)
│ probe │ │
├────────────────────► │ │
│ ◄── 402 (GatewayWalletBatched) + PAYMENT-REQUIRED │
│ │
│ build EIP-712 typed data (BatchEvmScheme) │
│ shell: circle wallet sign typed-data ─────────────►│
│ ◄────────────── signature ────────────────────────│
│ │
│ retry with Payment-Signature │
├────────────────────► │ │
│ ◄── upstream response │ │Routed mode (erc-3009 or tempo-native upstream — router translates):
selat-pay Upstream SELAT Router Agent Wallet (MPC)
│ probe │ │ │
├──────────────► │ │ │
│ ◄── 402 (erc-3009 / tempo-native) │
│ │
│ probe ${routerUrl}/proxy?target=upstream │
├─────────────────────────────────► │ │
│ │ probe upstream │
│ ├──────────────► … │
│ ◄── 402 (Gateway-batched) + x-selat-quote-id │
│ │
│ build EIP-712 + sign (same as above) ───────────────────► │
│ ◄──────── signature ───────────────────────────────────── │
│ │
│ retry with x-selat-quote-id + Payment-Signature │
├─────────────────────────────────► │ │
│ │ sign outbound (mpp/erc-3009)
│ ├──────────────► upstream
│ ◄── upstream response ────────────┤ │The mode is auto-detected per call. The first line of stderr after [selat-pay] probing upstream directly reports mode=direct or mode=routed. Router URL is only required for routed mode; direct mode skips it.
Outputs
- stdout: upstream response body (parsed JSON if possible, raw text otherwise). Pipe-friendly.
- stderr:
[selat-pay]progress lines (quoteId, price, status). Suppressed from stdout for clean piping.
Example:
selat-pay GET ... 2>/dev/null | jq .Exit codes
0— paid call succeeded with 2xx upstream response, or probe-only completed.1— runtime error (probe failed, cost cap exceeded, upstream non-2xx, network error).2— missing dependency (@circle-fin/x402-batchingnot installed).
Trade-offs and known limits
- One Gateway-batched signing scheme only. This CLI only signs against Circle's
GatewayWalletBatchedscheme. The router refuses inbound payments in any other scheme today, so that matches; if the router ever supports erc-3009 inbound, this CLI won't help with that path. - No quote attestation verification yet. The router doesn't currently attest its 402 quotes with a router key, so this CLI can't pin the operator's claimed upstream price. Once the router lands EIP-712-attested quotes, this CLI should verify the attestation and surface the catalog-vs-quoted comparison before paying.
- Routing markup is a flat, transparent 5%. The router adds a 5% markup on routed cross-protocol calls; direct Gateway-batched calls have zero markup. The fee is disclosed, not hidden — you pay SELAT only when it does cross-rail routing work. (Separately, see the quote-attestation note above: until the router attests its quotes, this CLI can't yet cryptographically pin the operator's claimed upstream base price; you can cross-check it against the federated discovery catalog's
payments[].amountUsdvia--probe-only.) --raw-keyis dev-only. Loses MPC custody. Useful for local testing against a dev router; do not deploy with this flag.
Roadmap
- Verify router-attested quotes once the router supports attestation.
- Auto-discover available routers from a registry (Phase 2 federation).
- Surface markup vs. catalog price warning before pay.
- Per-call telemetry (opt-in).
License
selat-pay/ is Apache-2.0 licensed (see LICENSE). The CLI is free to use, modify, and redistribute under those terms.
This is the client side of the SELAT ecosystem and is intentionally open to encourage adoption. The SELAT Router server that this CLI talks to (everything outside the selat-pay/ directory in this repo) is proprietary — see the repository root LICENSE.
