Common patterns & pitfalls
Practical tips for integrating @bento.fun/sdk, covering IDs, response shapes, auth, and errors.
Answers to questions that come up often when wiring the SDK against live APIs.
Two hosts, one factory
const sdk = createBentoSdk({
baseUrl: process.env.BENTO_URL!,
tournamentsBaseUrl: process.env.PARLAY_TOURNAMENT_URL,
auth: walletAuthProvider(() => ({ Authorization: `Bearer ${token}` })),
tournamentsAuth: jwtAuthProvider({ getAccessToken: () => token }),
});| Client | Host env | Typical auth |
|---|---|---|
sdk.public / sdk.user | BENTO_URL | Bearer JWT on user |
sdk.tournaments | PARLAY_TOURNAMENT_URL | JWT on tournamentsAuth |
See Two API hosts.
id vs duelId on market rows
listDuels / listMarkets return rows with two identifiers:
| Field | Example | Use for |
|---|---|---|
id | 6a1ef4329676cfd69ceec4f0 | Database row id: do not pass to getDuelById |
duelId | 94c88855f750… | On-chain duel id: use this for detail, bets, charts |
const { data } = await sdk.public.listDuels({ page: 1, limit: 10 });
const row = data[0];
// ✓ Correct
await sdk.public.getDuelById({ duelId: row.duelId });
// ✗ 404: wrong id type
await sdk.public.getDuelById({ duelId: row.id });Amounts are in wei
betAmountUsdc and betAmount are base units of the collateral token (wei), not whole tokens. On BSC / credits the collateral is 18 decimals, so 10 units is '10000000000000000000'; passing '10' means 10 wei, effectively zero.
Scale by the token's decimals (use collateralDecimals from your onchain.contracts, e.g. viem's parseUnits('10', 18)).
Collateral: credits vs USDC
Markets and bets carry a collateralMode of 'usdc' or 'credits':
| Mode | What it is | Geo |
|---|---|---|
usdc | on-chain USDC collateral | geo-gated: restricted regions get 403 with code GEO_BLOCKED |
credits | a platform-managed balance | not geo-gated; usable where usdc is blocked |
Either mode needs a funded balance, or writes fail with 400 (Insufficient Credits / insufficient balance). Fund the managed account before writing. On testnet, register/login does not fund you; call POST /bento/auto-mint/mint with { userAddress: managedAccountAddress } (~1000 USDC + ~1000 credits). See Money.
Wrapped API responses
Some tournaments endpoints return objects, not bare arrays:
// Bracket tournaments list
const { tournaments, total } = await sdk.tournaments!.tournaments.list({
status: 'active',
}) as { tournaments: unknown[]; total: number };Check the Tournaments OpenAPI schema when typing responses.
HTTP acceptance ≠ on-chain finality
placeBet, sellBet, createDuel, duel invite userJoin, pack picks, and tournament enters return when the server accepted the request, not when the chain finalized.
After mutations:
- Poll reads (
getDuelById,getUserShares, tournament status) - Use
waitFor*helpers where provided
See Mutation semantics.
Auth providers by integration
| Your app | Provider | Pass as |
|---|---|---|
| Browser wallet | walletAuthProvider | auth |
| JWT session (tournaments) | jwtAuthProvider | tournamentsAuth |
See Authentication.
Idempotency for bets
Always pass an idempotency key when placing bets:
// Estimate first (see Place a bet), then place with the quote fields:
await sdk.user.placeBet(
{ duelId, duelType, bet: 'optionA', optionIndex: 0, betAmount, betAmountUsdc,
sharesOut, minSharesOut, slippageBps: 100, quoteId },
{ idempotencyKey }, // caller-stable, not Date.now()
);Errors
Failed HTTP calls throw BentoSdkErrorException with error.sdkError:
import { isGeoblockError } from '@bento.fun/sdk';
try {
await sdk.user.placeBet(body, { idempotencyKey });
} catch (e) {
// NB: pass the `.sdkError` record to the guards, not the caught exception
const err = (e as { sdkError?: { kind: string; status?: number; correlationId?: string } }).sdkError;
if (isGeoblockError(err)) {
// geo-blocked: detects the 403 with code GEO_BLOCKED the API actually returns (and 451, defensively)
} else if (err?.kind === 'auth_error') {
// 401 / 403: message, status, and code when the body carries one (no correlationId)
} else if (err?.kind === 'http_error') {
console.log(err.status, err.correlationId);
}
}Error kinds
Each sdkError.kind carries different fields; branch on kind, not on status:
kind | HTTP | Fields present |
|---|---|---|
http_error | 404, 451, 5xx | status, correlationId, message, details |
auth_error | 401, 403 | message, status, code? (no correlationId) |
validation_error | 400, 422 | status, fieldErrors?, message |
rate_limited | 429 | retryAfterMs?, message |
Common errors
| You see | Cause | Fix |
|---|---|---|
401 / 403 (auth_error) on sdk.user | missing or invalid Bearer JWT | log in (eoaLogin) and send Authorization: Bearer; markets is Bearer, not x-wallet-* |
400 Insufficient Credits / insufficient balance | managed account unfunded in the chosen collateralMode | fund via testnet POST /bento/auto-mint/mint (or switch mode) |
400 Start time must be in the future on createDuel | startTime ≤ now | set startTime strictly ahead of wall clock |
500 Pre-flight simulation failed on createDuel | on-chain requestCreateDuel reverted in the managed (custodial) wallet simulation; often startTime only a few minutes ahead | use a larger offset (~31 min ahead public / ~5 min private); see Create a market |
500 Unable to place bet… after a good estimate | amount below the 5-unit platform / on-chain minimum (estimateBuy does not enforce it) | stake ≥ 5 units; pass tokenDecimals so the SDK rejects early; see Place a bet |
400 Paid tournaments require clientTournamentId… | stage-1 buy-in > 0 without a prior vault deposit | generate 24-hex id → approve + vault deposit → create with creatorDepositTxHash; see Create a tournament |
404 from getDuelById / getMarketById | passed the database id, not duelId | use duelId from the list row |
403 with code GEO_BLOCKED (auth_error) | region geo-gated on the usdc stack, not a JWT problem | check isGeoblockError before treating a 403 as auth; use collateralMode: 'credits' |
429 (rate_limited) | rate limited | back off at least retryAfterMs before retrying |
sdk.onchain.* returns { success: false } | on-chain methods never throw | branch on result.success; read result.error |
Rate limits
A 429 surfaces as kind: 'rate_limited'. The SDK parses Retry-After into retryAfterMs and honours it in automatic retry backoff (0.5.4+). When handling errors yourself, still wait at least retryAfterMs before retrying.
Security
- Wallet auth on
sdk.useris a Bearer JWT fromeoaLogin/eoaRegister. See Authentication. collateralMode: 'credits'bypasses geo-restrictions; theusdcstack is geo-gated.- Never hard-code private keys, API keys, or access codes; keep them in env / secrets.
Where to look next
| Need | Page |
|---|---|
| Every SDK method + route | SDK API reference |
| JSON request/response shapes | Markets · Tournaments OpenAPI |
| Smoke-test published npm package | Test the SDK |
| End-to-end flows | Quickstart · Guides |