Common patterns & pitfalls
Practical tips for integrating @bento.fun/sdk — IDs, amounts, auth, and errors.
Answers to questions that come up when wiring the SDK against live APIs. For the happy path, start with Quickstart.
Two hosts, one factory
import { createBentoSdk, jwtAuthProvider, walletAuthProvider } from '@bento.fun/sdk';
const sdk = createBentoSdk({
baseUrl: process.env.BENTO_URL!, // https://internal-server.bento.fun
apiKey: process.env.BENTO_BUILDER_API_KEY!,
tournamentsBaseUrl: process.env.PARLAY_TOURNAMENT_URL, // optional
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 |
→ Environments · Two API hosts
id vs duelId on market rows
| Field | Use for |
|---|---|
id | Database row id — do not pass to getDuelById |
duelId | On-chain duel id — use this for detail, bets, charts |
const { data } = await sdk.public.listDuels({ page: 1, limit: 10 });
await sdk.public.getDuelById({ duelId: data[0].duelId }); // ✓
// await sdk.public.getDuelById({ duelId: data[0].id }); // ✗ 404Amounts are in wei
betAmountUsdc / betAmount are base units (18 decimals on BSC / credits). '10' is 10 wei. Prefer parseUnits('10', 18) or a literal like '10000000000000000000'.
Collateral: credits vs USDC
| Mode | Geo |
|---|---|
usdc | Geo-gated (403 + GEO_BLOCKED) |
credits | Not geo-gated |
Fund the managed account before writing. Testnet: await sdk.public.faucet.mint({ address: managedAccountAddress }). → Money
Place bets with placeBetFromEstimate
const est = await sdk.user.bets.estimateBuy({
duelId,
optionIndex: 0,
betAmountUsdc: stake,
slippageBps: 100,
});
if (!est.success) throw new Error('estimate rejected');
await sdk.user.placeBetFromEstimate(
{
estimate: est.estimate,
duelId,
duelType: market.duelType ?? 'PREDICTION',
bet: market.options?.[0] ?? 'YES',
optionIndex: 0,
betAmount: stake,
betAmountUsdc: stake,
slippageBps: 100,
tokenDecimals: 18,
},
{ idempotencyKey }, // caller-stable, not Date.now()
);Wrapped API responses
Some tournaments endpoints return objects, not bare arrays:
const { tournaments, total } = (await sdk.tournaments!.tournaments.list({
status: 'active',
})) as { tournaments: unknown[]; total: number };Schemas: OpenAPI specs.
HTTP acceptance ≠ on-chain finality
placeBet / placeBetFromEstimate, createDuel, invite userJoin, pack picks, and tournament enters return when the server accepted the request. Poll reads afterward. → Mutations
Errors
Failed calls throw a BentoSdkErrorException. Inspect with the exported helpers, which take the value you caught and unwrap .sdkError for you:
import {
isAuthError, isValidationError, isRateLimited, isGeoblockError,
getFieldErrors, getRetryAfterMs, getRequestId, getStatus, getErrorCode,
} from '@bento.fun/sdk';
try {
await sdk.user.placeBetFromEstimate(input, { idempotencyKey });
} catch (err) {
if (isRateLimited(err)) {
await sleep(getRetryAfterMs(err) ?? 1_000);
} else if (isValidationError(err)) {
showFieldErrors(getFieldErrors(err));
} else if (isGeoblockError(err)) {
// 403 GEO_BLOCKED on the usdc stack — try collateralMode: 'credits'
} else if (isAuthError(err)) {
await refreshSession();
} else {
report({
status: getStatus(err),
code: getErrorCode(err),
requestId: getRequestId(err),
});
}
}Branch on getErrorCode(err) rather than matching message; messages are prose and will change.
getRequestId(err) returns the id the server echoed on x-request-id when it sends one, falling back to the client-generated id. Quote it in bug reports.
Do not call isBentoSdkError(caught). It narrows the error record, and what is thrown is the exception that carries that record on .sdkError, so it is always false. Use the helpers above (or toSdkError(err) for the raw record).
Error kinds
kind | HTTP | Fields present | Helper |
|---|---|---|---|
http_error | 404, 451, 5xx | status, code?, correlationId, message, details | getStatus, getErrorCode |
auth_error | 401, 403 | message, status, code? | isAuthError |
validation_error | 400, 422 | status, fieldErrors?, message | isValidationError, getFieldErrors |
rate_limited | 429 | status, retryAfterMs?, code?, message | isRateLimited, getRetryAfterMs |
timeout | n/a | timeoutMs, message | isTimeoutError |
network_error | n/a | retryable, cause?, message | isNetworkError |
Every kind also carries requestId; read it with getRequestId.
Common errors
| You see | Cause | Fix |
|---|---|---|
401 / 403 (auth_error) on sdk.user | missing/invalid Bearer JWT | eoaLogin / register → Authorization: Bearer |
400 Insufficient Credits | managed account unfunded | testnet auto-mint on managed address |
400 Start time must be in the future | startTime ≤ now | set strictly ahead |
500 Pre-flight simulation failed | on-chain create reverted (often short startTime) | larger offset (~31 min public / ~5 min private) |
500 Unable to place bet… after estimate | stake < 5 units | stake ≥ 5; pass tokenDecimals |
400 Paid tournaments require clientTournamentId… | stage-1 buy-in without vault deposit | approve + vault deposit → create with creatorDepositTxHash |
404 from getDuelById | passed database id | use duelId |
403 GEO_BLOCKED | USDC geo gate | isGeoblockError; use credits |
429 (rate_limited) | builder-key budget exhausted | wait getRetryAfterMs(err); configure SDK rateLimit |
sdk.onchain.* { success: false } | on-chain helpers never throw | branch on result.success |
Rate limits
Limits are enforced per builder key, not per endpoint or per user. Default is 120/min for team-issued keys; self-serve testnet keys get 300/min. Because the budget is shared, a burst on one endpoint returns 429 for everything that key is doing.
A 429 surfaces as kind: 'rate_limited'. The SDK parses Retry-After into retryAfterMs and honours it in automatic retry backoff:
if (isRateLimited(err)) await sleep(getRetryAfterMs(err) ?? 1_000);Pacing calls before you hit the limit
Opt-in client pacing (off unless configured):
const sdk = createBentoSdk({
baseUrl, apiKey, auth,
rateLimit: {
default: { limit: 120, intervalMs: 60_000 },
endpoints: {
'POST /bento/user/bets/create': { limit: 20, intervalMs: 60_000 },
'/bento/user': { limit: 60, intervalMs: 60_000 },
},
onLimit: 'wait', // or 'throw' to shed load
maxWaitMs: 60_000,
},
});Rules match most-specific-first: METHOD /exact/path, then /exact/path, then longest path prefix, then default. Paths work with or without the /bento prefix.
With onLimit: 'throw' the SDK fails fast with code: 'client_rate_limit' and does not retry it. Use getErrorCode(err) to tell a local refusal from a real server 429.
This is client-side politeness, not enforcement. The server remains the authority.
Security
- Never hard-code private keys or Builder API keys
- Markets
sdk.userauth is Bearer JWT, not rawx-wallet-* creditsbypasses geo;usdcdoes not
Where to look next
| Need | Page |
|---|---|
| Golden path | Quickstart |
| Method shortlist | Cookbook |
| Every SDK method | SDK API reference |
| JSON schemas | OpenAPI specs |
| npm smoke tests | Test the SDK |