@obolo/sdk
@obolo/sdk
SDK for discovering and paying for agent-payable APIs on Obolo. Install it, connect a wallet, and your agent can autonomously discover, pay for, and consume any API on Obolo.
Installation
npm install @obolo/sdk viem
Quickstart
import { Obolo } from '@obolo/sdk';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';
// Create a wallet from a private key
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({
account,
chain: base,
transport: http(),
});
// Initialize the Obolo client
const gf = new Obolo({
walletClient,
maxPriceUsdc: 0.10, // Safety cap: never pay more than $0.10 per call
});
// Discover available APIs
const endpoints = await gf.discover('crypto prices');
console.log(endpoints.map(e => `${e.name}: $${e.price_usdc}/call`));
// Call an API — payment handled automatically
const response = await gf.call('provider/crypto-prices', {
params: { ids: 'bitcoin', vs_currencies: 'usd' },
});
console.log(response.data); // { bitcoin: { usd: 65000 } }
console.log(response.payment.txHash); // 0x...
console.log(response.payment.amountUsdc); // 0.01
console.log(response.timing.totalMs); // 3421
API Reference
new Obolo(config)
| Option | Type | Default | Description |
|---|---|---|---|
walletClient |
WalletClient |
required | viem WalletClient with an account attached |
proxyBaseUrl |
string |
https://proxy.obolo.io |
Override proxy base URL |
catalogUrl |
string |
https://www.obolo.io/api/marketplace/catalog |
Override catalog URL |
autoApprove |
boolean |
true |
Auto-approve exact USDC spend before each payment |
maxPriceUsdc |
number |
1.00 |
Reject endpoints priced above this (safety cap) |
gf.discover(query?)
Returns all active endpoints from the catalog, optionally filtered by query (name, description, category, or provider).
gf.call(slug, options?)
Pays for and calls an endpoint. Returns { data, payment, timing, status, rawBody }.
Options: method, params, body, headers.
gf.getPrice(slug)
Returns price info without paying.
gf.getReputation(slug)
Returns provider reputation and trust score (0–1).
gf.getBalance()
Returns the wallet's USDC balance on Base as a formatted string.
Error Handling
import {
PriceExceededError,
InsufficientBalanceError,
PaymentFailedError,
EndpointNotFoundError,
} from '@obolo/sdk';
try {
const response = await gf.call('provider/expensive-api');
} catch (err) {
if (err instanceof PriceExceededError) {
console.error(`Too expensive: ${err.price} USDC (cap: ${err.maxPrice})`);
} else if (err instanceof InsufficientBalanceError) {
console.error(`Need ${err.required} USDC, have ${err.available}`);
} else if (err instanceof PaymentFailedError) {
console.error('Transaction rejected:', err.message);
}
}
Payment rails — one call, any chain
gf.call() is EVM-only. Payment rails give every non-EVM chain the same one-liner: payAndReplay()
probes the endpoint's 402, picks the option for your chain, enforces a price cap, sends the transfer,
signs the chain's wallet-proof, and replays the request — retrying automatically while the proxy's node
catches up to your tx.
import { payAndReplay, tonRail, solanaRail, aptosRail, stellarRail, evmRail } from '@obolo/sdk';
// One rail per chain — pick the one that matches the wallet you hold. Each is an optional install:
const rail = tonRail({ mnemonic: process.env.TON_MNEMONIC!.split(' ') }); // npm i @ton/ton @ton/crypto
const { data, payment } = await payAndReplay({
rail,
slug: 'acme/weather',
method: 'GET',
maxPriceUsd: 0.10, // enforced BEFORE any funds move
});
console.log(payment.txHash, data);
Every rail satisfies the same PaymentRail interface, so payAndReplay is identical across chains — only
the rail factory differs:
| Chain | Factory | Config | Optional install |
|---|---|---|---|
| Solana | solanaRail |
{ secretKey } (64-byte) |
@solana/web3.js @solana/spl-token tweetnacl |
| Aptos | aptosRail |
{ privateKeyHex } |
@aptos-labs/ts-sdk |
| Stellar | stellarRail |
{ secret } (S…) |
@stellar/stellar-sdk |
| EVM | evmRail |
{ chainId, walletClient, publicClient } |
viem |
| TON † | tonRail |
{ mnemonic, version?, workchain? } |
@ton/ton @ton/crypto |
† TON is the newest rail. The
tonRailcode and the proxy's TON verifier are live on the production proxy, but TON has not yet been exercised with a live mainnet payment — do a testnet dry-run first (see below). Solana, Aptos, Stellar, and EVM rails have been settling against the live proxy in production.
The chain SDKs are optional peer dependencies — the core SDK is ~46 KB and pulls in nothing unless you
build a rail that needs it. Each rail sends the exact recipient/amount/token from the proxy's authoritative
402 (never a hardcoded mint), and signs the wallet-proof with the same key that paid, so the proxy's
fail-closed payer-binding always matches. Bring your own rail by implementing PaymentRail (chainId,
pay(option), signProof(...)) if you'd rather send funds your own way.
Before enabling a rail against mainnet funds, do a testnet dry-run. The transfer construction is grounded against each chain's official SDK + the proxy verifier and adversarially reviewed, but on-chain submission is the one thing unit tests can't exercise.
Solana payments (low-level)
If you'd rather settle the payment yourself, gf.call() handles the EVM x402 path end-to-end — it
settles the USDC payment and attaches the EIP-191 wallet-proof for you. It is EVM-only by design. Solana wallets sign with ed25519, not
EIP-191, so they use a separate, domain-separated proof: build the headers with
signSolanaWalletProof and attach them to your own request.
import { signSolanaWalletProof } from '@obolo/sdk';
// A SolanaSignerLike: your base58 pubkey + a raw-message signMessage returning the
// raw 64-byte signature. Satisfied by an @solana/web3.js Keypair wrapper or a
// wallet-adapter signMessage. Your secret key never leaves your process.
const signer = {
publicKey: myBase58Address,
signMessage: (msg: Uint8Array) => nacl.sign.detached(msg, secretKey),
};
// After settling the SPL payment on-chain, sign a proof bound to that exact tx.
const headers = await signSolanaWalletProof(signer, {
method: 'GET',
pathname: '/v1/provider/crypto-prices', // the URL path you send to the proxy
paymentHash: base58TxSignature, // the X-Payment-Hash value for Solana
});
const res = await fetch('https://proxy.obolo.io/v1/provider/crypto-prices', {
headers: {
...headers, // X-Solana-Wallet, X-Solana-Signature, X-Solana-Proof-Issued
'X-Payment-Hash': base58TxSignature,
'X-Payment-Chain': 'solana-mainnet',
},
});
- The challenge domain string is
GateFlow-Solana-Wallet-Proof-v1(distinct from the EVMGateFlow-Wallet-Proof-v1), and the base58 wallet is case-preserved — never lowercased. - ed25519 has no public-key recovery, so you send your public key and the proxy verifies the signature against it, then binds it to the on-chain SPL payer. This is fail-closed: a Solana payment with no valid proof, or a payer mismatch, is rejected. The proof is valid for 5 minutes.
Stellar payments
Stellar also signs with ed25519, so it uses its own domain-separated proof
(GateFlow-Stellar-Wallet-Proof-v1). After settling the native-Circle USDC payment on
Horizon, build the headers with signStellarWalletProof and attach them to your own request.
import { signStellarWalletProof } from '@obolo/sdk';
// A StellarSignerLike: your 'G…' strkey account + a raw-message signMessage returning
// the raw 64-byte ed25519 signature. Your secret key never leaves your process.
const signer = {
address: myStellarGAddress, // the 'G…' strkey account
signMessage: (msg: Uint8Array) => stellarKeypair.sign(msg),
};
// After settling the classic-asset USDC payment, sign a proof bound to that exact tx.
const headers = await signStellarWalletProof(signer, {
method: 'GET',
pathname: '/v1/provider/crypto-prices', // the URL path you send to the proxy
paymentHash: stellarTxHash, // the X-Payment-Hash value (Stellar tx hash, 64-hex)
});
const res = await fetch('https://proxy.obolo.io/v1/provider/crypto-prices', {
headers: {
...headers, // X-Stellar-Wallet, X-Stellar-Signature, X-Stellar-Proof-Issued
'X-Payment-Hash': stellarTxHash,
'X-Payment-Chain': 'stellar-mainnet',
},
});
- The 'G…' strkey encodes the ed25519 pubkey, so you send your address and the proxy decodes it to verify the signature. The strkey is base32 and case-significant — never lowercase it. The signature is 0x-hex (unlike Solana's base58).
- Fail-closed: the proxy binds the proven wallet to the on-chain Stellar payer, so a payment with no valid proof, or a payer mismatch, is rejected. The proof is valid for 5 minutes.
Aptos payments
Aptos likewise signs with ed25519 and uses its own domain-separated proof
(GateFlow-Aptos-Wallet-Proof-v1). Aptos USDC is a native fungible asset. The Aptos twist: the
account address is not the pubkey (it is sha3_256(pubkey ‖ 0x00)), so you send your ed25519
public key and the proxy verifies against it and derives the address.
import { signAptosWalletProof } from '@obolo/sdk';
// An AptosSignerLike: your ed25519 PUBLIC KEY (0x-hex, 32 bytes — NOT the account
// address) + a raw-message signMessage returning the raw 64-byte signature.
const signer = {
publicKey: myAptosPublicKeyHex, // 0x-hex ed25519 pubkey, not the address
signMessage: (msg: Uint8Array) => aptosAccount.sign(msg),
};
// After settling the fungible-asset USDC payment, sign a proof bound to that exact tx.
const headers = await signAptosWalletProof(signer, {
method: 'GET',
pathname: '/v1/provider/crypto-prices',
paymentHash: aptosTxHash, // the X-Payment-Hash value (Aptos tx hash, 0x-hex)
});
const res = await fetch('https://proxy.obolo.io/v1/provider/crypto-prices', {
headers: {
...headers, // X-Aptos-Wallet (the pubkey), X-Aptos-Signature, X-Aptos-Proof-Issued
'X-Payment-Hash': aptosTxHash,
'X-Payment-Chain': 'aptos-mainnet',
},
});
X-Aptos-Walletcarries the ed25519 public key (0x-hex, lowercased in the challenge), and the signature is 0x-hex. The proxy derives the account address from the pubkey and binds it.- Fail-closed: an Aptos payment with no valid proof, or a payer mismatch, is rejected. The proof is valid for 5 minutes.
License
MIT