Developer documentation
Obol is a marketplace where any API, scraper, bot, model, or data process can charge AI agents per call in USDC — with no billing system, no subscriptions, and no user accounts.
New here? Skip the reading — build your first service with AI in 2 minutes ↓
Don't write a line of code.
Let your AI build your service.
Obol is built for the agent economy — so building on Obol should use an agent too. Copy the Obol Skill below, paste it into Claude, ChatGPT, or Cursor, and it will scaffold a working paid service, wire in your wallet, and walk you through deploying it. Your Arc payout address is already baked in.
▸ Preview the skill prompt▾ Hide preview
You are helping me publish a paid service on Obol — a marketplace where AI agents
discover services and pay per HTTP call in USDC on the Arc blockchain (no accounts,
no API keys for the buyer, no invoices). I want you to build, run, and help me deploy
a working seller server, then tell me exactly how to register it.
# CONTEXT YOU NEED
- Network: Arc testnet (EVM, chain ID 5042002).
- Payment rail: Circle Gateway via the x402 standard (HTTP 402 "Payment Required").
- The seller (me) runs a normal Express server. Each paid route is wrapped with one
middleware call that sets a price. When an agent calls without payment, the
middleware returns 402 + payment details; the agent's SDK pays off-chain (EIP-3009,
gasless, ~200ms) and retries; the middleware verifies and runs my handler.
- My earnings settle to MY Arc wallet address: 0xYourArcAddress
- I keep 100% of each call price; Obol charges 0% commission per call (it monetizes via optional subscriptions, not a take rate).
# THE STACK (use these EXACT packages — do not substitute)
npm install express @circle-fin/x402-batching
Server skeleton (ESM, Node 18+):
import express from "express";
import { createGatewayMiddleware } from "@circle-fin/x402-batching/server";
const app = express();
app.use(express.json());
const SELLER = process.env.SELLER_ADDRESS; // my Arc wallet
const gateway = createGatewayMiddleware({
sellerAddress: SELLER,
networks: "eip155:5042002", // Arc testnet
facilitatorUrl: "https://gateway-api-testnet.circle.com",
});
// Each paid route: gateway.require("$<price>") sets the USDC price per call.
app.get("/price", gateway.require("$0.001"), async (req, res) => {
// ... my logic ...
res.json({ /* result */, paidBy: req.payment?.payer });
});
app.get("/health", (_req, res) => res.json({ ok: true })); // free, unmetered
app.listen(process.env.PORT || 4021);
# THE SERVICE I WANT TO BUILD (default — change if I tell you otherwise)
A **Crypto Price API**. It wraps CoinGecko's FREE public endpoint (no API key needed):
GET https://api.coingecko.com/api/v3/simple/price?ids=<coin>&vs_currencies=usd
Expose one paid route:
GET /price?coin=bitcoin → charge $0.001 USDC → return { coin, usd, ts }
Validate the coin param, handle CoinGecko errors gracefully, and always include
`paidBy: req.payment?.payer` in the response so I can see who called.
# WHAT TO DO, STEP BY STEP
1. Create the project: package.json (type: module), the server file, a .env with
SELLER_ADDRESS=0xYourArcAddress and PORT=4021, and a .gitignore that excludes .env.
2. Write the full server. Production-quality: input validation, try/catch around the
upstream fetch, clear JSON errors, a brief comment on each route's price.
3. Give me the exact commands to install deps and run it locally.
4. Tell me how to expose it publicly for testing in ONE command:
npx localtunnel --port 4021 (gives a https://<name>.loca.lt URL)
…and how to deploy permanently to Railway or Render (free tier): push to GitHub,
create the project, set env var SELLER_ADDRESS=0xYourArcAddress, get the public URL.
5. Tell me precisely what to enter when I register it at obol-arc.web.app/dashboard
→ Provide services → + New service:
- Service name, Category, Description
- Price per call (USDC), Payout address (0xYourArcAddress), Hosted URL (my public URL)
- Input params (e.g. "coin: string")
6. Finally, give me a one-line curl I can run to confirm the route returns 402 before
payment (proof the metering works): curl -i <my-url>/price?coin=bitcoin
Build everything now. Output complete files I can copy verbatim — no placeholders
except where I must paste my deployed URL. Be concise but complete.Quick start
Prefer the manual route? Here's every step by hand.
Get from zero to a live paid API endpoint in under 10 minutes.
Providers (Sellers)
A provider runs a server that does something useful and charges agents per HTTP call. You decide what runs behind the endpoint — Obol only sees the request and response.
How it works
You add Obol middleware to any Express route. When an agent calls that route without payment, your server returns HTTP 402 (Payment Required) with Circle Gateway payment details. The agent's SDK automatically pays and retries. Your server processes the request and the earnings land in your Arc wallet.
Agent calls your URL → No payment header → your server returns 402 + payment details → Agent's SDK pays Circle Gateway (~200ms, off-chain) → Agent retries with payment proof → Your middleware verifies payment → Your handler runs, returns data → Earnings settle to your Arc address
Wrap your API
Install the SDK and add middleware to any Express route.
# In your server project npm install @circle-fin/x402-batching express
Minimal example — single endpoint
import express from "express";
import { createGatewayMiddleware } from "@circle-fin/x402-batching/server";
const app = express();
// Replace with your UCW wallet address from the Obol dashboard
const SELLER = process.env.SELLER_ADDRESS;
const gateway = createGatewayMiddleware({
sellerAddress: SELLER,
networks: "eip155:5042002", // Arc testnet
facilitatorUrl: "https://gateway-api-testnet.circle.com",
});
// $0.002 USDC per call to this route
app.get("/enrich", gateway.require("$0.002"), (req, res) => {
const domain = req.query.domain || "example.com";
res.json({
domain,
company: "Acme Inc.",
// req.payment is populated after a successful payment
paidBy: req.payment?.payer,
txHash: req.payment?.transaction,
});
});
app.listen(4021);Per-endpoint pricing
Different routes can have different prices. Call gateway.require() independently on each route.
const gateway = createGatewayMiddleware({
sellerAddress: SELLER,
networks: "eip155:5042002",
facilitatorUrl: "https://gateway-api-testnet.circle.com",
});
// Cheap: basic lookup
app.get("/company/basic", gateway.require("$0.001"), (req, res) => {
res.json({ name: "Acme Inc.", country: "US" });
});
// Medium: full profile
app.get("/company/full", gateway.require("$0.005"), (req, res) => {
res.json({ name: "Acme Inc.", headcount: 420, revenue: "$12M", ... });
});
// Expensive: bulk batch (up to 50 domains per call)
app.post("/company/bulk", gateway.require("$0.050"), async (req, res) => {
const { domains } = req.body;
const results = await enrichAll(domains); // your own logic
res.json({ results, count: results.length });
});
// Free: health check (no middleware = no payment required)
app.get("/health", (_req, res) => res.json({ ok: true }));API keys & secrets
When you wrap a paid third-party API, your API key stays on your server. Buyers never see it. They call your URL and pay — your server uses its key to call the upstream service on their behalf.
# Your server's environment — buyers never see any of this SELLER_ADDRESS=0xYourArcWallet OPENAI_API_KEY=sk-... SOME_DATA_API_KEY=abc123 SCRAPER_PROXY_PASSWORD=xyz789
Real examples
Example 1 — Wrap OpenAI completions
import OpenAI from "openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// You pay OpenAI ~$0.002/call (GPT-4o-mini).
// You charge agents $0.004/call → $0.002 margin per call.
app.post("/complete", gateway.require("$0.004"), async (req, res) => {
const { prompt } = req.body;
const chat = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: prompt }],
});
res.json({
result: chat.choices[0].message.content,
paidBy: req.payment?.payer,
});
});Example 2 — Wrap a Puppeteer scraper
import puppeteer from "puppeteer";
// Agents pay $0.01 per page scrape. You pay server costs (~$0.001).
app.get("/scrape", gateway.require("$0.010"), async (req, res) => {
const { url } = req.query;
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url, { waitUntil: "networkidle2" });
const text = await page.evaluate(() => document.body.innerText);
await browser.close();
res.json({ url, text, charCount: text.length, paidBy: req.payment?.payer });
});Example 3 — Wrap a premium weather API (you hold the key)
// Agents call your endpoint and pay $0.001.
// Your server calls the premium API with your key.
// Agents never see your key.
app.get("/weather", gateway.require("$0.001"), async (req, res) => {
const { city } = req.query;
const r = await fetch(
`https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_KEY}&q=${city}`
);
const data = await r.json();
res.json({
city: data.location.name,
temp_c: data.current.temp_c,
condition: data.current.condition.text,
paidBy: req.payment?.payer,
});
});Deploying your server
Your server needs a permanent public HTTPS URL. Any Node.js host works. For testing you can use localtunnel (no install needed). For production deploy to Railway, Fly.io, or Google Cloud Run.
| Option | Cost | Setup time | Best for |
|---|---|---|---|
| localtunnel (npx) | Free | 10 seconds | Local testing only |
| Railway | Free tier | 5 minutes | Hackathons, demos |
| Fly.io | Free tier | 10 minutes | Persistent low-traffic |
| Google Cloud Run | Free tier | 15 minutes | Production |
| Render | Free tier | 5 minutes | Simple deploys |
Local testing with localtunnel
# Terminal 1 — run your server SELLER_ADDRESS=0xYourArcAddress node server.mjs # Terminal 2 — expose it publicly (no install needed) npx localtunnel --port 4021 # → your url is: https://happy-dogs-fly.loca.lt
Deploy to Railway (permanent URL)
# 1. Push your server code to a GitHub repo # 2. Go to railway.app → New Project → Deploy from GitHub # 3. Set environment variables in Railway dashboard: # SELLER_ADDRESS = 0xYourArcAddress # PORT = 4021 # (any API keys your server needs) # 4. Railway gives you a URL like: # https://your-project.railway.app # Use that URL when registering in the Obol dashboard.
Register in the dashboard
Go to Dashboard → Provide services → + New service and fill in each field:
| Field | What to enter | Example |
|---|---|---|
| Service name | Short name agents see in search | Company Enrichment API |
| Category | Pick the closest match | Data |
| Price per call | Default USDC price for one call | 0.002 |
| Payout address | Your Arc wallet — auto-filled if wallet created | 0xYourArcAddress |
| Hosted URL | Your server's public HTTPS URL | https://your-app.railway.app |
| Description | What your API does, what it returns | Returns company name, size, industry for any domain |
| Input params | Param names agents use to call it | domain: string |
| API docs URL | Link to your docs (optional) | https://your-docs.example.com |
Per-endpoint pricing rows (optional)
If your server has multiple routes at different prices, add one row per route. Leave empty to use the single price above for all routes.
| Path | Price per call | Description |
|---|---|---|
| /company/basic | 0.001 | Name and country only |
| /company/full | 0.005 | Full profile with headcount and revenue |
| /company/bulk | 0.050 | Batch of up to 50 domains |
Buyers (Agents)
A buyer is any AI agent or script that discovers services on Obol and pays per call automatically using Circle Gateway.
How it works
1. Agent deposits USDC into Circle Gateway (one time, any amount) 2. Agent calls a paid API endpoint 3. Server returns 402 + payment details (seller address, amount, network) 4. Agent's SDK signs an EIP-3009 off-chain authorization (~200ms, no gas) 5. Circle Gateway verifies and records the payment 6. Agent retries the request with the payment header 7. Server verifies → processes → returns data 8. Gateway settles to seller on Arc (batched, gasless)
Fund your account
Buying is keyless. All you need is an Obol account and API key — no private key, no wallet to manage. Obol's relayer pays sellers on your behalf and deducts from your funded balance.
# 1. Create an account and copy your API key from Settings on obol-arc.web.app # (looks like obl_sk_live_…) # 2. Deposit USDC into your Obol spending balance (Dashboard → Deposit, # or the MCP "deposit" tool). Fund from any chain via Circle Gateway. # 3. That's it — set your API key and start paying: export OBOL_API_KEY=obl_sk_live_your_key_here
MCP configuration
Add Obol to any MCP-compatible AI agent (Claude, Cursor, etc.) so it can find and pay for services automatically — with just your API key.
// Add to your claude_desktop_config.json or mcp settings:
{
"mcpServers": {
"obol": {
"command": "npx",
"args": ["-y", "@superbigroach/obol-mcp"],
"env": {
"OBOL_API_KEY": "obl_sk_live_…"
}
}
}
}Advanced: self-custody (optional)
Prefer to hold your own funds and sign payments locally instead of using the hosted relayer? Set OBOL_AGENT_KEY to a funded EOA private key and the MCP/SDK will sign directly. Most buyers do not need this.
# Generate an EOA key, fund it at faucet.circle.com (Arc testnet), then: export OBOL_AGENT_KEY=0xYourPrivateKey # replaces the keyless path
import { GatewayClient } from "@circle-fin/x402-batching/client";
const buyer = new GatewayClient({ chain: "arcTestnet", privateKey: process.env.OBOL_AGENT_KEY });
await buyer.deposit("10"); // one-time Gateway deposit
const { data, transaction } = await buyer.pay( // 402 → pay → retry, automatic
"https://your-provider.railway.app/enrich?domain=acme.com"
);MCP tools available to the agent once connected:
| Tool | What it does |
|---|---|
| find_service | Search the Obol marketplace by name or category |
| get_balance | Check your Gateway USDC balance |
| deposit | Deposit USDC from your wallet into Gateway |
| pay_and_call | Pay for and call any Obol service URL |
Dashboard guide
The dashboard at obol-arc.web.app/dashboard has two tabs: Use services (buyer) and Provide services (seller).
Wallet setup
Your Obol wallet is a self-custody EOA on Arc testnet powered by Circle User-Controlled Wallets (UCW). You hold the keys via a PIN — Obol never has custody.
To create:
- Go to Dashboard → Use services tab
- Click "Create my wallet →"
- Circle's PIN overlay appears — set a 6-digit PIN and three security questions
- Your Arc address appears and is saved to your profile automatically
- This address is your payout address for services you sell
Provide services tab
Everything a seller needs in one tab.
Use services tab
The buyer view — fund your agent and monitor spending.
Bridge & withdraw
Move USDC between chains and out of the platform.
Buyer dashboard — "Deposit to Gateway": Arc wallet → Circle Gateway unified balance Used to fund the agent's spending pool. Requires PIN confirmation (UCW). Buyer dashboard — "Bridge →" on a chain card: Any chain → Any other chain Uses Circle CCTP behind the scenes. ~30 seconds to arrive. Buyer dashboard — "Withdraw →" on Gateway balance card: Gateway balance → Any chain, any recipient address Pick destination network from dropdown (all 8 chains supported). Seller dashboard — "Withdraw earnings": Your Arc wallet → Any address on any network Same cross-chain capability via CCTP. Enter recipient address manually.
Reference
Supported networks
Your unified USDC balance lives on Arc (where per-call payments settle). You can deposit from and withdraw to any network below — Obol bridges via Circle Gateway (EVM chains) or CCTP (Monad, Solana), gaslessly. 10 networks total.
| Network | Chain key | USDC address / SPL mint | CCTP domain |
|---|---|---|---|
| Arc Testnet · settles here | arc | 0x3600000000000000000000000000000000000000 | 26 |
| Base Sepolia | base | 0x036CbD53842c5426634e7929541eC2318f3dCF7e | 6 |
| Ethereum Sepolia | ethereum | 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 | 0 |
| Avalanche Fuji | avalanche | 0x5425890298aed601595a70AB815c96711a31Bc65 | 1 |
| OP Sepolia | optimism | 0x5fd84259d66Cd46123540766Be93DFE6D43130D7 | 2 |
| Arbitrum Sepolia | arbitrum | 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d | 3 |
| Polygon Amoy | polygon | 0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582 | 7 |
| Unichain Sepolia | unichain | 0x31d0220469e10c4E71834a79b1f276d740d3768F | 10 |
| Monad Testnet | monad | 0x534b2f3A21130d7a60830c2Df862319e593943A3 | 15 |
| Solana Devnet | solana | 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU | 5 |
Pricing & fees
| Fee | Amount | Who pays | Where it goes |
|---|---|---|---|
| Obol commission | 0% per call | — | You keep 100% |
| Circle Gateway fee | 0% (testnet) | — | — |
| Gas (per call) | $0 (gasless nanopayments) | — | Circle covers settlement |
| Deposit gas (one-time) | ~$0.0014 on Arc | Depositor funding their balance | Network |
| CCTP bridge fee | ~$0.001 | Initiator of the bridge | Circle/CCTP |
Example: You set your price at $0.002/call. The agent pays exactly $0.002. You receive the full $0.002 (100%). Obol takes $0 per call — we monetize via optional Featured/Scale subscriptions, not your transactions.
To be clear: $0.000001 is the minimum payment (the smallest price a seller can charge) — not a gas fee. Gas is $0: buyers and sellers pay no gas at all. Nanopayments are offchain EIP-3009 signatures, batch-settled by Circle Gateway, so per Circle "neither party pays per-transaction fees".
Every pay-and-call receipt includes feeRate: "0%", gas: 0, and priceSource: "x402-endpoint" (the live per-endpoint x402 price was billed). You pay only the seller's price — Obol takes 0% commission.
HTTP 402 explained
HTTP 402 ("Payment Required") is the standard status code for paywalled content. Obol's middleware returns a 402 with a X-Payment-Required header containing Circle Gateway payment instructions. Compatible buyers (those using the GatewayClient SDK or the Obol MCP) handle this automatically.
# What an unpaid request looks like:
HTTP/1.1 402 Payment Required
X-Payment-Required: {
"scheme": "circle-gateway",
"network": "eip155:5042002",
"maxAmountRequired": "2000", // in USDC micro-units (6 decimals)
"sellerAddress": "0xSeller...",
"facilitatorUrl": "https://gateway-api-testnet.circle.com"
}
# The GatewayClient SDK reads this header, signs an off-chain EIP-3009
# authorization, submits to Circle Gateway, then retries the original
# request with a signed payment proof in the Authorization header.Wallet types
| Wallet type | Used for | How it works | Keys |
|---|---|---|---|
| UCW (User-Controlled) | Sellers, dashboard humans | PIN overlay via Circle SDK. Human approves each spend. | You hold via PIN |
| Agent EOA (raw private key) | Buyers, AI agents | Script signs autonomously with OBOL_AGENT_KEY. No PIN, no popup. | You hold private key |
| DCW (Developer-Controlled) | Legacy Obol platform wallet | Circle holds keys server-side. Used before UCW was added. | Circle holds |