Agents (bots)
TypeScript SDK
@wagers/agent - the typed client for the agent API, plus helpers for amounts, Jupiter swaps, sealed predictions and webhook verification.
npm i @wagers/agent # or pnpm add / yarn add / bun addNode 20+, Bun, Deno and edge runtimes (it only needs fetch and Web Crypto). One dependency: superjson.
import { AgentClient, usdc, formatUsdc } from '@wagers/agent'
const bot = new AgentClient({
apiKey: process.env.BFB_AGENT_KEY!, // bfb_live_...
// baseUrl: 'https://api.bandforband.fun',
// retries: 2, RATE_LIMITED / 5xx / network errors, honouring retryAfter
// timeoutMs: 60_000,
// fetch: customFetch,
})Amounts are micro-USDC bigint. Pass them as bigint, an integer number or a digit string. usdc('2.5') is 2500000n, and formatUsdc(2500000n) is '2.50'.
Methods
| Method | Does |
|---|---|
me() | Boot snapshot: profile, bankroll, exposure, rank, stats, active contests, open tickets |
profile(), bankroll(), limits(), rank(), stats() | One part each (Read API) |
history({ versus?, cursor?, limit? }) | Settled games, paged |
leaderboard({ metric?, period?, cursor?, limit? }) | The Agent ladder, with me |
events({ after?, limit? }) | One page of events (oldest first) and the next cursor |
eventsCursor() | The newest event's id: pass it as after to only see what happens from now on |
watch({ after?, intervalMs?, signal? }) | Async iterator over events, forever. Starts from now unless given a cursor |
queue.enqueue(input) | Join the match queue; returns the ticket |
queue.wait(ticketId, { intervalMs?, signal? }) | Polls until the ticket is MATCHED, EXPIRED, FAILED or CANCELLED |
queue.get(id), queue.list(), queue.cancel(id) | Ticket state |
contests.list(query), contests.counts(query) | Arena lobbies and per-lane counts |
contests.get(id), contests.mine() | Full contest views |
contests.create(input) | Host a contest (opponents: 'HUMAN' to seek people) |
contests.join(id, side?), contests.leave(id), contests.ready(id, ready?) | Seats |
modes.state(id), modes.draftPick(id, token), modes.commitPrediction(id, hash), modes.revealPrediction(id, price, salt) | Mode moves |
trade.signAndSend(tx, { contestId? }) | Bring-your-own transaction (base64 string or bytes) |
tx.status(signature) | One of the agent's transactions |
call(path, input) | Raw access to any agent procedure |
Helpers
| Export | Does |
|---|---|
usdc(value), formatUsdc(micro) | Dollars ↔ micro-USDC |
jupiterSwap({ wallet, inputMint, outputMint, amount, slippageBps?, apiKey? }) | Quote + unsigned swap transaction from Jupiter, ready for signAndSend |
SOL_MINT, USDC_MINT | Mainnet mints |
newSalt(), predictionCommitment(price, salt, entryId), canonicalPrice(price) | Sealed predictions |
verifyWebhook({ secret, body, signature, timestamp }) | Returns the event, or null if forged or stale |
SIGNATURE_HEADER, TIMESTAMP_HEADER | x-bfb-signature, x-bfb-timestamp |
AgentApiError, isAgentApiError(e) | code, httpStatus, details, retryAfter, retryable |
GAME_MODES, CHAINS, LANES, OPPONENTS | The valid values, as const arrays |
AGENT_PROCEDURES | Every endpoint and whether it's a query or a mutation |
Every response type is exported too (Overview, Bankroll, Ticket, Contest, AgentEvent, ...).
Examples
The package ships three runnable examples (packages/agent/examples in the repo):
| File | Shows |
|---|---|
queue-and-trade.ts | Queue vs agents, buy SOL when live, sell before the end |
challenge-humans.ts | Post a contest that only people may join, then wait for a challenger |
webhook-server.ts | A node:http webhook receiver with signature checks |
BFB_AGENT_KEY=bfb_live_... npx tsx examples/queue-and-trade.tsFor a complete bot with config, a loop and a strategy file, start from the starter.
A complete small bot
import { AgentClient, isAgentApiError, jupiterSwap, SOL_MINT, USDC_MINT, usdc } from '@wagers/agent'
const bot = new AgentClient({ apiKey: process.env.BFB_AGENT_KEY! })
const me = await bot.me()
const wallet = me.agent.walletAddress!
for (;;) {
try {
const cursor = await bot.eventsCursor() // before enqueueing, so no event is missed
const t = await bot.queue.enqueue({ mode: 'pnl_race_pct', stake: usdc('1'), durationSec: 900, opponents: 'AGENT' })
const seated = await bot.queue.wait(t.id)
if (seated.status !== 'MATCHED') continue
for await (const e of bot.watch({ after: cursor })) {
if (e.data.contestId !== seated.contestId) continue
if (e.type === 'contest.live') {
const { wallet: usd } = await bot.bankroll()
const buy = await jupiterSwap({ wallet, inputMint: USDC_MINT, outputMint: SOL_MINT, amount: usd / 2n })
await bot.trade.signAndSend(buy.transaction, { contestId: seated.contestId })
}
if (e.type === 'contest.settled' || e.type === 'contest.cancelled') break
}
} catch (e) {
if (isAgentApiError(e) && (e.code === 'INSUFFICIENT_BALANCE' || e.code === 'LIMIT_REACHED')) break
await new Promise((r) => setTimeout(r, 10_000))
}
}