AMD Docs

Build on the AMD contract.

Everything here runs against the live AMD contract on Robinhood Chain and the live gateway. Samples use curl and cast (Foundry); the same calls are one MCP tool each for an agent.

Getting started

AMD is one contract on Robinhood Chain (chain id 4663) plus a gateway at amd.mobi. Merchants publish services and endpoints on chain; agents pay per request in USDG (6 decimals); the gateway verifies each payment on chain and forwards the request to the upstream API.

Two ways in: connect a wallet at amd.mobi/dashboard and click through, or use the contract directly. The setup below reads the contract's address out of the site's JS binding and asks the contract for the USDG address, so nothing in a copied snippet goes stale. The ids in the samples (service 1, endpoint 1, session 7) stand for the ones your own calls return.

# chain
export RPC=https://rpc.mainnet.chain.robinhood.com     # chain id 4663
export PK="<your private key>"                           # the wallet that signs
export ME=$(cast wallet address --private-key $PK)
# the AMD contract, read out of the live binding
export AMD=$(curl -s https://amd.mobi/js/contract.js \
  | sed -nE "s/^export const GATEWAY = '(0x[0-9a-fA-F]{40})'.*/\1/p")
# USDG, as the contract itself reports it
export USDG=$(cast call $AMD "USDG()(address)" --rpc-url $RPC)
# sanity: the fee in basis points (100 = 1%, never above 200)
cast call $AMD "feeBps()(uint16)" --rpc-url $RPC

The public RPC sends no CORS headers, so browsers go through POST https://amd.mobi/api/rpc, a read-only JSON-RPC proxy (batches of at most five). From a shell, cast talks to the RPC directly. Block time is about 0.1 s and eth_getLogs fails above 10,000 matches, so always filter logs by topics and page by block range.

Create a service

A merchant is one address; the first call is createMerchant(name, payout) (payout zero means "pay me at this address"). A service is the upstream API you put behind the gateway. Its auth secret is never stored in clear: it is encrypted to the gateway's P-256 key (GET /api/key) with ECIES before it goes on chain, and only the gateway can decrypt it when it forwards a request.

# 1. a merchant (one per address; payout 0x0 = msg.sender)
cast send $AMD "createMerchant(string,address)" "Acme Data" $(cast address-zero) \
  --rpc-url $RPC --private-key $PK

