Cases: an AI agent buys an eSIM, then gets its money back
Contents
Real runs of the kit, with their transactions, so you can check every step on chain. Case 2 (section 10) is the same agent buying our capability through the hub. The first one is the whole agent path in both directions: an AI agent finds an offer, pays for it in USDC over x402, hands the eSIM to its owner, and later asks for a refund, which the seller node pays back without anyone touching a wallet.
1. What happened
On 15 October 2026 the owner of ARGUS, a personal AI agent, wrote to it in Telegram: buy me the cheapest eSIM for Turkey. ARGUS asked the kit's seller node for offers, picked Turkey 500 MB for 7 days at 0.31 USDC, asked its owner for approval, and paid from its own wallet. Seconds later the owner had the QR code and the activation code in the chat. ARGUS itself never saw them.
Thirty-four minutes later the owner asked ARGUS for a refund. The eSIM was not installed, so the seller node cancelled it at the wholesaler and sent the 0.31 USDC back to the agent's wallet. Every wallet and the wholesale balance ended where they started.
The seller was the kit's private test stand: an ordinary seller node, selling at cost, reachable only from the agent's host. Nothing in the run was simulated.
2. Who took part
| Party | What it is | Address or place |
|---|---|---|
| Owner | the person ARGUS works for; approves every payment in Telegram | a private Telegram chat |
| ARGUS | the agent (buyer); pays from its own wallet | 0x428f2Be4327908948e3B1915C55ddF8fA6bf73cc |
| Seller node | the kit's rails/ node (seller) with automatic refunds on | https://esim.modelmarket.dev/stand |
| Seller wallet | receives payments and pays refunds (PAY_TO and REFUND_KEY_FILE are the same wallet) | 0xF47161fBB56D0D910a17e503C8292903F41BbAE2 |
| Facilitator | Coinbase's x402 facilitator: verifies each signed transfer and puts it on chain, paying the gas | CDP |
| Wholesaler | eSIM Access: issues and cancels the profile | the operator's account |
3. The whole chain
sequenceDiagram
autonumber
actor O as Owner (Telegram)
participant A as ARGUS (agent)
participant S as Seller node
participant F as x402 facilitator
participant W as eSIM Access
O->>A: buy me the cheapest eSIM for Turkey
A->>S: POST /quote {"country":"TR"}
S-->>A: TR_0.5_7, 500 MB, 7 days, 0.31 USDC (signed)
A->>O: approve esim_buy?
O-->>A: /yes
A->>S: POST /order {"offer_id":"TR_0.5_7"}
S-->>A: 402 Payment Required: 0.31 USDC to the seller wallet
A->>S: the same order with PAYMENT-SIGNATURE (EIP-3009, signed by ARGUS)
S->>F: verify
S->>W: order one profile (idempotent by the payment)
W-->>S: profile 26101505200012
S->>F: settle
F-->>S: tx 0x617efd7a…3ae0
S-->>A: delivery: activation code, QR, status token
A->>O: QR and activation code, sent around the model
Note over O,W: 34 minutes later
O->>A: refund the last eSIM, I did not install it
A->>O: approve esim_refund?
O-->>A: /yes
A->>S: POST /refund {esim_tran_no, status_token}
S->>W: is it installed? any data used?
W-->>S: GOT_RESOURCE, not installed, 0 bytes
S->>W: cancel the profile
S->>F: verify and settle 0.31 USDC, seller wallet to ARGUS
F-->>S: tx 0x6dcfb7f0…ed4b
S-->>A: refunded
A->>O: refunded 0.31 USDC, tx 0x6dcfb7f0…ed4b
4. The transactions
All on Base mainnet, in USDC (0x8335…2913). Open any of them on basescan.org.
| Step | Transaction | Block, time (UTC) | From → to | Amount | Gas paid by |
|---|---|---|---|---|---|
| Funding the agent | 0x225afba5cd651e3ccdf70593fe985d1e24ddc14836270e83b13f546b6d017578 | 52624834 | owner's wallet 0x097e…475a → ARGUS | 0.50 | the owner |
| Purchase | 0x617efd7abe4b4c6a80b54046617a73415aa54df7ada5f002ce7260cf68ac3ae0 | 52626137, 05:20:21 | ARGUS → seller wallet | 0.31 | facilitator 0x93f6…be44 |
| Refund | 0x6dcfb7f075ad7fb6308064ac53e310dfb6adfd9b65d9f44b3c9a6dd39ca9ed4b | 52627173, 05:54:53 | seller wallet → ARGUS | 0.31 | facilitator 0x4c93…0205 |
Both the purchase and the refund are EIP-3009 transferWithAuthorization calls: the paying wallet signs, the facilitator submits. Neither ARGUS nor the seller wallet held any ETH, before, during or after.
5. Money and balances
flowchart LR
A["ARGUS wallet"] -- "purchase: 0.31 USDC (x402)" --> S["Seller wallet"]
S -- "refund: 0.31 USDC (EIP-3009 via the facilitator)" --> A
N["Seller node"] -- "order 0.30 USD, then cancel" --> W["eSIM Access balance"]
F["x402 facilitator"] -. "gas for both transfers" .-> B["Base"]
| Before | After the purchase | After the refund | |
|---|---|---|---|
| ARGUS wallet | 0.50 USDC | 0.19 USDC | 0.50 USDC |
| Seller wallet | 0 | 0.31 USDC | 0 |
| eSIM Access balance | $20.00 | $19.70 | $20.00 |
| ETH spent by ARGUS or the seller | 0 | 0 | 0 |
The stand sold at cost (MARGIN_PCT=0, MIN_MARGIN_USD=0.01): $0.30 wholesale, 0.31 USDC to the agent. With the kit's default margin the same plan sells for $1.30.
6. The refund rules
stateDiagram-v2
[*] --> delivered: paid and settled
delivered --> in_use: installed or data used
in_use --> [*]: 409 in_use, no refund
delivered --> cancelled: POST /refund with the status token
cancelled --> refunded: refund wallet pays the price back
cancelled --> due: no refund wallet, or the payout failed
due --> [*]: the operator pays it by hand
refunded --> [*]
- Only the payer can ask. The request carries the status token that came with the delivery. A wrong token gets
404, the same answer as an unknown eSIM. - Only an unused eSIM. The node asks the wholesaler first: an installed profile, or one that has spent any data, gets
409 in_useand stays as it is. - Only to the address that paid, only the price paid. The node keeps the payer and the price with the order, and never refunds more.
- Once. A second request, or two at once, gets the same answer and no second payment. A payout that failed is not tried again by itself: its transfer may still have landed, so the operator checks the chain first.
- Capped.
REFUND_MAX_USDper refund andREFUND_DAILY_MAX_USDper day, 50 each by default. The stand ran with 1 and 2. - Cancel first, then pay. The wholesaler refunds the operator's balance for the cancelled profile, so the operator loses nothing but the time.
7. Turning refunds on for your node
Refunds are off until you give the node a wallet to pay them from. Without it, POST /refund still cancels the unused eSIM and records the refund as due, and you get the agent_order.refund_due webhook to pay it yourself.
# a wallet that holds USDC on Base for refunds: PAY_TO's own wallet, so refunds return the money
# that came in, or a separate float. It needs no ETH: the facilitator pays the gas.
cd ~/esim-operator-kit
umask 077
printf 'Refund wallet private key: '; read -rs v; printf %s "$v" > secrets/refund_key; echo; unset v
sudo chown 1000:1000 secrets/refund_key && sudo chmod 600 secrets/refund_key
echo "REFUND_KEY_FILE=/run/secrets/esim/refund_key" >> .env
echo "REFUND_MAX_USD=20" >> .env
echo "REFUND_DAILY_MAX_USD=100" >> .env
docker compose up -d rails
curl -s https://<your domain>/agents/healthz # "refunds": "automatic"
| Setting | What it does |
|---|---|
REFUND_KEY_FILE | The key of the wallet refunds come from. The only private key the kit ever reads, and only when you opt in. |
REFUND_MAX_USD | The most one refund pays. |
REFUND_DAILY_MAX_USD | The most all refunds pay in a day. |
CDP_KEY_FILE | Already set for payments; refunds go through the same facilitator. |
Keep only a float on the refund wallet if it is not your main wallet: a key on a server is a key a break-in can read.
8. Asking for a refund
An agent needs the two values it got with the delivery:
curl -s -X POST https://<seller>/agents/refund -H 'content-type: application/json' \
-d '{"esim_tran_no":"26101505200012","status_token":"…"}'
{"success": true, "result": {"esim_tran_no": "26101505200012", "status": "refunded", "amount_usd": "0.31",
"to": "0x428f2be4327908948e3b1915c55ddf8fa6bf73cc", "at": "2026-10-15T05:54:50.399Z",
"transaction": "0x6dcfb7f075ad7fb6308064ac53e310dfb6adfd9b65d9f44b3c9a6dd39ca9ed4b"}}
status is refunded with the transaction, or due when the seller pays by hand. /listing.json says which: its refund.policy names the rule, and /healthz shows "refunds": "automatic" or "operator".
9. What we learned
- A careful agent questions an instant refund. Asked to "buy and refund right away" in one message, ARGUS stopped and asked what was wanted: buy and keep, buy nothing, or a refund for a real reason. The owner bought, then asked for the refund as its own request. Real agents will refund for real reasons, and a seller can expect them to ask politely and once.
- The activation code never left the delivery. The seller sends it only in the answer to the paid order; ARGUS forwarded it to the owner's chat and kept only the profile number and the status token. Neither the seller's records nor the agent's ledger hold it.
- No gas anywhere. The agent signed one authorization to pay, the seller wallet signed one to refund, and the facilitator did the rest.
- The wholesaler's refund is what makes a refund safe. The node cancels before it pays, so a refunded order costs the operator nothing.
10. Case 2: through the hub, from our public node
In case 1 the agent was given the seller's address. In case 2 it found the seller itself: ARGUS searched the modelmarket.dev hub for eSIM sellers, got our capability esim.quote@v1 of esim-modelmarket, asked it for offers through the hub, and paid the seller's own /order over x402. The hub routed the free quote and never touched the money.
For this test our public node was switched from demo to selling, and switched back to demo right after. The node behind esim-modelmarket (modelmarket.dev/providers/esim) sold for 52 minutes, from 06:56:55 to 07:48:33 UTC on 15 October 2026, at cost (MARGIN_PCT=0, MIN_MARGIN_USD=0.01), paying into the same seller wallet as the stand and refunding from it automatically (at most $2 per refund, $5 a day). It has refused every order with demo_mode again since 07:48:33.
sequenceDiagram
autonumber
actor O as Owner (Telegram)
participant A as ARGUS (agent)
participant H as modelmarket.dev hub
participant S as Seller node (modelmarket.dev/providers/esim)
participant F as x402 facilitator
O->>A: buy me the cheapest eSIM for Turkey
A->>H: GET /ai-market/v2/search?intent=esim
H-->>A: esim.quote@v1 of esim-modelmarket (via esim.modelmarket.dev)
A->>H: POST /ai-market/v2/invoke esim.quote@v1 (free)
H->>S: relays the quote
S-->>A: seller demo: false · TR_0.5_7 0.31 USDC · order at …/providers/esim/order
A->>O: [SELLS] TR_0.5_7 0.31 USDC · approve esim_buy?
O-->>A: /yes
A->>S: x402 straight to the seller: 402, then PAYMENT-SIGNATURE (EIP-3009)
S->>F: verify, then settle
F-->>S: tx 0xeea12962…881e
S-->>A: delivery
A->>O: QR and activation code, around the model
O->>A: refund the last eSIM
A->>O: approve esim_refund?
O-->>A: /yes
A->>S: POST /refund {esim_tran_no, status_token}
S->>F: cancel at eSIM Access, then 0.31 USDC back
F-->>S: tx 0x45dc5ce6…ec88
S-->>A: refunded
| Time (UTC) | What happened |
|---|---|
| 06:56:55 | The public node switches from demo to selling. Through the hub, its quote now says demo: false and 0.31 USDC |
| 07:21 | First attempt: ARGUS sees the offers but refuses to buy. The node was still called "modelmarket.dev demo seller", and the agent judged by the name |
| 07:23 | Fix deployed: the node's name follows its mode, and ARGUS labels every offer [SELLS] or [DEMO] from the seller's own answer |
| 07:47:35 | ARGUS buys TR_0.5_7 after the owner's /yes; eSIM 26101507470034 is delivered to the owner's Telegram |
| 07:48:02 | The owner asks for the refund, approves with /yes; the node cancels the unused eSIM and pays back |
| 07:48:33 | The public node is switched back to demo |
| Step | Transaction | Block, time (UTC) | From → to | USDC | Gas paid by |
|---|---|---|---|---|---|
| Purchase | 0xeea12962c4f494e55acc59a376203bd4b704804f19a84b4b8f47a96b265a881e | 52630556, 07:47:39 | ARGUS → seller wallet 0xF471…bAE2 | 0.31 | facilitator 0x68a9…7863 |
| Refund | 0x45dc5ce6579d8e889750442d63773586c481a11b30ddf44805960860f3bbec88 | 52630569, 07:48:05 | seller wallet → ARGUS | 0.31 | facilitator 0x625d…ac39 |
ARGUS was the only buyer in the window: the node's records hold one order, refunded. At the end ARGUS held 0.50 USDC again, the seller wallet 0, the eSIM Access balance $20.00, the eSIM was in state CANCEL, and no wallet had spent ETH.
What this case showed:
- An agent reads names. The seller's own quote said it was selling, but its name said "demo", and the agent believed the name. A seller node should be named for what it does; an agent should decide from the seller's answer, not its name.
- The hub finds, the seller sells. Discovery and the free quote went through the hub; the payment and the refund went straight between the agent and the seller, and the hub held no money at any point.
- Demo is one setting. The same node, the same catalogue and the same wholesale account sell or refuse depending on
DEMO_MODE: an operator can open and close a selling window in seconds.