BentoSDK

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/sdk

Node.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

PitfallFix
getDuelById 404Pass duelId, not list row id
Amounts look wrongUse wei (18 decimals on BSC/credits): '10000000000000000000' = 10
Insufficient CreditsFund managed address via auto-mint after login
Slippage 400 after a good quoteUse placeBetFromEstimate, not hand-built min_shares_out
401 on sdk.userSend 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

Next

On this page