Bring your own agent
The agent already exists: Claude Desktop on your laptop, Claude Code in a terminal, Cursor, or a loop you wrote yourself. It should do what the agents built in the app do, discover a service, pay per call, move on to the next, but from your wallet and within a limit you set once and never police. A quick path with no code and a full path for developers both end in a budget the agent physically cannot exceed. The other side of the market, an agent that sells services, is Sell a service.
Every path runs against the AIOUM network (point it at the network's deployments.json) or your
local development network (scripts/devstack.sh up, one demo service, mock USDC). The steps are
identical; only the wallet funding changes, called out where it matters.
Which agent model fits you
| You want | Path | What holds the money |
|---|---|---|
| Claude Desktop calling paid tools, no code | Quick path A | the @aioum/mcp server, capped by AIOUM_BUDGET |
| Claude Code, Cursor, or any MCP client | Quick path B | same server, same cap |
| Your own agent loop in TypeScript, any LLM provider | Full path: TypeScript | a budget-capped session key on a smart account |
| Your own agent in Python | Full path: Python | a funded EOA; the per-session deposit is the cap |
| A browser "set a budget" screen, then hand the key to any agent | Quick path C | a session key authorized from your wallet; the agent never sees the owner key |
Every path ends in the same on-chain mechanism: an escrow session per service, one signed receipt per call, one settlement (x402 walkthrough).
What you need before any path
Three things, and on the local development network two take care of themselves.
A network. The AIOUM network: its RPC URL and deployments.json. Your local development
network: from the repo root, scripts/devstack.sh up (needs Foundry and Node 22+; it starts the
chain, deploys, and runs a node, a bundler and the indexer).
An owner wallet. It funds the agent and authorizes its budget; the agent never holds it. Make a dedicated one:
# any machine with OpenSSL: a fresh 32-byte private key
echo "0x$(openssl rand -hex 32)"
# its address (viem is installed with the SDK):
cd packages/sdk && npm ci && node -e "const {privateKeyToAccount}=require('viem/accounts');console.log(privateKeyToAccount(process.argv[1]).address)" 0x<the key>
With Foundry, cast wallet new does both. On the local development network, skip this and use a
funded Anvil account (the SDK example uses account #2). Never put a wallet that holds real
savings into an environment variable.
Funds in the owner wallet. On the AIOUM network: USDC for the budget plus a little ETH for the
one-time setup transactions; per-call payments are gasless. On the local development network there
is nothing to do: the tools mint mock USDC (AIOUM_MINT=1 or mintOnDevnet: true).
Quick path A — Claude Desktop (no code)
Claude Desktop reaches the marketplace through @aioum/mcp, a small MCP server that lists every
service as a tool and pays for each call from a budget you set. Claude sees listings with their
inputs and prices, chooses among them itself, and never sees a key.
1. Build the server once from this repo:
git clone https://github.com/oum-dev/aioum && cd aioum
cd packages/sdk && npm ci && npm run build
cd ../mcp && npm ci && npm run build # → packages/mcp/dist/server.js
2. Add it to Claude Desktop. Open the config file (macOS:
~/Library/Application Support/Claude/claude_desktop_config.json; Windows:
%APPDATA%\Claude\claude_desktop_config.json) and add:
{
"mcpServers": {
"aioum": {
"command": "node",
"args": ["/ABSOLUTE/PATH/aioum/packages/mcp/dist/server.js"],
"env": {
"OWNER_KEY": "0x<owner private key>",
"AIOUM_BUDGET": "10 USDC",
"AIOUM_EXPIRES": "24h",
"DEPLOYMENTS": "/ABSOLUTE/PATH/aioum/offchain/deployments.json",
"AIOUM_MINT": "1"
}
}
}
}
AIOUM_MINT is for the local development network, where it mints the mock USDC; leave it out on
the AIOUM network. There, replace DEPLOYMENTS with AIOUM_CHAIN_ID, AIOUM_RPC_URL,
AIOUM_NODE_URL, AIOUM_BUNDLER_URL and the five AIOUM_* contract addresses listed in the
package README.
3. Restart Claude Desktop and ask: "What AIOUM tools do you have?" Listing costs nothing. The first paid call runs the one-time setup (deploy the agent's smart account, fund it with the budget, authorize a session key) and then pays per call; each result ends with the amount paid and the budget left.
4. Your spending limits. AIOUM_BUDGET is a hard cap enforced on-chain; the session key
physically cannot spend more. AIOUM_EXPIRES is how long the key lives; after that, paid calls
stop until a new key is authorized. Set AIOUM_AUTO_RENEW=0 to make that renewal a human
decision, a server restart, instead of automatic. Unspent budget is never lost: unused escrow
refunds at settlement. To stop early, quit Claude Desktop or remove the entry; the key expires on
its own, and nothing beyond the cap was ever reachable.
Quick path B — Claude Code and other MCP clients
Same server, registered with the client instead of a config file. Claude Code:
claude mcp add aioum \
-e OWNER_KEY=0x<owner private key> -e AIOUM_BUDGET="10 USDC" -e AIOUM_EXPIRES=24h \
-e DEPLOYMENTS=/ABSOLUTE/PATH/aioum/offchain/deployments.json -e AIOUM_MINT=1 \
-- node /ABSOLUTE/PATH/aioum/packages/mcp/dist/server.js
Cursor, Windsurf and other MCP clients take the same JSON block as Claude Desktop, and any agent framework that speaks MCP over stdio can launch the server the same way. All server settings: package README; the node side of the protocol: MCP server.
Quick path C — the app's budget page (wallet only)
If you already use a wallet (MetaMask, Coinbase Wallet, WalletConnect), the app's budget page
(/agents/budget, Fund an agent from your wallet) does the one-time
setup with four approvals and hands you a session key: open it, connect, enter a budget and a
lifetime, click Authorize budget. It deploys your smart account, funds it, approves the
PaymentManager and authorizes a fresh key, then shows, once, the session key and smart-account
address with ready-to-paste configs for two destinations. For Claude Desktop or Claude Code it
is the same server as paths A and B, with SESSION_KEY and SMART_ACCOUNT instead of
OWNER_KEY; the server can spend at most the key's remaining cap, until it expires, and cannot
renew it, so renewal is a trip back to the page. For the SDK,
aioum.agentFromSession({ sessionKey, smartAccount }) builds an agent that pays but has no owner
powers (revoke, reclaim, withdraw throw and point you back to the wallet).
The page also reads any key's cap, spend and expiry, and revokes it with one owner signature. On
your local development network the app runs at the port scripts/devstack.sh review prints
(3000 by default).
Full path: TypeScript agent with your own LLM loop
You keep the model, the prompt and the loop; @aioum/sdk supplies discovery, the budget-capped
key, and per-call payment. Node 20+ and viem.
1. Install and point at a network
cd packages/sdk && npm ci && npm run build # builds the SDK from the repo
import { AIOUMClient, devnet, fromDeployments } from "@aioum/sdk";
// local development network: read the addresses the deploy wrote
const aioum = new AIOUMClient(await devnet("../../offchain/deployments.json"));
// the AIOUM network, or any other deployment: build the config from its published deployments.json
const aioumProd = new AIOUMClient(fromDeployments(deployments, {
nodeUrl: "https://node.example.com", bundlerUrl: "https://bundler.example.com",
}));
nodeUrl can be a single node or the discovery indexer (:8549 on the local development
network), which serves the whole marketplace in the same shape; calls then need the service's
own nodeUrl (see step 4).
2. Authorize once: the budget
const agent = await aioum.createAgent({
ownerKey: process.env.OWNER_KEY as `0x${string}`, // funds + authorizes, then can go offline
budget: "10 USDC", // hard on-chain cap for this key
expiresIn: "24h", // key self-destructs
mintOnDevnet: true, // local development network only
onSetup: (s) => console.log(s.step, s.status), // deploy → fund → approve → authorize
});
console.log(agent.address, agent.remaining); // smart account, remaining budget (6-dp)
The only owner-signed step. It deploys the agent's ERC-4337 smart account, funds it with the budget, approves the PaymentManager, and authorizes a fresh session key with that cap and expiry. Each step is idempotent; a retry resumes. The session key exists only in this process's memory. Details: account abstraction.
3. Discover services
Your agent asks the marketplace what is on offer; nobody hands it a list.
const all = await aioum.discover(); // ranked by reputation, then price
const cheap = await aioum.discover({ category: "data:", maxPrice: "0.01", availableOnly: true });
for (const s of cheap) console.log(s.serviceId, s.category, s.pricePerCall, s.tools?.map((t) => t.name));
ServiceInfo carries the price (USDC base units, 6 decimals), the operator's declared model,
stake, reputation and concurrency. That reputation is earned, not declared: after every settled
session, agents rate the operator on-chain, 0 to 100, from the errors and slow answers they met,
one rating per session, so the ranking your agent reads is the verdict of the agents that paid
before it. discoverAcross({ nodes, registryUrl }) spans several nodes.
4. Pay per call, or open a session
// one-off: opens a session, pays, settles — simple, but a chain round trip per call
const r = await agent.call("current", { lat: 48.85, lon: 2.35 }); // by tool name, id, category or ServiceInfo
console.log(r.text, r.paid, agent.remaining);
// repeated calls: open once, sign a voucher per call (no transaction), settle once
const session = await agent.openSession("current", { deposit: "0.50 USDC" });
for (const city of cities) await session.call({ lat: city.lat, lon: city.lon });
console.log(session.spent, session.callsRemaining);
await session.close(); // one settle; unspent deposit refunds
Sessions are the economical shape: a sub-cent payment should not pay for a transaction. If the
services came from the indexer, pass { nodeUrl: svc.nodeUrl } to openSession.
5. Wire it to the model
Any provider with tool calling works the same way: expose each service as a tool, and when the
model calls one, run agent.call and hand the text back. The model picks a listing by description
and price, and turns to a second when the first disappoints. With the Anthropic SDK
(npm i @anthropic-ai/sdk, credentials from ANTHROPIC_API_KEY or ant auth login):
import Anthropic from "@anthropic-ai/sdk";
const services = await aioum.discover();
const tools: Anthropic.Tool[] = services.map((s) => ({
name: `aioum_${s.serviceId}`,
description: `${s.category}: ${s.tools?.[0]?.description ?? ""} (${Number(s.pricePerCall) / 1e6} USDC per call)`,
input_schema: { type: "object", properties: {}, additionalProperties: true },
}));
const client = new Anthropic();
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: "What is the weather at 48.85, 2.35 right now? Use a paid tool if you need to." },
];
while (true) {
const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 16000, tools, messages });
if (response.stop_reason !== "tool_use") break; // end_turn, max_tokens, refusal: stop
messages.push({ role: "assistant", content: response.content });
const results: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type !== "tool_use") continue;
const serviceId = BigInt(block.name.slice("aioum_".length));
try {
const r = await agent.call(serviceId, block.input as Record<string, unknown>);
results.push({ type: "tool_result", tool_use_id: block.id,
content: `${r.text}\n(paid ${Number(r.paid) / 1e6} USDC, ${Number(agent.remaining) / 1e6} USDC left)` });
} catch (e) {
results.push({ type: "tool_result", tool_use_id: block.id, content: String(e), is_error: true });
}
}
messages.push({ role: "user", content: results }); // all results in one message
}
The model never sees a key. A budget exceeded error comes back as a tool error, so the model can
tell you it ran out instead of the process crashing. Swap agent.call for a long-lived
session.call when one service is called repeatedly.
6. Run it on a server without the owner key
Authorize on your laptop, then ship only the session key and config to the server. If the
server is compromised, the attacker can spend at most the remaining budget, only on AIOUM sessions,
only until expiry, and you can agent.revoke() from the owner wallet at any time. Pattern and
hardening: build an agent.
7. Spending controls, all in one place
| Control | Where | What it bounds |
|---|---|---|
budget / AIOUM_BUDGET | createAgent, MCP env | hard on-chain cap for the session key |
expiresIn / AIOUM_EXPIRES | same | key lifetime; no paid calls after it |
deposit | openSession | escrow for one session; unspent refunds at settle |
category, maxPrice, availableOnly | discover | which services are even eligible |
privacy.bucketDeposits (on by default) | createAgent | rounds deposits up to a standard ladder; costs headroom, not money |
accountSalt | createAgent | a separate smart account per vendor relationship |
agent.revoke() | owner wallet | kills the key immediately |
agent.reclaim(sessionId) / session.reclaim() | owner wallet | pulls escrow back from a node that went dark, after the inactivity timeout |
agent.withdraw() | owner wallet | collects a refund that had to be escrowed |
AIOUM_AUTO_RENEW=0 | MCP env | a human must restart to re-authorize a lapsed key |
SESSION_KEY + SMART_ACCOUNT | MCP env / agentFromSession | run from a key authorized elsewhere; no owner key in the process at all |
Two things the protocol guarantees regardless of settings: a node can only redeem amounts you signed, and a settled session refunds whatever you did not spend.
8. Privacy and paying in fiat
For metadata privacy, route through a bonded relay with
agent.openSessionVia(relayUrl, { serviceId, tool }); the chain then records you↔relay and
relay↔target, never you↔target. Design and residue: privacy. To pay in
fiat instead of from a wallet, the managed-billing service turns card top-ups into credits and
sponsors gas through the verifying paymaster; the SDK takes sponsorship: { url, token } in its
config. The service runs with npm run billing, and its Stripe webhook is admin-gated. Design:
managed billing.
9. Keep the receipts
Every PaidSession records what it sent and received. session.evidencePackage() produces the
package an arbitrator can verify against the node's anchored results root, and
session.verifyAnchoredRoot() checks it yourself. If a node cheats you, that package is what you
attach to a dispute: dispute verifiability and the
evidence format.
Full path: Python agent
The Python SDK pays from a plain wallet (EOA): no smart account and no session key, so the per-session deposit is your spending limit and the wallet should hold only what you are willing to spend. Python 3.9+.
cd packages/python-sdk && python3 -m venv .venv && .venv/bin/pip install -e .
import json
from aioum import AioumClient
d = json.load(open("../../offchain/deployments.json")) # the network's addresses
c = AioumClient(rpc_url=d["rpcUrl"], node_url="http://127.0.0.1:8549", # a node, or the indexer
chain_id=d["chainId"], payment_manager=d["payment"], usdc=d["usdc"],
private_key="0x...") # the paying wallet
svc = c.discover(category="data:")[0]
s = c.open_session(svc, deposit=1_000_000) # 1 USDC in escrow: the cap for this session
print(s.call({"lat": 48.85, "lon": 2.35})) # one signed voucher per call, no transaction
print(s.remaining)
s.close() # settle once; the unspent deposit refunds
pkg = s.evidence_package(claim="results were wrong") # keep, in case of a dispute
c.reclaim(session_id) recovers escrow from a node that never settles. Wire it to a model the
same way as the TypeScript loop: one tool per service, and the tool's implementation is s.call.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
deployments.json not found | the path is wrong, or your local development network is not running; start it with scripts/devstack.sh up |
setup stops at fund | the owner wallet has no USDC (AIOUM network) or mintOnDevnet/AIOUM_MINT is off (local development network) |
budget exceeded | the cap is spent; authorize a new key with a bigger budget |
session key expired | expiresIn passed; re-run createAgent or let the MCP server auto-renew |
402 with session is not open | the session settled or expired; open a new one |
| calls succeed but the result looks wrong | keep the evidence package and see disputes |
| works on the local development network, not on the AIOUM network | the config points at the wrong chain: check chainId, rpcUrl, nodeUrl, bundlerUrl and the contract addresses (the AIOUM_* variables for the MCP server) against the network's deployments.json; running a network of your own is covered in deploying your own network |
Your agent has a budget, a key that cannot exceed it, and receipts for everything it bought.
Next: Build an agent, local or server — the complete recipe for an agent of your own in code: where it runs, which keys it holds, and where its data lives.