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 wantPathWhat holds the money
Claude Desktop calling paid tools, no codeQuick path Athe @aioum/mcp server, capped by AIOUM_BUDGET
Claude Code, Cursor, or any MCP clientQuick path Bsame server, same cap
Your own agent loop in TypeScript, any LLM providerFull path: TypeScripta budget-capped session key on a smart account
Your own agent in PythonFull path: Pythona funded EOA; the per-session deposit is the cap
A browser "set a budget" screen, then hand the key to any agentQuick path Ca 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

ControlWhereWhat it bounds
budget / AIOUM_BUDGETcreateAgent, MCP envhard on-chain cap for the session key
expiresIn / AIOUM_EXPIRESsamekey lifetime; no paid calls after it
depositopenSessionescrow for one session; unspent refunds at settle
category, maxPrice, availableOnlydiscoverwhich services are even eligible
privacy.bucketDeposits (on by default)createAgentrounds deposits up to a standard ladder; costs headroom, not money
accountSaltcreateAgenta separate smart account per vendor relationship
agent.revoke()owner walletkills the key immediately
agent.reclaim(sessionId) / session.reclaim()owner walletpulls escrow back from a node that went dark, after the inactivity timeout
agent.withdraw()owner walletcollects a refund that had to be escrowed
AIOUM_AUTO_RENEW=0MCP enva human must restart to re-authorize a lapsed key
SESSION_KEY + SMART_ACCOUNTMCP env / agentFromSessionrun 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

SymptomCause and fix
deployments.json not foundthe path is wrong, or your local development network is not running; start it with scripts/devstack.sh up
setup stops at fundthe owner wallet has no USDC (AIOUM network) or mintOnDevnet/AIOUM_MINT is off (local development network)
budget exceededthe cap is spent; authorize a new key with a bigger budget
session key expiredexpiresIn passed; re-run createAgent or let the MCP server auto-renew
402 with session is not openthe session settled or expired; open a new one
calls succeed but the result looks wrongkeep the evidence package and see disputes
works on the local development network, not on the AIOUM networkthe 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.