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 90% of each call price; Obol's network fee is 10% (deducted, not added).
# 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 agent
Agents need a self-custody EOA (not a UCW) because they sign payments autonomously without a PIN.
# 1. Generate a new EOA private key (keep this secret):
node -e "const {Wallet} = require('ethers'); const w = Wallet.createRandom(); console.log('address:', w.address, '\nkey:', w.privateKey)"
# 2. Fund it with testnet USDC:
# Go to faucet.circle.com → select Arc testnet → paste the address
# 3. Set your agent key in your environment:
export OBOL_AGENT_KEY=0xYourPrivateKeySDK usage
npm install @circle-fin/x402-batching
import { GatewayClient } from "@circle-fin/x402-batching/client";
const buyer = new GatewayClient({
chain: "arcTestnet",
privateKey: process.env.OBOL_AGENT_KEY,
});
// One-time: deposit USDC into Gateway (from your Arc wallet)
await buyer.deposit("10"); // deposit 10 USDC
// Check your Gateway balance
const balances = await buyer.getBalances();
console.log("Available:", balances.available, "USDC");
// Pay and call any Obol-powered endpoint
const { data, transaction } = await buyer.pay(
"https://your-provider.railway.app/enrich?domain=acme.com"
);
console.log("Response:", data);
console.log("Paid in tx:", transaction);
// The buyer.pay() call handles the full 402 → pay → retry flow.
// No manual steps. If the server returns 402, the SDK pays and retries.Discover services from the marketplace
// List all services in the Obol marketplace
const res = await fetch("https://obol-arc.web.app/api/services");
const { services } = await res.json();
for (const svc of services) {
console.log(svc.name, "→", svc.hostedUrl, "@ $" + svc.priceUsdc + "/call");
}
// Then pay and call the one you want
const { data } = await buyer.pay(`${svc.hostedUrl}/enrich?domain=stripe.com`);MCP configuration
Add Obol to any MCP-compatible AI agent (Claude, Cursor, etc.) so it can find and pay for services automatically.
// Add to your claude_desktop_config.json or mcp settings:
{
"mcpServers": {
"obol": {
"command": "node",
"args": ["packages/obol-mcp/dist/index.js"],
"env": {
"OBOL_AGENT_KEY": "0xYourAgentPrivateKey"
}
}
}
}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
| Network | Chain key | USDC address | CCTP domain |
|---|---|---|---|
| Arc Testnet | arc | 0x3600000000000000000000000000000000000000 | 26 |
| Base Sepolia | base | 0x036CbD53842c5426634e7929541eC2318f3dCF7e | 6 |
| Ethereum Sepolia | ethereum | 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238 | 0 |
| Avalanche Fuji | avalanche | 0x5425890298aed601595a70AB815c96711a31Bc65 | 1 |
| OP Sepolia | optimism | 0x5fd84259d66Cd46123540766Be93DFE6D43130D | 2 |
| Arbitrum Sepolia | arbitrum | 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d | 3 |
| Polygon Amoy | polygon | 0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582 | 7 |
| Unichain Sepolia | unichain | 0x31d0220469e10c4E71834a79b1f276d740d3768F | 10 |
Pricing & fees
| Fee | Amount | Who pays | Where it goes |
|---|---|---|---|
| Obol network fee | 10% of call price | Seller (deducted from earnings) | Obol treasury |
| Circle Gateway fee | 0% (testnet) | — | — |
| Gas | $0 (gasless) | — | Circle covers on Arc |
| CCTP bridge fee | ~$0.001 | Initiator of the bridge | Circle/CCTP |
Example: You set your price at $0.002/call. The agent pays $0.002. You receive $0.0018 (90%). Obol keeps $0.0002 (10%). The agent pays no extra fees on top of $0.002.
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 |