DEV Community

OpenClaw Cash
OpenClaw Cash

Posted on

Resolve token addresses for your agent from a public token list

An agent that is told "send 25 USDC on Base" has to turn a symbol into a contract address before anything moves. Guessing that address, or trusting whatever a model remembers, is how money ends up at a token that only looks right. Symbols are not unique on chain. Addresses are.

OpenClawCash publishes the token metadata its wallets work with as a public document in the Token Lists format, the JSON schema Uniswap started for sharing ERC20 lists. It needs no key, so your agent, your backend or a plain shell can read it before it ever asks the wallet to act.

This walkthrough takes you from a symbol to a transfer request that names the token by address.

1. Fetch the list for one chain

The endpoint is GET /api/public/tokenlist. No auth header, CORS is open, and the response is cacheable.

curl "https://openclawcash.com/api/public/tokenlist?chainId=8453"
Enter fullscreen mode Exit fullscreen mode

chainId scopes the list to one chain. The docs list 1 Mainnet, 137 Polygon, 8453 Base, 11155111 Sepolia and 101 Solana mainnet. Leave it off to get every chain in one document.

The response is a standard token list: a name, a version object, and a tokens array where every entry carries chainId, address, name, symbol, decimals and logoURI.

2. Pick curated or extended

By default the list is extended: the OpenClawCash curated set merged with the Uniswap default list on EVM chains and the Jupiter verified list on Solana. Where both name the same (chainId, address), the curated entry wins.

If you want the short list with no external sources behind it, ask for that:

curl "https://openclawcash.com/api/public/tokenlist?extended=false"
Enter fullscreen mode Exit fullscreen mode

When I fetched both on 2026-10-19, the extended Base list held 160 entries and the curated list held 67 entries across all five chains. Those counts will move as the upstream lists change, so read them, do not hard code them.

A sensible default for an agent that pays people: resolve from the curated list, and only fall back to the extended one when a human asked for a specific token.

3. Resolve a symbol to an address

With jq the lookup is one line:

curl -s "https://openclawcash.com/api/public/tokenlist?chainId=8453" \
  | jq -r '.tokens[] | select(.symbol=="USDC") | "\(.address) \(.decimals)"'
Enter fullscreen mode Exit fullscreen mode

On Base that returns:

0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 6
Enter fullscreen mode Exit fullscreen mode

The same idea in Node, straight from the docs:

const response = await fetch("https://openclawcash.com/api/public/tokenlist?chainId=8453");
const tokenList = await response.json();
console.log(tokenList.tokens.length, "Base tokens");
Enter fullscreen mode Exit fullscreen mode

If your filter ever returns more than one row for a symbol, stop and ask. That is exactly the case where guessing goes wrong.

4. Check the balance by address

From here the calls need your agent key. POST /api/agent/token-balance accepts tokenAddress, so you can confirm what the wallet holds of that exact contract:

curl -X POST https://openclawcash.com/api/agent/token-balance \
  -H "Content-Type: application/json" \
  -H "X-Agent-Key: occ_your_api_key" \
  -d '{ "walletId": "Q7X2K9P", "tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "chain": "evm" }'
Enter fullscreen mode Exit fullscreen mode

The response is a balances array with symbol, balance and decimals per token.

5. Send by address, in base units

POST /api/agent/transfer takes the token "by symbol or address/mint". Passing the address you just resolved removes the ambiguity, and decimals from the list tells you how to write valueBaseUnits. USDC has 6 decimals, so 25 USDC is 25000000:

curl -X POST https://openclawcash.com/api/agent/transfer \
  -H "Content-Type: application/json" \
  -H "X-Agent-Key: occ_your_api_key" \
  -d '{
    "chain": "evm",
    "walletId": "Q7X2K9P",
    "network": "base-mainnet",
    "to": "0xRecipientWalletAddress...",
    "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "valueBaseUnits": "25000000"
  }'
Enter fullscreen mode Exit fullscreen mode

Two rules from the docs worth knowing here. Send exactly one of amountDisplay or valueBaseUnits, never both, or you get a 400. And network lets an EVM wallet act on a chain other than its default, which is why base-mainnet is set explicitly above.

The wallet's policies still run before anything is signed, whichever way you named the token.

What the list is not

It is not a whitelist of what the wallet will touch. The authenticated GET /api/agent/supported-tokens endpoint says it plainly: any valid ERC20 contract on EVM and any valid SPL mint on Solana works in transfer, swap and balance calls. The public list is the set of well known tokens with metadata you can trust as a starting point, not a limit.

It is also not a price feed. There is no price field in a token list, so value checks belong elsewhere.

Wrap up

One unauthenticated GET gives your agent addresses and decimals it can check, instead of addresses it remembers. Resolve first, then pass the address into the wallet calls. The full reference for every endpoint above is at openclawcash.com/docs.

Top comments (0)