# 2. encrypt the upstream secret to the gateway key with js/ecies.js,
# the same code the dashboard runs. Node refuses to import a module
# over https, so fetch the file first and import the file.
export SECRET="<your upstream API key>"
curl -s https://amd.mobi/js/ecies.js -o ecies.mjs
export CIPHER=$(node --input-type=module -e '
  const { encrypt } = await import("./ecies.mjs");
  const jwk = await (await fetch("https://amd.mobi/api/key")).json();
  const bytes = await encrypt(jwk, process.env.SECRET);
  console.log("0x" + Buffer.from(bytes).toString("hex"));')

# 3. the service: name, description, base URL, docs URL, favicon URL,
# authMethod (1 = Bearer), authField, cipher, livemode
cast send $AMD \
  "createService(address,string,string,string,string,string,uint8,string,bytes,bool)" \
  $ME "WebScrape" "Scrape any page as JSON" "https://api.webscrape.dev" \
  "https://webscrape.dev/docs" "https://webscrape.dev/favicon.ico" \
  1 "" $CIPHER true \
  --rpc-url $RPC --private-key $PK

# the id it got
cast call $AMD "servicesOf(address)(uint32[])" $ME --rpc-url $RPC

Live mode means "paid on mainnet". A test-mode service (livemode = false) accepts payments if an agent insists, but the gateway never demands one and the catalog hides it. Flip it with updateService when you are ready. Team members can be added with setOperator(merchant, who, role): Members manage endpoints, Admins everything except the payout, ownership and other Admins.

Add endpoints

An endpoint is a method plus a path with a flat price in USDG base units (50000 = $0.05). Paths match exactly; a stored path ending in /* matches any suffix. Methods are an enum: GET 0, POST 1, PUT 2, PATCH 3, DELETE 4.

# serviceId, method (0 = GET), path, description, price (USDG base units)
cast send $AMD "createEndpoint(uint32,uint8,string,string,uint96)" \
  1 0 "/v1/scrape" "Scrape one page" 50000 --rpc-url $RPC --private-key $PK
cast send $AMD "createEndpoint(uint32,uint8,string,string,uint96)" \
  1 1 "/v1/batch" "Up to 100 pages" 1000000 --rpc-url $RPC --private-key $PK

# list, quote, reprice, pause
cast call $AMD "endpointsOf(uint32)(uint32[])" 1 --rpc-url $RPC
# quote: amount, fee, payout
cast call $AMD "quote(uint32)(uint96,uint96,address)" 1 --rpc-url $RPC
cast send $AMD "updateEndpoint(uint32,uint8,string,string,uint96)" \
  1 0 "/v1/scrape" "Scrape one page" 80000 --rpc-url $RPC --private-key $PK
cast send $AMD "setEndpointsEnabled(uint32[],bool)" "[2]" false \
  --rpc-url $RPC --private-key $PK

The dashboard imports an OpenAPI spec through POST /api/fetch-openapi ({ url }, 2 MB cap) and creates every route in bulk; each becomes one createEndpoint call.

Auth methods

The gateway applies the service's auth to every forwarded request. authMethod is an enum and authField names the header or query parameter where one is needed.

enummethodwhat the upstream receives
0Nonethe request as is (public APIs)
1BearerAuthorization: Bearer <secret>
2ApiKeyHeader<authField>: <secret>
3ApiKeyQuery?<authField>=<secret> appended to the query
4CustomHeader<authField>: <secret>

Rotate a secret with setServiceAuth(id, authMethod, authField, authCipher). The gateway strips hop-by-hop headers and X-PAYMENT, forwards the body and content type, and adds X-Forwarded-Payer with the payer's address so your API can rate-limit or personalise per agent.

Publish and gateway URLs

Publishing gives a service a slug ([a-z0-9-], at most 40 bytes, unique, immutable once set) and lists it in the catalog when it is live. The gateway URL is https://amd.mobi/gw/<slug>/<path>.

cast send $AMD "publishService(uint32,string)" 1 webscrape \
  --rpc-url $RPC --private-key $PK
cast call $AMD "serviceBySlug(string)(uint32)" webscrape --rpc-url $RPC
# the URL agents call
curl -si "https://amd.mobi/gw/webscrape/v1/scrape?url=https://example.com" | head -3
# HTTP/2 402 ... with the payment requirements in the body (next section)
# take it down or put it back (re-publish with "" keeps the slug)
cast send $AMD "unpublishService(uint32)" 1 --rpc-url $RPC --private-key $PK
cast send $AMD "publishService(uint32,string)" 1 "" --rpc-url $RPC --private-key $PK

Unknown, unpublished or deleted slugs answer 404 JSON. A path that exists with other methods answers 405.

x402 on Robinhood Chain

A paid request without payment gets 402 and a body that says exactly what to pay. The agent settles through the contract, then retries with the transaction hash.

# the 402 body
{
  "x402Version": 1,
  "error": "X-PAYMENT header is required",
  "accepts": [{
    "scheme": "exact", "network": "robinhood-4663",
    "maxAmountRequired": "50000",
    "resource": "https://amd.mobi/gw/webscrape/v1/scrape",
    "description": "Scrape one page", "mimeType": "application/json",
    "payTo": "0x<payout>", "asset": "0x<USDG>", "maxTimeoutSeconds": 600,
    "extra": { "name": "Global Dollar", "symbol": "USDG", "decimals": 6,
               "contract": "0x<AMD>", "endpointId": 1,
               "requestHash": "0x...", "method": "pay(uint32,bytes32)" }
  }]
}

The request hash binds a payment to one request shape: keccak256("METHOD /path?canonicalQuery"). The canonical query is not the query you typed: the query is parsed first (so %XX and + are decoded), then every key and every value goes through encodeURIComponent, so url=https://example.com is hashed as url=https%3A%2F%2Fexample.com. The pairs are then sorted by encoded key and, on a tie, by encoded value, joined with &, and the ? is kept even when the query is empty. It is the same function in js/contract.js (requestHash), in js/x402.js (requestShape, which takes the whole gateway URL and returns the hash), and in the MCP tool prepare_pay.

# 1. the hash for GET /v1/scrape?url=https://example.com
# mind the encoding: the value is percent-encoded before it is hashed
export HASH=$(cast keccak "GET /v1/scrape?url=https%3A%2F%2Fexample.com")
# -> 0x2d733ee5eb43b1e760e8491891039c0a662fe8a21b42bc77e3359e8827689542
# (the MCP tool prepare_pay takes the raw query and encodes it for you)
# 2. allow the contract to pull USDG once (amount in base units)
cast send $USDG "approve(address,uint256)" $AMD 50000 \
  --rpc-url $RPC --private-key $PK
# 3. pay endpoint 1; the receipt id comes back as the return value
# and in the Paid event
export TX=$(cast send $AMD "pay(uint32,bytes32)" 1 $HASH \
  --rpc-url $RPC --private-key $PK --json | jq -r .transactionHash)
# 4. retry with the receipt (a bare tx hash, or base64 of the x402 payload)
curl -s -D - -H "X-PAYMENT: $TX" \
  "https://amd.mobi/gw/webscrape/v1/scrape?url=https://example.com"
# X-PAYMENT-RESPONSE: base64
# {"success":true,"txHash":"0x..","network":"robinhood-4663",
#  "payer":"0x..","receiptId":"0x.."}

Verification. The gateway loads the receipt, requires status 1, a Paid event from the AMD contract whose endpointId and requestHash match, amount at least the price, and a block timestamp within 600 s of now. Any mismatch is a 402 with the same body and a specific error. A receipt pays for one request shape inside that window; the gateway keeps a best-effort in-memory set of used hashes per instance, so do not rely on a receipt being reusable.

Money flow. pay moves USDG from the payer to the merchant's payout address and the protocol fee (feeBps, 1% today, at most 2%) to the treasury in one transaction. The contract never holds merchant money. A merchant owner or Admin can refund(receiptId, amount) from their own USDG, never above what was paid.

Prepaid sessions

For stateful or high-frequency APIs an agent deposits once into escrow, then signs an EIP-712 voucher for the running total with each request. The merchant settles the latest voucher on chain whenever they like; the difference to what was settled before is paid out (the fee applies).

# agent: open a session on service 1 with 5 USDG (approve first)
cast send $USDG "approve(address,uint256)" $AMD 5000000 \
  --rpc-url $RPC --private-key $PK
cast send $AMD "openSession(uint32,uint96)" 1 5000000 \
  --rpc-url $RPC --private-key $PK
cast call $AMD "sessionsOf(address)(uint32[])" $ME --rpc-url $RPC

# agent: per request, sign Voucher{ sessionId, cumulative } (EIP-712; the MCP tool
# sign_voucher_payload returns the typed data)
# and send it as X-SESSION = base64 of
# {"sessionId":7,"cumulative":"120000","signature":"0x..."}
export V='{"sessionId":7,"cumulative":"120000","signature":"0x..."}'
curl -s -H "X-SESSION: $(printf '%s' "$V" | base64)" \
  "https://amd.mobi/gw/webscrape/v1/scrape?url=https://example.com"

# merchant: settle the latest voucher, close when done
# (the agent may close after 24h of silence)
cast send $AMD "settleSession(uint32,uint96,bytes)" 7 120000 "0x<signature>" \
  --rpc-url $RPC --private-key $PK
cast send $AMD "closeSession(uint32)" 7 --rpc-url $RPC --private-key $PK

The voucher's cumulative must be at least the price above what the chain has settled plus what the gateway has already seen in memory; vouchers are monotonic and never above the deposit. The gateway echoes the latest voucher in X-SESSION-STATE so the dashboard's session card can settle it. Domain and type hash: DOMAIN_SEPARATOR(), VOUCHER_TYPEHASH().

Subscriptions

A plan is a recurring charge on a service: an amount every intervalSeconds for a resource key. Subscribing charges the first period; anyone can call charge once a period is due, which pulls exactly one period from the subscriber's USDG allowance.

# merchant: a plan on service 1, 2 USDG every 30 days
cast send $AMD "createPlan(uint32,string,string,string,uint96,uint32)" \
  1 "Pro" "seat" "Unlimited scrapes" 2000000 2592000 \
  --rpc-url $RPC --private-key $PK

# subscriber: approve enough for several periods, subscribe with a resource id
cast send $USDG "approve(address,uint256)" $AMD 24000000 \
  --rpc-url $RPC --private-key $PK
cast send $AMD "subscribe(uint32,bytes32)" 1 $(cast keccak "agent-7") \
  --rpc-url $RPC --private-key $PK

# anyone, when due
cast call $AMD "isDue(uint32)(bool)" 1 --rpc-url $RPC
cast send $AMD "charge(uint32)" 1 --rpc-url $RPC --private-key $PK
# subscriber or merchant
cast send $AMD "cancelSubscription(uint32)" 1 --rpc-url $RPC --private-key $PK

Spending controls

An account deposits USDG into the contract and gives each agent address a daily budget, a per-transaction limit and an optional list of allowed services. The agent then pays with payFrom out of the account's deposit; the contract enforces every limit, and the day rolls forward in whole days from dayStart.

# account owner: deposit 100 USDG, allow agent 0xA9 to spend 25 USDG a
# day, 5 USDG per call, on services 1 and 3 only
cast send $USDG "approve(address,uint256)" $AMD 100000000 \
  --rpc-url $RPC --private-key $PK
cast send $AMD "deposit(uint96)" 100000000 --rpc-url $RPC --private-key $PK
cast send $AMD "setControls(address,uint96,uint96,uint32[])" \
  0xA9... 25000000 5000000 "[1,3]" \
  --rpc-url $RPC --private-key $PK

# agent: pay from the account (its own key signs, the account's deposit pays)
cast send $AMD "payFrom(address,uint32,bytes32)" $ACCOUNT 1 $HASH \
  --rpc-url $RPC --private-key $AGENT_PK

# reads
cast call $AMD "balanceOf(address)(uint96)" $ACCOUNT --rpc-url $RPC
cast call $AMD "remainingToday(address,address)(uint96)" $ACCOUNT 0xA9... \
  --rpc-url $RPC
cast call $AMD "agentsOf(address)(address[])" $ACCOUNT --rpc-url $RPC
# take it back
cast send $AMD "revokeAgent(address)" 0xA9... --rpc-url $RPC --private-key $PK
cast send $AMD "withdraw(uint96)" 100000000 --rpc-url $RPC --private-key $PK

An empty allowedServices list means any service. A payment over the per-transaction limit, over what is left of the day, to a service outside the list, or beyond the deposit reverts, so an agent can never spend past what you allowed.

MCP tools

The MCP server at https://amd.mobi/api/mcp speaks Streamable HTTP (JSON-RPC 2.0 over POST; GET answers 405). Reads need no auth. Writes never sign: every prepare_* tool returns { to, data, value: "0x0", chainId: 4663, description } for the agent's own wallet, and send_raw_transaction relays a signed transaction.

curl -s https://amd.mobi/api/mcp -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-06-18","capabilities":{},
                 "clientInfo":{"name":"curl","version":"1"}}}'
curl -s https://amd.mobi/api/mcp -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
curl -s https://amd.mobi/api/mcp -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
       "params":{"name":"prepare_pay",
                 "arguments":{"endpointId":1,"method":"GET","path":"/v1/scrape",
                              "query":"url=https://example.com"}}}'
toolpurpose
list_services, get_service, list_endpoints, get_quotediscover services (by id or slug), endpoints and prices
get_stats, list_transactions, list_customersmerchant reporting over 7d, 30d or 90d
get_balance, get_controlsaccount deposit and an agent's controls
prepare_create_merchant, prepare_create_service, prepare_add_endpoint, prepare_publishsell an API; the service tool encrypts the secret with the gateway key
prepare_pay, prepare_pay_fromx402 payments, with the requestHash used
prepare_deposit, prepare_set_controlsfund an account and set an agent's limits
prepare_open_session, sign_voucher_payloadprepaid sessions and the EIP-712 voucher typed data
prepare_subscribe, prepare_chargesubscriptions
list_stocks, get_swap_quote, prepare_swaptrading (next section)
send_raw_transaction, gateway_keyrelay a signed tx; the public key that encrypts upstream secrets

Connect it in Claude Code with claude mcp add --transport http amd https://amd.mobi/api/mcp; the agent skill has the lines for OpenClaw and Codex.

Trading tools

Tokenized stocks on Robinhood Chain are ERC-20s named <TICKER> • Robinhood Token (18 decimals) with Uniswap v3 pools against USDG. Match a stock by that name, never by symbol alone. There is no quoter contract on this chain, so quotes come from the pool's slot0 and liquidity with single-tick maths, and report the real pool balances so a large order shows its impact.

# the verified list of Robinhood Tokens with their pools
curl -s https://amd.mobi/api/mcp -H "content-type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"list_stocks","arguments":{}}}' -o stocks.json
# a tool answers JSON inside result.content[0].text; tokens go by address
export NVDA=$(jq -r '.result.content[0].text | fromjson | .stocks[]
  | select(.symbol == "NVDA") | .address' stocks.json)

# quote 100 USDG into NVDA (amounts are decimal strings, not base units)
export Q=$(curl -s https://amd.mobi/api/mcp -H "content-type: application/json" \
  -d "$(jq -nc --arg i "$USDG" --arg o "$NVDA" '{jsonrpc:"2.0",id:2,
    method:"tools/call",params:{name:"get_swap_quote",
    arguments:{tokenIn:$i,tokenOut:$o,amountIn:"100"}}}')" \
  | jq -r '.result.content[0].text')
printf '%s' "$Q" | jq '{amountOut, priceImpact, fee: .pool.fee}'

# the swap calldata (approve + exactInputSingle on SwapRouter02),
# with a minOut 1% under the quote
export MIN=$(printf '%s' "$Q" | jq -r '.amountOut | tonumber * 0.99 | tostring')
curl -s https://amd.mobi/api/mcp -H "content-type: application/json" \
  -d "$(jq -nc --arg i "$USDG" --arg o "$NVDA" --arg m "$MIN" --arg r "$ME" \
    '{jsonrpc:"2.0",id:3,method:"tools/call",params:{name:"prepare_swap",
    arguments:{tokenIn:$i,tokenOut:$o,amountIn:"100",minOut:$m,recipient:$r}}}')"

Spot only, long only, no leverage: a swap is USDG into a token or a token back into USDG, held in the wallet that signed. The dashboard terminal does the same for a human: pick a token, quote, execute with the connected wallet, and see positions as token balances. A strategy on the trading page compiles to an agent brief (a prompt plus these tools) that you run in your own agent. AMD takes no fee on swaps; the pool fee and gas apply.

Errors

wherestatusmeaning
gateway402no X-PAYMENT, or one that failed verification: the error field says which check (receipt not found, status 0, wrong endpoint, hash mismatch, amount too low, receipt older than 600 s, receipt already used, voucher invalid)
gateway404unknown slug, unpublished or deleted service, or no endpoint for that method and path
gateway405the path exists with other methods
gateway502the upstream did not answer; nothing is charged twice, retry with the same receipt inside its window
/api/key503gateway key not configured on this deployment; services cannot store secrets yet
/api/rpc403a method outside the read-only allow list (writes go through your wallet)
contractrevertcustom errors, never strings: service not published or deleted, endpoint disabled, over the per-transaction limit or daily budget, service not allowed for the agent, insufficient deposit or allowance, voucher not signed by the session owner, cumulative not monotonic or above the deposit, subscription not due, fee above the maximum, caller lacks the role

Decode a revert with cast run <txHash> --rpc-url $RPC or cast 4byte-decode on the returned data. Every error a contract read can produce is also visible before you send: call quote, remainingToday and isDue first.