stellar-agent-pay-cli
v0.1.0
Published
CLI that pays an x402-gated URL from the terminal: 402 -> pay -> unlock, for agents and shell scripts
Maintainers
Readme
stellar-agent-pay-cli
Pay an x402-gated URL from the terminal. One command does the whole loop: 402 → pay → unlock.
stellar-agent-pay https://api.example.com/weatherAny agent or shell script that can run a binary can now pay for API access. No custody logic, no manual transaction building.
Built for the GrantFox Stellar Builder Summit SP 2026 bounty, sub-lane 3B (CLI Plugins for Agents). It pairs with stellar-x402-paywall-kit (sub-lane 3A): gate a route with the kit, pay it with this CLI.
Contents: Install · Setup · Usage · Safety · MCP tool · Demo · Testnet runbook · Testing · Proof of work · Design notes and known limits · Troubleshooting
Install
Requires Node.js 20 or newer.
npm install -g stellar-agent-pay-cliNot on npm yet. Until it is published, install from source:
git clone https://github.com/StevenMolina22/stellar-agent-pay-cli.git
cd stellar-agent-pay-cli && npm install && npm linkSetup
Two environment variables:
export STELLAR_NETWORK=stellar:testnet
export STELLAR_SECRET_KEY=S... # needs a funded USDC balance, see the testnet runbookBoth can be overridden per call with --network and --secret.
Usage
# Pay and print the unlocked response body
stellar-agent-pay https://api.example.com/weather
# See what it would cost, without paying
stellar-agent-pay https://api.example.com/weather --dry-run
# Refuse to pay more than $0.01
stellar-agent-pay https://api.example.com/weather --max-price 0.01
# POST with a body
stellar-agent-pay https://api.example.com/report --method POST --data '{"city":"BA"}'
# Extra header, plus a JSON envelope on stdout
stellar-agent-pay https://api.example.com/weather --header "X-Trace: 1" --json
# Append an audit line per attempt
stellar-agent-pay https://api.example.com/weather --log-file ~/.stellar-agent-pay/payments.jsonlOptions
| Flag | What it does |
|---|---|
| --method <verb> | HTTP method. Default GET. |
| --data <json> | Request body, sent as JSON. |
| --header "K: V" | Extra request header. Repeatable. |
| --max-price <usd> | Refuse to pay more than this. Fails closed. |
| --allow-recipient <G...> | Only pay these recipients. Repeatable. Fails closed. |
| --block-recipient <G...> | Never pay these recipients. Repeatable. Wins over allow. |
| --dry-run | Show the price and requirements, pay nothing. |
| --network <id> | CAIP-2 network id. Default STELLAR_NETWORK or stellar:testnet. |
| --secret <S...> | Stellar secret key. Default STELLAR_SECRET_KEY. |
| --json | Wrap stdout as { status, body, settlement }. |
| --log-file <path> | Append one JSONL line per attempt (timestamp, url, status, amount, tx). |
| -h, --help | Show help. |
Output and exit codes
The response body goes to stdout. Settlement info (amount, tx hash, network) goes to stderr, so piping stays clean:
stellar-agent-pay https://api.example.com/weather | jq .temp| Code | Meaning |
|---|---|
| 0 | Success (2xx response after payment, or a --dry-run that needed no payment) |
| 1 | Config error: missing URL, missing secret key, malformed flag |
| 2 | Payment or HTTP error, including "every offer exceeded --max-price" |
Safety
x402 lets the server name its price per request. For an unattended agent that is a blank check, so this CLI ships three guardrails. All of them are checked before anything is signed, so a refusal costs no transaction and no fee.
--max-price <usd>caps what a single call may pay. Offers above the cap are filtered out; if nothing survives, the request fails instead of paying whatever was asked.--allow-recipient/--block-recipientfilter on the server'spayToaddress. A block-list entry always wins, so you cannot allow-list your way past a known-bad address by mistake.STELLAR_AGENT_PAY_SESSION_CAP_USDcaps cumulative spend across a whole MCP session. The one-shot CLI cannot do this (each run is a fresh process); the long-lived MCP server can.
Why the checks sit where they do, and what they do not cover, is in Design notes and known limits.
MCP tool
For MCP-aware agents (Claude Code and similar), the same logic is exposed as three tools instead of shelling out to the CLI.
| Tool | Purpose | Parameters |
|---|---|---|
| peek_paywall | Inspect the price without paying | url (required) |
| pay_url | Pay and return the unlocked resource | url (required), method (default GET), maxPriceUsd (e.g. "0.01") |
| session_status | Report spend so far and remaining budget | none |
npm install stellar-agent-pay-cli @modelcontextprotocol/sdk zod{
"mcpServers": {
"stellar-agent-pay": {
"command": "stellar-agent-pay-mcp",
"env": {
"STELLAR_NETWORK": "stellar:testnet",
"STELLAR_SECRET_KEY": "S...",
"STELLAR_AGENT_PAY_SESSION_CAP_USD": "1.00",
"STELLAR_AGENT_PAY_BLOCK_RECIPIENTS": "GBADACTOR1...,GBADACTOR2..."
}
}
}
}STELLAR_AGENT_PAY_ALLOW_RECIPIENTS and STELLAR_AGENT_PAY_BLOCK_RECIPIENTS (comma-separated G... addresses) apply to every pay_url call. They are set in the environment on purpose, not passed as tool parameters: the agent picks what to buy, the operator decides which guardrails apply.
Demo: pair it with the paywall kit
# Terminal 1: gate a route
cd stellar-x402-paywall-kit/examples/express-app && npm start
# Terminal 2: pay it
stellar-agent-pay http://localhost:3001/weatherTestnet runbook
- Generate a payer keypair and fund it with XLM:
node -e "const {Keypair}=require('@stellar/stellar-sdk'); const k=Keypair.random(); console.log(k.publicKey(), k.secret())" curl "https://friendbot.stellar.org?addr=<G...>" - Add a USDC trustline.
stellar-x402-paywall-kit/scripts/setup-testnet.mjssets up both sides of the demo. - Fund it with testnet USDC at faucet.circle.com (Stellar testnet, web only, cannot be scripted).
- Export the keys from step 1:
export STELLAR_SECRET_KEY=S... export STELLAR_NETWORK=stellar:testnet
Testing
npm test # 55 offline tests: no network, no facilitator, no funded accounttest/e2e.test.js adds 5 live tests that stand up a real paywall with the sibling kit and pay it through this package's own payUrl, against the live OZ Channels testnet facilitator. They cover the happy path, a --max-price refusal, a session-cap refusal, and the amount actually charged. They need the sibling repo checked out alongside this one plus a funded testnet payer:
OZ_API_KEY=... STELLAR_RECIPIENT=G... STELLAR_SECRET_KEY=S... npm test # 60/60Without those, the 5 live tests skip and the suite stays green, so npm test never fails for someone who has neither.
The live tests live here rather than in the seller kit because the buyer is what they exercise. An earlier arrangement where the kit owned the only live test meant that test rebuilt its own x402 client by hand and never touched this package's code.
Proof of work
Real payments on Stellar testnet, made by this CLI against the sibling repo's example server. Not mocked, not simulated. Verify any of them on Stellar Expert:
| tx hash | command |
|---|---|
| d8adef29...ee7e14fe | stellar-agent-pay http://localhost:3001/weather |
| 4b0524e7...cdab979b83 | stellar-agent-pay http://localhost:3001/weather --max-price 0.01 |
| 3ee14d47...9fed611 | stellar-agent-pay http://localhost:3001/weather/premium |
| bd0f08a6...27018d576 | stellar-agent-pay http://localhost:3001/weather --allow-recipient G... |
| c5917913...e6f06cd8 | stellar-agent-pay http://localhost:3002/catalog --max-price 0.01 |
| 8c2eca1e...c55d3333b | stellar-agent-pay http://localhost:3002/reports/3 --max-price 0.01 |
Design notes and known limits
Where the cap is enforced. The session cap runs on the x402 client's onBeforePaymentCreation hook (src/spendGuard.js), which fires after the client picks the offer it will pay and before the scheme signs anything. That is the only point where the price is known for certain. Fetching a price separately and then paying means two independent negotiations that can disagree about which offer is being bought, and accepts has no defined price ordering that would make the first one safe to assume. Concurrent MCP tool calls are serialized (src/sessionCap.js) so two payments cannot both pass the check before either is recorded.
This is an app-level approximation, not an on-chain guarantee. For a session budget with a real on-chain guarantee (pre-authorized deposit, cumulative commitments, one settlement), that is what MPP Channel mode is for rather than x402.
Recipient lists are the same guardrail shape as a contract allow-list on an on-chain policy signer, applied at the app level because x402 on Stellar does not ship one yet. The design thinking comes from ArkivGate, a policy gateway for paid AI-agent runtimes the author built independently. It was reimplemented here for Stellar's client-side PaymentPolicy API, not ported.
Replay and double-grant on the seller side. A signed x402 payment auth entry is valid until its max_ledger expiry, which bounds how long it is valid, not whether it has already been redeemed. Research on x402 deployments found resource servers that grant access repeatedly for a single settlement (Five Attacks on x402, arXiv:2605.11781). This CLI is the buyer side and does not control that. If you are building the seller, see the security note in the paywall kit.
A settled payment does not tell you what it cost. A live PAYMENT-RESPONSE from OZ Channels decodes to {success, payer, transaction, network} with no amount field, pinned by a live test in test/e2e.test.js so it fails loudly if that changes. Anything reporting settlement.amount records undefined on every payment, which is what this CLI's own audit log did until that test caught it. The price is only reliably known from the offer the client selected, so payUrl returns amountPaid from there.
Facilitator settle dedup is unverified for Stellar. Whether OZ Channels' /settle deduplicates a retried submission of the same auth entry is not documented anywhere we could find for the Stellar scheme. That is why the retry in src/resilientFetch.js is scoped to the transport layer only (a dropped connection mid-request) and never re-runs the payment flow. Retrying a delivery and signing a second payment are different things.
Dependency pinning. @x402/* is pinned to exact 2.20.0, not a floating ^ range. GHSA-3j63-5h8p-gf7c affected the older x402/x402-express/x402-hono/x402-next v1 packages, and GHSA-qr2g-p6q7-w82m hit @x402/svm facilitators rather than Stellar's. Neither applies here directly, but both show this protocol has had real facilitator-side vulnerabilities. Pin and watch advisories.
Troubleshooting
Config error: ... (exit 1) means a missing URL, a missing secret key, or a malformed flag. Check STELLAR_SECRET_KEY is exported.
Payment fails with an insufficient balance or trustline error. The payer account needs both a USDC trustline and a funded USDC balance. Walk through the testnet runbook again; the Circle faucet step is the one most often missed.
Every offer exceeded --max-price (exit 2) is working as intended. Run with --dry-run to see what the server is actually asking.
Crash on exit with Assertion failed: !(handle->flags & UV_HANDLE_CLOSING) (Windows). A real bug, already fixed. Calling process.exit() right after creating the Ed25519 signer could race a pending libuv handle. The fix uses process.exitCode and lets the event loop drain. If you see it, update.
License
MIT
