@bento.fun/sdk
v0.7.2
Published
Official TypeScript SDK for Bento prediction markets, packs, parlays, tournaments, fantasy, portfolio, and realtime.
Maintainers
Readme
@bento.fun/sdk
Official TypeScript SDK for Bento — prediction markets, packs, parlays, bracket tournaments, F1, and portfolio.
Features
- Two API hosts — markets (
baseUrl) and tournaments / parlay (tournamentsBaseUrl) from one factory - Contract-backed types — OpenAPI-generated TypeScript for request/response shapes
- Wallet-agnostic — bring Privy, Wagmi, Turnkey, or server signers; SDK accepts headers and viem wallets
- Typed HTTP methods — public reads, authenticated trading, parlays, bracket tournaments, and gated admin
- On-chain orchestration — optional viem helpers for parlay placement, vault deposit/claim, LP
Requirements
- Node.js 18+
- TypeScript 5+ (recommended)
Install
npm install @bento.fun/sdkQuick start
Every createBentoSdk call needs a Builder API key (apiKey). Catalog reads still work without a user login. sdk.user (bets, create market) also needs a Bearer JWT from eoaLogin.
import {
createBentoSdk,
jwtAuthProvider,
walletAuthProvider,
} from '@bento.fun/sdk';
const baseUrl = process.env.BENTO_URL!; // https://internal-server.bento.fun on testnet
const tournamentsBaseUrl = process.env.PARLAY_TOURNAMENT_URL; // optional
const BUILDER_API_KEY = process.env.BENTO_BUILDER_API_KEY!;
// apiKey is required; public reads ignore the empty user JWT slot
const sdk = createBentoSdk({
baseUrl,
apiKey: BUILDER_API_KEY,
tournamentsBaseUrl,
auth: walletAuthProvider(() => ({})),
});
const { data } = await sdk.public.listDuels({ page: 1, limit: 20 });
// Get a Bearer JWT (see Authentication below). A brand-new wallet must register
// first — eoaLogin returns { exists: false } (no token) until then.
const token = await getBearerToken();
// Authenticated client. walletAuthProvider only injects headers, so put the
// Bearer token inside it for the markets host.
const authed = createBentoSdk({
baseUrl,
tournamentsBaseUrl,
apiKey: BUILDER_API_KEY,
auth: walletAuthProvider(() => ({ Authorization: `Bearer ${token}` })),
tournamentsAuth: jwtAuthProvider({ getAccessToken: () => token }),
});
// Use duelId (on-chain), not id (database row).
const row = data[0];
if (row?.duelId) {
await authed.public.getDuelById({ duelId: row.duelId });
}Authentication
Two layers: the builder API key (apiKey → x-builder-api-key) identifies
your app; the user JWT (auth) identifies the user. createBentoSdk
requires apiKey (type + runtime).
| Host | Config | Headers |
| ------------------------ | ----------------- | --------------------------------------------------------------------------------------- |
| Markets (sdk.user) | auth | Authorization: Bearer <JWT> from public.auth.eoaLogin |
| Tournaments | tournamentsAuth | Bearer JWT (from tournaments.auth.login) or legacy x-wallet-* signature headers |
| All hosts | apiKey | x-builder-api-key: <key> |
Both hosts sign
Bento.fun Login\nTimestamp: …\nWallet: …. The tournamentsx-wallet-*header path is legacy and verifies a different message (Bento.fun Authentication\n…) — prefer the Bearer JWT.
walletAuthProvider only injects headers — for markets pass Bearer inside it:
const ts = String(Date.now());
const signature = await wallet.signMessage({
message: `Bento.fun Login\nTimestamp: ${ts}\nWallet: ${address}`,
});
// eoaLogin returns { exists: false } (HTTP 200, no token) for a wallet that has
// never registered. Register once to mint the account + JWT; afterwards eoaLogin
// returns { exists: true, token }.
let res = await publicClient.auth.eoaLogin({ address, signature, timestamp: ts });
if (!res.exists) {
// username is required; mainnet also requires inviteCode.
res = await publicClient.auth.eoaRegister({
address,
signature,
timestamp: ts,
username,
});
}
const token = res.token as string;| Provider | Use for |
| -------------------- | ---------------------------------------------------- |
| walletAuthProvider | Inject Authorization: Bearer (or tournaments headers) |
| jwtAuthProvider | Bearer JWT |
Weblink auth: sdk.public.externalLink.getLinkUrl → user confirms on
/connect → exchange({ code }) → you store the JWT. See docs/auth.md.
See Authentication and Accounts & wallets (signing key ≠ managed account address).
Important semantics
- HTTP acceptance ≠ on-chain finality — poll reads after mutations.
duelIdvsid— list rows have both; useduelIdforgetDuelById/ bets.- Amounts in wei — bet/parlay amounts are base units (18 decimals on BSC/credits), not whole tokens.
- Prefer
placeBetFromEstimate— avoids slippage 400s from hand-builtmin_shares_out. creditsvsusdc—collateralModeon markets; credits stack is not geo-gated like USDC.- Referral analytics —
sdk.user.referralAnalytics.getMine({ collateralStack: 'usdc' | 'credits' }); see backendREFERRAL_ANALYTICS.mddeploy checklist.
On-chain (optional)
Requires 0.5.4+ for correct parlay/LP approval amounts.
await sdk.onchain!.placeParlayFromQuoteId(quoteId);
await sdk.onchain!.depositAndEnterTournament(tournamentId, { amountUsdc: '100' });Handling errors
Every failure is a BentoSdkErrorException carrying a typed record. Inspect it with
the helpers. Do not reach for isBentoSdkError on the caught value, which narrows
the record type, not the exception that was thrown, and so is always false:
import {
isAuthError, isValidationError, isRateLimited,
getFieldErrors, getRetryAfterMs, getRequestId, getStatus, getErrorCode,
} from '@bento.fun/sdk';
try {
await sdk.user.placeBet(body);
} catch (err) {
if (isRateLimited(err)) {
await sleep(getRetryAfterMs(err) ?? 1_000);
} else if (isValidationError(err)) {
showFieldErrors(getFieldErrors(err)); // { amount: ['too small'] }
} else if (isAuthError(err)) {
await refreshSession();
} else {
// Quote this in a bug report: it is the server's id when the backend echoes
// `x-request-id`, so it can be found in backend logs.
report({ status: getStatus(err), code: getErrorCode(err), requestId: getRequestId(err) });
}
}Branch on getErrorCode(err), not on message; messages are prose and will change.
Rate limiting
Bento enforces a limit per builder key: 120/min for team-issued keys, 300/min for self-serve testnet keys, configurable per app. It is a shared budget, so a burst on one endpoint returns 429 for everything that key is doing.
The SDK can pace calls before they leave your process. This is off by default:
const sdk = createBentoSdk({
baseUrl, apiKey, auth,
rateLimit: {
default: { limit: 120, intervalMs: 60_000 }, // your key's ceiling
endpoints: {
'POST /bento/user/bets/create': { limit: 20, intervalMs: 60_000 },
'/bento/user': { limit: 60, intervalMs: 60_000 }, // longest prefix wins
},
onLimit: 'wait', // or 'throw' to shed load instead of blocking
},
});Rules match most-specific-first: METHOD /exact/path, then /exact/path, then the
longest matching path prefix, then default. Paths work with or without the
/bento prefix, so you can copy keys straight from the API reference.
The window is fixed and clock-aligned, matching how the server counts
(floor(now / intervalMs)). That is deliberate: a smooth token bucket would allow
a full burst plus steady refill inside one server window and still trip the limit
at your configured ceiling.
On a server 429 the SDK also pauses that endpoint locally. The markets backend
sends no Retry-After, so the pause defaults to the remainder of the current
window, which is when the server's own counter resets.
This is politeness, not enforcement. The server remains the authority.
Documentation
| Resource | Description | | ----------------------------------------------------------------------- | --------------------------------------------------------- | | Quickstart | Zero → first bet (golden path) | | Cookbook | ~15 methods that cover most apps | | Authentication | Bearer login / register flow | | Common patterns | IDs, errors, rate limits | | Test the SDK | sdk-sandbox | | SDK API reference | Every method + route | | llms.txt | AI-friendly docs index |
Links
- App: app.bento.fun
- GitHub: github.com/Bentodotfun/bento.fun
- Issues: github.com/Bentodotfun/bento.fun/issues
License
MIT — see LICENSE.
