Wiring payments into an agent is easy to get wrong in a way that only shows up with real money: a retry after a network timeout that pays the same invoice twice, a recipient address copied out of a stale config, a webhook handler that accepts a replayed delivery. None of that is visible on a mainnet balance you cannot undo.
The cheapest place to find those bugs is a test network, and the agent API treats one almost exactly like mainnet: same endpoints, same X-Agent-Key header, same response shapes.
Why a testnet transfer finds what unit tests miss
A mock proves your code runs. It cannot prove what sits on the other side of the call: that the wallet your agent was handed is readable, that the amount clears the fee preflight, that your signing secret verifies a real delivery, that your server answers inside the retry window. The API reference suggests the same drill: subscribe an endpoint, make a small transfer on a test network such as Sepolia, and confirm the delivery reaches your server.
Three things a live testnet run proves:
- Your agent can read the wallet it was given, and the balance it sees is the balance you expect.
- The transfer passes the minimum-transferable check, fees included.
- Your server verifies the signature and answers 2xx fast enough that the platform never retries.
Step 1: create a wallet your mistakes cannot cost anything
Create wallets on sepolia, mainnet or polygon-mainnet (EVM), or on solana-devnet, solana-testnet or solana-mainnet. Start on a test network.
export OCC_AGENT_KEY="occ_your_api_key"
curl -X POST https://openclawcash.com/api/agent/wallets/create \
-H "Content-Type: application/json" \
-H "X-Agent-Key: $OCC_AGENT_KEY" \
-d '{
"label": "agent-testnet",
"network": "sepolia",
"exportPassphrase": "store-this-before-you-call",
"exportPassphraseStorageType": "env",
"exportPassphraseStorageRef": "WALLET_EXPORT_PASSPHRASE_AGENT_TESTNET",
"confirmExportPassphraseSaved": true
}'
Two reasons for that shape. It follows the passphrase-safe flow: you persist the export passphrase first and then assert you did with confirmExportPassphraseSaved. And it needs wallet creation permission on the key, a scope chosen when the key is made. For this phase leave the key's live-transactions flag off: with it off, the key can only act on test networks, so a bug in your loop cannot move real value.
The response is the wallet record: id (the walletId you pass everywhere else), label, address, network, chain.
Step 2: discover, then read the balance
curl https://openclawcash.com/api/agent/wallets \
-H "X-Agent-Key: $OCC_AGENT_KEY"
curl "https://openclawcash.com/api/agent/wallet?walletId=AB12CD3&chain=evm" \
-H "X-Agent-Key: $OCC_AGENT_KEY"
The list is discovery only, cheap in an agent loop. Wallet detail returns nativeBalanceDisplay and tokenBalances, where an "is there enough to pay this invoice" branch gets tested as written. Fund the address from a testnet faucet, then read again.
Step 3: make the transfer, then read the record
curl -X POST https://openclawcash.com/api/agent/transfer \
-H "Content-Type: application/json" \
-H "X-Agent-Key: $OCC_AGENT_KEY" \
-d '{
"chain": "evm",
"walletId": "AB12CD3",
"to": "0xRecipientWalletAddress",
"amountDisplay": "0.01"
}'
curl "https://openclawcash.com/api/agent/transactions?walletId=AB12CD3&network=sepolia" \
-H "X-Agent-Key: $OCC_AGENT_KEY"
Two rules trip people up here. Send exactly one amount field: amountDisplay for human units or valueBaseUnits for base units, never both, or the call answers 400. And native transfers run a preflight for the minimum transferable amount, so a wallet holding dust comes back amount_below_min_transfer with the minimum it needs, instead of broadcasting a transaction that cannot pay for itself.
Every history row carries data.network; scope with network=sepolia, or network=all to merge every EVM chain.
Step 4: hear the payment on your own server
Wallet events are a named group, wallet.transaction.confirmed, never covered by a * subscription, so list it by name.
curl -X POST https://openclawcash.com/api/agent/checkout/webhooks \
-H "Content-Type: application/json" \
-H "X-Agent-Key: $OCC_AGENT_KEY" \
-H "Idempotency-Key: webhook-create-001" \
-d '{
"url": "https://your-server.example/occ",
"eventTypes": ["wallet.transaction.confirmed"],
"enabled": true
}'
You get back a publicId and a secret starting with whsec_, shown once. Keep it server side, never in the agent's context. Then hold every delivery to the documented checks: read the raw request body before any JSON parsing, reject a webhook-timestamp older than 5 minutes, and compare the HMAC-SHA256 of {id}.{timestamp}.{body} (base64) against the v1 entry of webhook-signature in constant time. De-duplicate on webhook-id, because a delivery can arrive more than once, and answer 2xx within 10 seconds. Miss that window and failed deliveries are retried for about a day; answer 410 and the endpoint is switched off.
A testnet transfer produces the same event as a mainnet one. That is the point: watch the duplicate delivery, the stale timestamp and the 410 path fail on Sepolia, not on a customer's invoice.
What changes when you go live
Once the loop is boring, issue a second key for production with live transactions on, scope it to one chain and one wallet, and attach the wallet's policies so the server refuses anything that violates them before it is signed. Keep the testnet key for your test suite.
Two limits, plainly. Scopes and policies are enforced on the platform, not by your agent's code, so a production key with live transactions on and no policy attached is as dangerous as the private key you were avoiding. And revocation is a dashboard action: delete the key, the wallet keeps its balance, because the wallet's key material was never bound to the API key.
Endpoint paths, headers and fields above come from the published API reference at openclawcash.com/docs; the key-scope flags are described in the site's setup guide, Hermes Agent Wallet Setup: Safe by Default.
Top comments (0)