Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Bankr Skills equip builders with plug-and-play tools to build more powerful agen
| yoink | [yoink](yoink/) | Social on-chain game. "Yoink" a token from the current holder. Uses Bankr for transaction execution. |
| [Neynar](https://neynar.com) | [neynar](neynar/) | Full Farcaster API integration. Post casts, like, recast, follow users, search content, and manage Farcaster identities. |
| [Nookplot](https://nookplot.com) | [nookplot](nookplot/) | Decentralized agent coordination on Base. On-chain identity, messaging, bounties, marketplace escrow, knowledge mining, reputation, guilds, and 410 MCP tools via gasless meta-transactions. |
| [Obol](https://obol.org/stack) | [obol](obol/) | Buy services from Obol Agents such as RPC queries, specialised smart contract indices, work from agents with proprietary data and skills, and more. |
| [Quicknode](https://www.quicknode.com) | [quicknode](quicknode/) | Blockchain RPC and data access for all supported chains. Native/token balances, gas estimation, transaction status, and onchain queries for Base, Ethereum, Polygon, Solana, and Unichain. Supports API key and x402 pay-per-request access. |
| [Hydrex](https://hydrex.fi) | [hydrex](hydrex/) | Liquidity pools on Base. Lock HYDX for voting power, vote on pool strategies, deposit single-sided liquidity into auto-managed vaults, and claim oHYDX rewards. |
| [Helixa](https://helixa.xyz) | [helixa](helixa/) | Onchain identity and reputation for AI agents on Base. Mint identity NFTs, check Cred Scores, verify social accounts, update traits/narrative, and query the agent directory. Supports SIWA auth and x402 micropayments. |
Expand Down
121 changes: 121 additions & 0 deletions obol/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
name: obol
description: Pay for and call x402-monetized APIs sold by Obol Stack agents. Use when the user wants to call an HTTPS endpoint that returns `402 Payment Required` (typically `https://<host>/services/<name>/*` on an Obol Stack trycloudflare tunnel) and pay in USDC or $OBOL via the Bankr Wallet API. Handles x402 v1 + v2, signs EIP-3009 `TransferWithAuthorization` for USDC and EIP-2612 `Permit` for $OBOL on mainnet (gasless via the Obol facilitator at `x402.gcp.obol.tech`). Reads asset decimals so a 1 OBOL price isn't misread as a trillion-dollar USDC ask. Buy-side only.
metadata:
{
"clawdbot":
{
"emoji": "♾️",
"homepage": "https://obol.org/stack",
"requires": { "bins": ["bun"] },
},
}
---

# Obol (x402 buy-side)

Call paid APIs hosted by Obol Stack agents. An Obol Stack seller exposes services behind `/services/<name>/*` on a public tunnel; hitting one of those URLs without payment returns HTTP 402 with a JSON payment challenge. This skill signs the challenge using the Bankr Wallet API and retries the request — the agent gets the response, the seller gets paid.

**Buy-side only.** Don't use this for installing the Obol Stack, running validators, or selling your own x402 endpoints.

Scripts are TypeScript on `bun` — no extra deps. `bun` is already in the Bankr sandbox.

## When to use

- The user wants to call an x402-protected URL (any HTTPS endpoint returning 402).
- The user wants to pay in USDC on Base or mainnet, or in $OBOL on mainnet (gasless via the Obol facilitator).
- The user wants to discover what an Obol Stack host is selling (fetch `<host>/skill.md`).

## Setup

Relies on the Bankr skill's config at `~/.clawdbot/skills/bankr/config.json`. The API key must have **Wallet API** access enabled and not be read-only — signing typed data is blocked otherwise.

```bash
test -f ~/.clawdbot/skills/bankr/config.json && echo OK || echo "set up the bankr skill first"
```

The buyer EVM address is fetched from `GET /wallet/me` automatically. Pass `--from 0x...` if the agent has multiple addresses and you need a specific one.

## Quick start

```bash
# List what an Obol Stack host is selling
bun ~/.clawdbot/skills/obol/scripts/obol-skill-list.ts https://example.trycloudflare.com

# Probe a single service — show price/network/asset, do NOT pay
bun ~/.clawdbot/skills/obol/scripts/obol-x402-call.ts --probe \
https://example.trycloudflare.com/services/hello

# Pay and call
bun ~/.clawdbot/skills/obol/scripts/obol-x402-call.ts \
https://example.trycloudflare.com/services/hello

# POST with a JSON body
bun ~/.clawdbot/skills/obol/scripts/obol-x402-call.ts -X POST -d '{"prompt":"hi"}' \
https://example.trycloudflare.com/services/quant

# Cap by base units (the script has no USD oracle — convert yourself)
bun ~/.clawdbot/skills/obol/scripts/obol-x402-call.ts --max-amount 1000000 \
https://example.trycloudflare.com/services/quant
```

The script prints the parsed challenge (price + network + asset + signing path) on stderr before signing. Paid response body goes to stdout. `-v` prints intermediate state.

**Always run `--probe` first on a URL you haven't called before** — the seller can post any price, and `--probe` shows it without committing.

## How the flow works

1. Unpaid request → server returns 402 with `accepts[]` (price, network, asset, payTo, scheme hints) and optional `extensions`.
2. Script picks `accepts[0]`, looks up the asset's decimals (built-in registry or on-chain `decimals()`), and prints a human-readable price (`1 OBOL (= 1000000000000000000 base units)`).
3. Builds EIP-712 typed-data:
- **EIP-3009 `TransferWithAuthorization`** (default for `exact` scheme — used by USDC).
- **EIP-2612 `Permit`** when the seller signals gas-sponsored permits: `extensions.eip2612GasSponsoring` present, OR `extra.assetTransferMethod: "permit2"` on an EIP-2612-capable token, OR v1's `extra.permit: true`, OR `--force-permit`.
4. `POST /wallet/sign` (Bankr Wallet API) with `signatureType: "eth_signTypedData_v4"`.
5. Wraps signature + auth/permit fields in a base64 JSON envelope.
6. Re-requests with `PAYMENT-SIGNATURE` (v2) and/or `X-PAYMENT` (v1) header. 200 → prints body + decodes the `PAYMENT-RESPONSE` settlement receipt.

**Critical**: the `amount` in a 402 challenge is in the asset's base units, not USD. OBOL = 18 decimals; USDC/USDT = 6; DAI = 18. Assuming USDC decimals on an OBOL price overshoots by 10^12. The script reads `decimals()` and shows the formatted price so this can't happen via the script.

See [`references/x402-protocol.md`](references/x402-protocol.md) for full wire details (v1↔v2 renames, EIP-712 schemas, envelope examples).

## Payment rails

| Asset | Network | Signing path | Buyer needs gas? |
|-------|---------|--------------|------------------|
| USDC | base, base-sepolia, polygon[-amoy], arbitrum[-sepolia], avalanche[-fuji], optimism[-sepolia] | EIP-3009 | No — Coinbase facilitator |
| USDC | ethereum (mainnet) | EIP-3009 | No — Obol facilitator |
| **$OBOL** | ethereum (mainnet) | EIP-2612 Permit (gas-sponsored permit batching) | **No** — Obol facilitator |

The buyer doesn't pick the facilitator; the seller does, and the script picks the signing path from the challenge.

## Discovery: the seller's `/skill.md`

Every Obol Stack tunnel publishes a markdown catalogue at `<host>/skill.md` listing service names, prices, and URLs. Unauthenticated — only the `/services/<name>/*` URLs cost money.

```bash
bun ~/.clawdbot/skills/obol/scripts/obol-skill-list.ts https://example.trycloudflare.com
```

## Troubleshooting

- **Agent quoted a wildly wrong price (trillions, billions)**: it's reading raw base units without decimals. Always route through `obol-x402-call.ts --probe` — it formats with the correct decimals.
- **Paid request returned 402 again**: payment envelope rejected. Re-run `--probe` — if `payTo`/`asset`/`extra` changed, the seller rotated, just re-run. Otherwise the signature didn't verify (wrong `extra.name`/`extra.version`, wrong chainId, expired `validBefore`). Re-running generates a fresh signature.
- **`403` from `/wallet/sign`**: Bankr API key is read-only or missing Wallet API access. New key at [bankr.bot/api](https://bankr.bot/api).
- **`unknown x402 network`**: the script handles CAIP-2 (`eip155:<chainId>`) and a list of v1 plain names. Add unknown networks to `V1_NETWORK_TO_CHAIN` in the script if needed.
- **`scheme` is not `exact`**: out of scope. Show the user the full 402 body.
- **Token unknown — wrong decimals/symbol**: extend `KNOWN_TOKENS` in `scripts/obol-x402-call.ts`. The on-chain `decimals()` fallback works but doesn't know about EIP-2612 support; pass `--force-permit` if you know the token supports it natively.
- **`assetTransferMethod: "permit2"` but token isn't EIP-2612-capable**: script errors with a clear message. Pass `--force-permit` if you trust the seller's facilitator handles it.

## Security

- The `PAYMENT-SIGNATURE` / `X-PAYMENT` value is a signed authorisation for a specific amount, recipient, and expiry. Facilitators record nonces; replay is rejected.
- `validBefore` is `now + maxTimeoutSeconds` (typically 60s). Stale auths are rejected — re-run for a fresh signature.
- Never log the payment header to anywhere persistent.
- The Bankr API key is the sensitive bit. It lives in `~/.clawdbot/skills/bankr/config.json` (gitignored by Bankr). Never echo it.

## Resources

- Obol Stack docs: <https://docs.obol.org/obol-stack/>
- Obol mainnet x402 facilitator: <https://x402.gcp.obol.tech>
- x402 protocol: <https://www.x402.org/>
- Bankr Wallet API signing: <https://docs.bankr.bot/wallet-api/sign>
201 changes: 201 additions & 0 deletions obol/references/x402-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# x402 wire-level reference (buy-side)

Detail for debugging or extending the script. SKILL.md has the user-facing flow; this doc is the implementation reference.

## v1 vs v2 — what changed

| | v1 | v2 |
|---|---|---|
| Version field | `x402Version: 1` | `x402Version: 2` |
| Network identifier | plain string (`"ethereum"`, `"base"`) | CAIP-2 (`"eip155:1"`, `"eip155:8453"`) |
| Amount field | `maxAmountRequired` | `amount` |
| Resource field | string URL | `{url, description, mimeType?}` object |
| Transfer method hint | `extra.permit: true` (informal) | `extra.assetTransferMethod: "permit2" \| "transferWithAuthorization"` |
| Extensions | absent | top-level `extensions: {...}` object |
| Client → server header | `X-PAYMENT` | `PAYMENT-SIGNATURE` |
| Server → client receipt | `X-PAYMENT-RESPONSE` | `PAYMENT-RESPONSE` |
| Envelope must echo `accepted`? | no | yes — plus `extensions` |

The script handles both. On v2 retries it sends `PAYMENT-SIGNATURE` AND `X-PAYMENT` to cover sellers in transition.

## Real 402 challenge (Obol Stack seller, v2)

```json
{
"x402Version": 2,
"error": "Payment required for this resource",
"resource": {
"url": "http://example.trycloudflare.com/services/demo-hello",
"description": "Payment required for /services/demo-hello"
},
"accepts": [
{
"scheme": "exact",
"network": "eip155:1",
"asset": "0x0B010000b7624eb9B3DfBC279673C76E9D29D5F7",
"amount": "1000000000000000000",
"payTo": "0x09f4a31c591421062A8dba9FcE24F29C5e88419A",
"maxTimeoutSeconds": 60,
"extra": {
"assetTransferMethod": "permit2",
"name": "Obol Network",
"version": "1"
}
}
],
"extensions": {
"eip2612GasSponsoring": {}
}
}
```

Field meanings:

| Field | Notes |
|-------|-------|
| `amount` | uint256 string, **base units of the asset**. Look up decimals before showing to a human. |
| `asset` | ERC-20 contract address. Domain `verifyingContract` for the signature. |
| `payTo` | Transfer recipient. EIP-3009 `to` field. For EIP-2612, the actual spender is `extra.spender ?? payTo`. |
| `extra.name`, `extra.version` | EIP-712 domain `name`/`version`. Wrong values silently break signature recovery. Verify against the token's on-chain `name()` if signatures keep failing. |
| `extra.assetTransferMethod` | `"permit2"` → permit-style; `"transferWithAuthorization"` → EIP-3009. |
| `extensions.eip2612GasSponsoring` | Seller's facilitator gas-sponsors EIP-2612 permits (Obol mainnet facilitator). Triggers the permit signing path when the token supports it. |

## EIP-3009 `TransferWithAuthorization` (USDC default)

```ts
{
domain: {
name: extra.name, // "USD Coin" / "USDC"
version: extra.version, // "2"
chainId,
verifyingContract: asset, // token, NOT the facilitator
},
types: {
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
},
primaryType: "TransferWithAuthorization",
message: {
from: <buyer wallet>,
to: payTo,
value: amount,
validAfter: "0",
validBefore: String(now + maxTimeoutSeconds),
nonce: "0x" + 32 random bytes,
},
}
```

`nonce` is random per request (32 bytes). Facilitator records `(from, nonce)` against replay.

## EIP-2612 `Permit` (Obol facilitator path)

Triggered when any of: `extensions.eip2612GasSponsoring` present + token supports EIP-2612, `extra.assetTransferMethod === "permit2"` + token supports EIP-2612, v1's `extra.permit: true`, or `--force-permit`.

```ts
{
domain: {
name: extra.name, // "Obol Network"
version: extra.version, // "1"
chainId,
verifyingContract: asset,
},
types: {
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
},
primaryType: "Permit",
message: {
owner: <buyer wallet>,
spender: extra.spender ?? payTo,
value: amount,
nonce: <on-chain nonces(owner)>,
deadline: String(now + maxTimeoutSeconds),
},
}
```

The `nonce` is **per-token, per-owner, incremented on-chain on every successful permit**. Read fresh via `eth_call` to `nonces(address)` (selector `0x7ecebe00`). Never cache.

### Why not Uniswap Permit2 (the contract)

`extra.assetTransferMethod: "permit2"` is a generic label for "permit-style transfer." With `extensions.eip2612GasSponsoring`, the facilitator translates that into a native EIP-2612 call against the token if the token supports it. The script does NOT sign a Uniswap Permit2 message — that would require a one-time approval of the Permit2 contract, which the gasless EIP-2612 path bypasses.

## Signing via Bankr

```bash
curl -X POST https://api.bankr.bot/wallet/sign \
-H "X-API-Key: $BANKR_KEY" \
-H "Content-Type: application/json" \
-d '{ "signatureType": "eth_signTypedData_v4", "typedData": { ... } }'
```

Response: `{ "success": true, "signature": "0x...", "signer": "0x...", "signatureType": "eth_signTypedData_v4" }`.

`signer` must match the `from`/`owner` in the typed data. If they differ, pass `--from` to the script to pin a specific address.

## Payment envelopes

Base64-encode the JSON and send as `PAYMENT-SIGNATURE` (v2) or `X-PAYMENT` (v1).

### v2 — EIP-2612 permit (OBOL on mainnet via Obol facilitator)

```json
{
"x402Version": 2,
"scheme": "exact",
"network": "eip155:1",
"accepted": { /* the chosen accepts[] entry, verbatim */ },
"extensions": { "eip2612GasSponsoring": {} },
"payload": {
"signature": "0xabc...",
"permit": {
"owner": "0x...",
"spender": "0x...",
"value": "1000000000000000000",
"nonce": "42",
"deadline":"1735689600"
},
"payTo": "0x..."
}
}
```

### v1 — EIP-3009 (legacy)

```json
{
"x402Version": 1,
"scheme": "exact",
"network": "base",
"payload": {
"signature": "0xabc...",
"authorization": {
"from": "0x...", "to": "0x...", "value": "1000",
"validAfter": "0", "validBefore": "1735689600",
"nonce": "0xdead..."
}
}
}
```

## Adding a new token

Drop it in `KNOWN_TOKENS` in `scripts/obol-x402-call.ts`, keyed by `<chainId>:<address-lowercase>`:

```ts
"1:0x0b010000b7624eb9b3dfbc279673c76e9d29d5f7": { symbol: "OBOL", decimals: 18, supportsEip2612: true },
```

For unknown tokens the script falls back to an on-chain `decimals()` read (selector `0x313ce567`). It can't infer EIP-2612 support from chain reads — pass `--force-permit` if you know the token supports it.
39 changes: 39 additions & 0 deletions obol/scripts/obol-skill-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bun
// @ts-nocheck — runs under bun; IDE doesn't have @types/node loaded.
/**
* obol-skill-list.ts — fetch the `/skill.md` catalogue from an Obol Stack host.
*
* Every Obol Stack tunnel publishes a `/skill.md` at the tunnel root listing the services
* it sells. This route is unauthenticated and free — only the `/services/<name>/*` URLs
* below it cost money.
*
* Usage:
* bun obol-skill-list.ts HOST
*
* Examples:
* bun obol-skill-list.ts https://example.trycloudflare.com
* bun obol-skill-list.ts https://my-stack.example.com
*/

async function main() {
const host = process.argv[2];
if (!host) {
console.error("Usage: bun obol-skill-list.ts HOST");
process.exit(2);
}
const url = host.replace(/\/$/, "") + "/skill.md";
const r = await fetch(url);
const text = await r.text();
if (!r.ok) {
console.error(`GET ${url} → HTTP ${r.status}`);
if (text) console.error(text);
console.error("(no /skill.md catalogue here — host may not be an Obol Stack tunnel, or the route isn't published)");
process.exit(1);
}
process.stdout.write(text);
}

main().catch((e) => {
console.error(e instanceof Error ? e.message : String(e));
process.exit(1);
});
Loading