Quickstart
Zero to first bet on testnet — one contiguous script you can paste into an agent or Node file.
By the end: you have listed a market, logged in (or registered), funded the managed account, and placed a bet with placeBetFromEstimate.
Agents: prefer this page + Cookbook + Common patterns. Plain text: /llms.txt.
1. Install & env
npm install @bento.fun/sdkNode.js 18+.
createBentoSdk requires a Builder API key (apiKey). Catalog reads are
open without it on the wire, but login / register and authenticated
calls need x-builder-api-key so your app is attributed.
Mint a testnet Builder API key, then:
export BENTO_URL='https://internal-server.bento.fun'
export BENTO_BUILDER_API_KEY='bnt_…'Full host table: Environments.
2. Zero → first bet (copy-paste)
Replace account with a viem (or compatible) wallet that can signMessage. Run once on testnet.
import { createBentoSdk, walletAuthProvider } from '@bento.fun/sdk';
const baseUrl = process.env.BENTO_URL!; // https://internal-server.bento.fun
const apiKey = process.env.BENTO_BUILDER_API_KEY!;
// --- public client (reads + login/register) ---
const sdk = createBentoSdk({
baseUrl,
apiKey,
auth: walletAuthProvider(() => ({})),
});
// A. List markets — use duelId (on-chain), NEVER id (database row)
const { data } = await sdk.public.listDuels({ page: 1, limit: 10 });
const row = data[0];
if (!row?.duelId) throw new Error('no markets');
const duelId = row.duelId;
const market = await sdk.public.getDuelById({ duelId });
// B. Challenge → sign the server message verbatim (includes first-party warning)
const challenge = await sdk.public.auth.eoaChallenge({
address: account.address,
domain: 'yourapp.example',
});
const signature = await account.signMessage({ message: challenge.message });
// C. Login, or register once if the wallet is new
// JWT `address` = managed (custodial) account where funds live; signing key ≠ that address
type AuthRes = { exists?: boolean; token?: string; address?: string };
let authRes = (await sdk.public.auth.eoaLogin({
address: account.address,
signature,
timestamp: challenge.timestamp,
domain: challenge.domain,
})) as AuthRes;
if (!authRes.exists || !authRes.token) {
authRes = (await sdk.public.auth.eoaRegister({
address: account.address,
signature,
timestamp: challenge.timestamp,
domain: challenge.domain,
username: `builder-${account.address.slice(2, 8)}`,
})) as AuthRes;
}
const token = authRes.token!;
const managedAccountAddress = authRes.address!; // fund THIS address, not your EOA
// D. Authenticated client — markets user routes need Bearer JWT
const authed = createBentoSdk({
baseUrl,
apiKey,
auth: walletAuthProvider(() => ({ Authorization: `Bearer ${token}` })),
});
// E. Fund on testnet (register does NOT credit you)
await authed.public.faucet.mint({ address: managedAccountAddress });
// ~1000 USDC + ~1000 credits on the managed account
// F. Estimate → place (always use placeBetFromEstimate; min bet = 5 units)
const stake = '10000000000000000000'; // 10 units at 18 decimals — NOT '10'
const est = await authed.user.bets.estimateBuy({
duelId,
optionIndex: 0,
betAmountUsdc: stake,
slippageBps: 100,
});
if (!est.success) throw new Error('estimate rejected');
await authed.user.placeBetFromEstimate(
{
estimate: est.estimate,
duelId,
duelType: market.duelType ?? 'PREDICTION', // PREDICTION | VERSUS
bet: market.options?.[0] ?? 'YES', // option label text, not only optionIndex
optionIndex: 0,
betAmount: stake,
betAmountUsdc: stake,
slippageBps: 100,
tokenDecimals: 18,
},
{ idempotencyKey: crypto.randomUUID() },
);
// G. Reconcile — poll the managed address (acceptance ≠ settlement)
const shares = await authed.user.bets.getUserShares({
duelId,
address: managedAccountAddress,
});
console.log({ duelId, shares });Rules that trip everyone
| Pitfall | Fix |
|---|---|
getDuelById 404 | Pass duelId, not list row id |
| Amounts look wrong | Use wei (18 decimals on BSC/credits): '10000000000000000000' = 10 |
Insufficient Credits | Fund managed address via auto-mint after login |
| Slippage 400 after a good quote | Use placeBetFromEstimate, not hand-built min_shares_out |
401 on sdk.user | Send Authorization: Bearer <JWT> (not raw x-wallet-*) |
More: Common patterns · FAQ
Tournaments (optional second host)
Only when you need brackets / F1:
import { createBentoSdk, jwtAuthProvider, walletAuthProvider } from '@bento.fun/sdk';
const sdk = createBentoSdk({
baseUrl: process.env.BENTO_URL!,
apiKey: process.env.BENTO_BUILDER_API_KEY!,
tournamentsBaseUrl: process.env.PARLAY_TOURNAMENT_URL, // see Environments
auth: walletAuthProvider(() => ({ Authorization: `Bearer ${token}` })),
tournamentsAuth: jwtAuthProvider({ getAccessToken: () => token }),
});
const { tournaments } = (await sdk.tournaments!.tournaments.list({
limit: 5,
})) as { tournaments: unknown[] };→ Enter a tournament · Environments