Client SDK for the Vybe x402 API. Pay-per-call USDC over HTTP and prepaid-credit WebSocket streaming for Vybe's Solana analytics API. Built for AI agents.
x402-client is an established TypeScript project in the AI payments / x402 ecosystem, focused on ai-agents, micropayments, pay-per-call, sdk. It currently has 168 GitHub stars and 65 forks, and sits alongside related tools like piprail, APITOLL, Sentinel, pincerpay, surf, x402-toolkit.
x402-client is Vybe’s SDK for consuming its Solana analytics API on a pay-per-call basis: USDC over HTTP for individual requests, plus prepaid-credit WebSocket streaming for continuous data. It is a clean example of a real product — market data — being sold to agents per call rather than per subscription seat.
What makes it instructive beyond Vybe’s own API is the dual model: discrete HTTP 402 payments for one-off queries and prepaid credits for streams. Most agent-payment demos only cover the request/response case; handling streaming metering is where production data products actually get hard, and this SDK shows one working answer on Solana.
Best for: agents that need Solana market data, and builders studying how to meter both request and streaming access. A Solana pay-per-call client.
Client SDK for Vybe's Solana analytics API. Pay per call in USDC via the x402 protocol — no API keys, no subscriptions, just bring a Solana wallet.
Beta status
- The SDK is pre-release (0.1.x) — the public API may change before 1.0.
- The service is in beta. The x402 API endpoint, request/response shapes, error semantics, WS protocol, and per-route pricing can all change. Pin your dependency, watch the changelog.
- Beta runs on Solana mainnet. Payments settle in real mainnet USDC. Check
GET /on the API URL to confirm the live network at any time — that endpoint is authoritative.- Pricing is currently a beta discount. $0.001 default, $0.003 / $0.005 / $0.008 / $0.010 on heavier endpoints, $0.01 per WS session of 1000 credits. Hit
GET /on the API URL for the live price table. These rates will go up at general availability — they exist to make beta usage cheap for early integrators. If you'd rather a traditional flat-fee subscription instead of pay-per-call, see vybe.fyi/api-pricing.- Rate limits. 600 paid HTTP requests per minute per wallet (sliding window). High-traffic clients should batch where possible or run multiple wallets.
- You bring your own Solana RPC. The SDK signs payments client-side, which calls Solana RPC twice per request (fetch USDC mint metadata + recent blockhash). Without
rpcUrlset, it falls back to the public mainnet RPC, which rate-limits at ~5 RPS. Use a paid tier (Helius, Triton, QuickNode, etc.) — see Configuration reference below.
client.get() / client.request() — no special AI helpers, full Vybe surface.maxUsd and the SDK rejects calls before signing.npm install @vybenetwork/x402-client
You need a dedicated Solana keypair with mainnet USDC. The API covers Solana transaction fees on every transfer, so the wallet only needs USDC — no SOL required for gas.
Use a dedicated wallet. Don't point the SDK at a wallet you also use for trading, custody, or anything else. The keypair is held in process memory and signs USDC transfers per call (or per session); mixing API spend with other activity makes bookkeeping fragile and risks unintended charges if a balance check elsewhere assumes a USDC float that the SDK is quietly draining.
1. Get a keypair. Two paths — pick whichever is closer to where your USDC already lives:
solana-keygen new --no-bip39-passphrase --outfile client.json
node -e "console.log(Buffer.from(JSON.parse(require('fs').readFileSync('client.json'))).toString('base64'))"
solana-keygen pubkey client.json # the address to fund
loadKeypair auto-detects both formats — paste the base58 from option A directly, or the base64 from option B.
2. Fund it with USDC (mainnet, mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v). In order of how most people do it:
A few dollars goes a long way given default pricing of $0.001 per call.
3. Wire it up. Set the encoded keypair string as CLIENT_PRIVATE_KEY in your environment (or pass directly to loadKeypair). Treat it like any other secret — it's the full keypair, not just the pubkey. See .env.example for the full list of env vars the examples read.
import { VybeClient, loadKeypair, ApiError, NetworkError } from "@vybenetwork/x402-client";
const client = new VybeClient({
wallet: await loadKeypair(process.env.CLIENT_PRIVATE_KEY!),
rpcUrl: process.env.SOLANA_RPC_URL, // strongly recommended (see Beta status)
budget: { maxUsd: 1.00, onExceed: "reject" }, // optional safety cap
});
const BONK_MINT = "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263";
try {
// HTTP — auto-pays the $0.001 fee on first 402 challenge
const token = await client.get(`/v4/tokens/${BONK_MINT}`);
console.log(token);
} catch (err) {
if (err instanceof ApiError) console.error(`API ${err.status}: ${err.message}`);
else if (err instanceof NetworkError) console.error(`Transport: ${err.message}`);
else throw err;
}
The endpoint, network, USDC mint, and pay-to address are inferred from the API's discovery endpoint (GET /) — you only bring your wallet. See Errors for the full type hierarchy.
The SDK is a thin pay-on-success wrapper over the Vybe REST API. Paths, query strings, request bodies, and response shapes are all unchanged — anything documented for Vybe works through client.get() / client.request(). The only thing the SDK adds on top is the x402 payment dance on the way in and a typed receipt on the way back.
Different base URL. The SDK targets the pay-per-call x402 API at
https://x402-api.vybenetwork.xyz, not the subscription API athttps://api.vybenetwork.xyz. The SDK handles this for you — but if you're tailing logs, configuring egress rules, or sharing API calls with someone on the subscription side, you'll see the different host.
// Note: Vybe endpoints take SPL mint addresses, NOT ticker symbols.
// "BONK" is not valid input — use the actual mint address.
const BONK_MINT = "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263";
// GET — returns the parsed body on 2xx, throws a typed error on non-2xx
const holders = await client.get(`/v4/tokens/${BONK_MINT}/top-holders?limit=10`);
// Other methods — `request()` returns the parsed body plus payment receipt and
// status. It still throws the same typed errors as `get()` on non-2xx, so
// wrap in try/catch (or let it propagate) when error handling matters.
const { data, receipt, status } = await client.request("/v4/wallets/batch/token-balances", {
method: "POST",
headers: { "Content-Type": "application/json" },
// Vybe's batch endpoints take `ownerAddresses` (an array of wallet
// addresses, max 10), not `wallets`. See:
// https://docs.vybenetwork.com/reference/post_wallet_tokens_many_v4
body: JSON.stringify({ ownerAddresses: ["7EK976zyBWhYikjXGASSfN5KoNEekSLqx7wEkUJ8YkHv"] }),
});
console.log(`paid $${receipt?.amount} via ${receipt?.txHash}`);
Both get() and request() accept paths like /v4/... (joined with the configured API URL) or absolute URLs on the same origin. See the Vybe API Reference for each endpoint's required fields.
// Vybe's WS filter keys are `trades`, `transfers`, `oraclePrices`,
// `priceCandles`, and `newToken` (all confirmed live through the x402 API;
// `newMarket` and `ageBucket` are accepted by the protocol but
// emit no events at the moment). Each takes an array of filter objects
// with optional `tokenMintAddress`, `marketId`, or `programId`. Empty
// array = "all events of that type".
// See: https://docs.vybenetwork.com/docs/filter-configuration
const BONK_MINT = "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263";
const stream = await client.stream({
filters: { trades: [{ tokenMintAddress: BONK_MINT }] },
});
for await (const event of stream) {
console.log(event.data, "balance:", event.balance);
if (event.warning === "LOW_BALANCE") console.warn("topping up soon");
}
The SDK pays $0.01 to mint a session (1000 credits, ≈995 events after the 5-credit connection charge), opens the WS, and yields each event with credit metadata. Auto-topup fires when the balance drops below the threshold (default 50, set 0 or pass autoTopup: false to disable).
// Cancel via AbortSignal
const ctrl = new AbortController();
const stream = await client.stream({
filters: { trades: [] },
signal: ctrl.signal,
});
setTimeout(() => ctrl.abort(), 60_000);
// Or call .close() to end the stream
stream.close();
// Or break out of the for-await — the SDK closes the socket automatically
let received = 0;
for await (const ev of stream) {
if (++received >= 100) break;
}
If you're on an MCP-aware host (Claude Desktop, Cursor, Windsurf, VS Code with Copilot, etc.), you don't need this SDK at all — point the host at Vybe's MCP server and the model can call all 35+ Vybe endpoints natively, no tool-spec wiring required. The rest of this section is for building your own agent that pays per call.
The SDK has no AI-specific helpers — wiring a Vybe endpoint into a model's tool-calling loop is just two steps:
tool_call to client.get() (for GETs) or client.request() (for POSTs and others). Send the response back to the model as a tool message.const topHoldersTool = {
type: "function",
function: {
name: "get_token_top_holders",
description: "Top wallets holding a Solana SPL token, ranked by balance.",
parameters: {
type: "object",
properties: {
// SPL mint address — system-prompt the model to use mints, not
// ticker symbols. "USDC" / "BONK" will fail the API.
mintAddress: { type: "string" },
limit: { type: "integer" },
},
required: ["mintAddress"],
},
},
};
// ...inside your tool-call handler:
if (call.function.name === "get_token_top_holders") {
const { mintAddress, limit } = JSON.parse(call.function.arguments);
const data = await client.get(
`/v4/tokens/${encodeURIComponent(mintAddress)}/top-holders?limit=${limit ?? 10}`,
);
// push `data` back to the model as a tool message...
}
For POST endpoints (wallet batch, trading swap, etc.), use client.request(path, { method: "POST", headers, body }) — see the HTTP section above.
Budget cap for runaway models. Set budget: { maxUsd, onExceed: "reject" } on the client. client.get() / request() will throw BudgetExceededError once cumulative spend passes the cap; catch it and surface to the model as a tool error so it knows to stop.
A full runnable example is in examples/agent.ts.
loadKeypair accepts the three formats you actually have on hand:
| Format | Source | Example call |
|---|---|---|
| Base58 string | Most wallets' "Export Private Key" — Phantom, Solflare, Backpack, etc. | await loadKeypair("3NYE…") |
| Base64 string | A solana-keygen JSON file piped through base64. Safe to hold in env vars. |
await loadKeypair(process.env.CLIENT_PRIVATE_KEY!) |
| JSON file path (Node only) | Solana CLI keypair files — ~/.config/solana/id.json style. |
await loadKeypair.fromFile("./client.json") |
The format is auto-detected from the string contents. The keypair is held in module-private memory; the only thing the SDK exposes back is the public address (wallet.address).
See Setup for the full generate-and-fund flow. The API covers Solana transaction fees, so the wallet only needs USDC — no SOL required. Always confirm the live network with GET / on the API URL; that endpoint is authoritative.
const client = new VybeClient({
wallet,
budget: { maxUsd: 1.00, onExceed: "reject" }, // "warn" to log instead
});
// Throws BudgetExceededError before signing once cumulative spend would
// exceed $1. WS sessions and topups also count.
console.log(client.budgetState()); // { capUsd: 1, spentUsd: 0.013, remainingUsd: 0.987 }
Catch with instanceof:
| Error | When | Notes |
|---|---|---|
PaymentRequiredError |
API rejected the payment | Includes amountUsd, payTo, network |
ApiError |
Non-2xx (not 402) | status, refunded, chargedUsd |
ServiceUnavailableError |
Vybe API down | Retry with backoff |
InsufficientCreditsError |
WS session ran out of credits | Carries final balance |
BudgetExceededError |
Call would push spend over maxUsd |
Carries attemptedUsd, capUsd, spentUsd |
UntrustedPaymentError |
SDK refused to sign a 402 (bad payTo, network, or over maxUsdPerCall) |
reason; no funds at risk — thrown before signing |
NetworkError |
Transport / DNS / parse failure | Wraps the underlying cause |
VybeError |
Base class for all of the above | Catch-all |
ApiError.refunded === true means the API abandoned the payment (typically 5xx). false means the call billed despite the error (typically 4xx user errors). chargedUsd reflects the actual amount taken.
import {
ApiError, BudgetExceededError, InsufficientCreditsError,
PaymentRequiredError, ServiceUnavailableError, NetworkError,
UntrustedPaymentError,
} from "@vybenetwork/x402-client";
try {
const data = await client.get("/v4/tokens/<mint>/top-holders?limit=10");
} catch (err) {
if (err instanceof BudgetExceededError) {
// We hit our self-imposed cap. Stop, tell the user, raise the cap,
// or wait — the SDK didn't sign anything for this call.
console.error(`Stopped at $${err.spentUsd.toFixed(3)} of $${err.capUsd.toFixed(3)} cap`);
} else if (err instanceof PaymentRequiredError) {
// API refused the payment (bad signature, expired tx, etc.).
// Usually transient — retry. If persistent, check the wallet has
// USDC on Solana mainnet (the wallet doesn't need SOL — the API
// covers Solana gas).
} else if (err instanceof ApiError) {
// API returned a non-2xx, non-402 status.
if (err.refunded) console.error(`API ${err.status} — no charge`);
else console.error(`API ${err.status} — billed $${err.chargedUsd.toFixed(4)}`);
} else if (err instanceof ServiceUnavailableError) {
// Vybe API is degraded. Retry with backoff.
} else if (err instanceof InsufficientCreditsError) {
// Only thrown by client.stream() when the WS session is exhausted.
// The session is dead; mint a new one with another client.stream().
} else if (err instanceof NetworkError) {
// DNS / connection / timeout / body-parse. Retry, but log .cause —
// 429s from the public Solana RPC surface here when rpcUrl isn't set.
}
}
The SDK ships an ESM browser bundle (dist/index.browser.js) that maps the ws package to the platform's native WebSocket. Bundlers that respect the browser package field pick it up automatically.
That said, do not use this SDK in a browser with a raw private key. loadKeypair takes a base58 or base64 keypair, which would be visible to any user (or extension, or DevTools tab) with access to the page. Real browser usage requires wallet-adapter signing (Phantom, Solflare, etc.) — which the SDK doesn't currently support. Server-side Node is the realistic target today.
The x402 API also doesn't have CORS configured yet; in practice the browser bundle is for a future use case, not a present one.
The SDK is just convenience. The wire protocol is x402 over HTTP plus a small JSON session API for WebSocket. If you'd rather build against the wire directly — for a non-Node language, a thin shell script, or to keep your dependency tree small — the recipes below cover everything the SDK does.
Every /v4/* request expects a payment. The first call returns 402 with the requirements; sign a USDC transfer matching them and retry with the signed payload in an X-PAYMENT header. On 2xx, a base64-encoded payment-response header carries the receipt.
# 1. First call — API returns 402 with what to pay
curl -i https://x402-api.vybenetwork.xyz/v4/tokens/DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263
# HTTP/2 402
# content-type: application/json
# {
# "x402Version": 2,
# "accepts": [{
# "scheme": "exact",
# "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
# "amount": "1000", // 0.001 USDC in atomic units (6 decimals)
# "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // mainnet USDC mint (devnet differs)
# "payTo": "Cic...", // pay-to address (the API publishes this)
# "maxTimeoutSeconds": 3600,
# "extra": { "feePayer": "..." } // API covers Solana gas
# }]
# }
To sign: build a USDC SPL token transfer matching accepts[0], sign it with your wallet, base64-encode the signed transaction inside the x402 payment payload, and resend the request with the payload in the X-PAYMENT header. The x402 API verifies the payment, settles it on-chain, and forwards the response. Successful responses include a payment-response header (base64 JSON) with txHash, amount, and settlement mode.
The x402 reference implementations cover Solana signing in TypeScript, Go, Python, and Rust — picking up the SDK becomes a 10-line wrapper around @x402/core + @x402/svm. There's no protocol secret sauce in this SDK.
If you don't want any dependency at all, x402-proxy is a third-party curl-style CLI and MCP proxy that auto-pays any x402 endpoint, including Vybe:
npx x402-proxy https://x402-api.vybenetwork.xyz/v4/tokens/<mintAddress>
It derives keys from a mnemonic, signs the USDC transfer, retries with the payment header, and streams the response. Also runs as an MCP server, so AI hosts like Claude Desktop, Cursor, or Windsurf can call Vybe over stdio MCP without this SDK. Equally valid path — pick whichever fits the deployment.
Streaming uses one x402 payment to mint a prepaid session, then connects to /live with a JWT.
# 1. Pay $0.01 to mint a session (same x402 dance, POST instead of GET)
curl -X POST -H "X-PAYMENT: <signed payload>" \
https://x402-api.vybenetwork.xyz/api/sessions
# → 201 { "sessionId": "...", "jwt": "<jwt-string>", "credits": 1000, "expiresAt": "..." }
// 2. Connect with the JWT from step 1's response, send Vybe's
// configure message verbatim. Query param is `jwt` (not `token`)
// to disambiguate from SPL token.
const { sessionId, jwt } = await sessionResponse.json(); // from step 1
const ws = new WebSocket(`wss://x402-api.vybenetwork.xyz/live?jwt=${jwt}`);
ws.onopen = () => ws.send(JSON.stringify({
type: "configure",
filters: { trades: [{ tokenMintAddress: "..." }] }, // see Vybe's WS filter docs
}));
ws.onmessage = (ev) => {
const msg = JSON.parse(ev.data);
if (msg.type === "connected") return; // session info frame
// Event frame: { data, credits: { balance, cost, warning? } }
console.log(msg.data, "balance:", msg.credits.balance);
};
# 3. Top up before credits run out (default cap at balance < 50)
curl -X POST -H "X-PAYMENT: <signed payload>" \
-H "Authorization: Bearer <jwt>" \
-H "Idempotency-Key: <uuid>" \
https://x402-api.vybenetwork.xyz/api/sessions/<sessionId>/topup
# → 200 { "creditsAdded": 1000, "balance": ..., "jwt": "<refreshed-jwt>", ... }
The WebSocket close codes the SDK maps to typed errors are documented but stable: 4010 insufficient credits, 4503 API unavailable, 4001/4002/4008 auth/protocol errors. 1000/1001 are normal closes.
GET / returns network, pay-to, and per-route pricing. The SDK uses this to build the x402 signer and predict cost; you can hit it the same way to avoid hardcoding addresses.
curl https://x402-api.vybenetwork.xyz/
# → { "network": "solana:...", "payTo": "...", "defaultPrice": "$0.001",
# "pricing": [{ "match": "top-holders", "price": "$0.008" }, ...] }
new VybeClient({
wallet, // required — from loadKeypair()
apiUrl: "https://x402-api.vybenetwork.xyz", // default; override only for staging/self-host
rpcUrl: "https://my-paid-rpc/...", // strongly recommended — see below
budget: { maxUsd, onExceed }, // optional cumulative-spend cap
maxUsdPerCall: 0.10, // per-call hard cap, default $0.10
autotopupThreshold: 50, // WS auto-topup trigger (0 disables)
});
maxUsdPerCall (per-call hard cap)The SDK refuses to sign any 402 challenge demanding more than this amount, throwing UntrustedPaymentError before the signature is produced. Default: $0.10 — 10× the highest known endpoint tier. It's a defense against API misconfiguration, a 402 injected by a man-in-the-middle, or unexpected price drift. Complements budget.maxUsd (cumulative cap): maxUsdPerCall bounds a single transfer, budget.maxUsd bounds the running total.
import { VybeClient, UntrustedPaymentError } from "@vybenetwork/x402-client";
try {
await client.get("/v4/tokens/<mint>");
} catch (err) {
if (err instanceof UntrustedPaymentError) {
// err.reason is "payTo_mismatch" | "network_mismatch" | "amount_exceeds_per_call_cap"
console.error(`SDK refused to sign: ${err.reason} — ${err.message}`);
}
}
Raise it if you've audited a higher-priced endpoint; lower it for tighter control.
rpcUrl (Solana RPC for client-side signing)The SDK signs each x402 payment client-side, which makes two read-only RPC calls (fetchMint + getLatestBlockhash) per paid request. Without rpcUrl, it falls back to whatever @x402/svm picks — currently the public mainnet RPC, which rate-limits at roughly 5 RPS and surfaces as NetworkError with a 429: Too Many Requests cause as soon as you have any concurrency.
For anything beyond casual sequential use, set this to a paid tier:
const client = new VybeClient({
wallet: await loadKeypair(process.env.CLIENT_PRIVATE_KEY!),
rpcUrl: process.env.SOLANA_RPC_URL, // Helius / Triton / QuickNode / etc.
});
Need a free RPC to get started? Helius has a free tier (10 RPS, no card) — sign up, paste the URL into rpcUrl. Agents can also obtain one autonomously via the Helius CLI ($1 USDC, no browser): see their agents.md.
Runnable scripts in examples/:
examples/http.ts — paid HTTP requestsexamples/ws-stream.ts — WebSocket streaming with auto-topupexamples/agent.ts — LLM tool-call loopRun with CLIENT_PRIVATE_KEY=<base64> npx tsx examples/<file>.ts.
See AGENTS.md — machine-readable onboarding doc covering install, prerequisites, the paid-call flow, error semantics, and discovery.
Hit a bug, an unclear error, or want an endpoint added to the curated tool catalog? Open an issue on this repo, or reach out via the Intercom widget on vybe.fyi — we'll see it directly.
For traditional subscription pricing instead of pay-per-call x402, see vybe.fyi/api-pricing.
npm install
npm test # vitest
npm run typecheck
npm run build # ESM + CJS + browser bundle
MIT
x402 (HTTP 402 Payment Required) SDK + MCP server: let any API charge for itself and any AI agent pay for itself, USDC & stablecoins across EVM, Solana & 8 more chain families, in a couple of lines. Backendless, no fee, self-custodial, paid straight to your wallet. TypeScript, MIT.
The commerce layer for the x402 agent economy — micropayments for AI agents on Base and Solana
Enterprise audit, compliance & budget enforcement layer for the x402 payment protocol
On-chain USDC payment gateway for AI agents. x402 protocol.
Pay-per-use API gateway for AI agents. Inference, Twitter, Reddit, and web APIs - no signup, pay per call via x402 and MPP.
Paywalled HTTP tools for agents using x402 (HTTP 402): auto-handle 402 → pay → retry with a server middleware, agent-friendly client, and examples
The agent-native LLM router for autonomous agents. 55+ models (8 free), <1ms local routing, USDC payments on Base & Solana via x402.
The AI agent with a wallet — spends USDC autonomously to get real work done. Apache-2.0, TypeScript.
DePIN for Vintage Hardware — Proof-of-Antiquity blockchain where old machines outmine new ones. AI-powered hardware fingerprinting, 15+ CPU architectures, Solana bridge (wRTC). $0 VC.
🚀 Curated list of x402 resources: HTTP 402 Payment Required protocol for blockchain payments, crypto micropayments, AI agents, API monetization. Includes SDKs (TypeScript, Python, Rust), examples, facilitators (Coinbase, Cloudflare), MCP integration, tutorials. Accept USDC payments with one line of code. Perfect for AI agent economy.
The financial operating system for AI agents. A single HTTP client that intercepts '402 Payment Required', routes across x402, L402, and MPP, persists credentials as recoverable assets, enforces per-envelope budgets, and emits a structured trace.
MCP server for Run402 — AI-native Postgres + REST + auth + storage + static sites. Pay with x402 USDC on Base. No signups.