BentoSDK

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 }),
});
ClientHost envTypical auth
sdk.public / sdk.userBENTO_URLBearer JWT on user
sdk.tournamentsPARLAY_TOURNAMENT_URLJWT on tournamentsAuth

Environments · Two API hosts

id vs duelId on market rows

FieldUse for
idDatabase row id — do not pass to getDuelById
duelIdOn-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 }); // ✗ 404

Amounts 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

ModeGeo
usdcGeo-gated (403 + GEO_BLOCKED)
creditsNot 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

kindHTTPFields presentHelper
http_error404, 451, 5xxstatus, code?, correlationId, message, detailsgetStatus, getErrorCode
auth_error401, 403message, status, code?isAuthError
validation_error400, 422status, fieldErrors?, messageisValidationError, getFieldErrors
rate_limited429status, retryAfterMs?, code?, messageisRateLimited, getRetryAfterMs
timeoutn/atimeoutMs, messageisTimeoutError
network_errorn/aretryable, cause?, messageisNetworkError

Every kind also carries requestId; read it with getRequestId.

Common errors

You seeCauseFix
401 / 403 (auth_error) on sdk.usermissing/invalid Bearer JWTeoaLogin / register → Authorization: Bearer
400 Insufficient Creditsmanaged account unfundedtestnet auto-mint on managed address
400 Start time must be in the futurestartTime ≤ nowset strictly ahead
500 Pre-flight simulation failedon-chain create reverted (often short startTime)larger offset (~31 min public / ~5 min private)
500 Unable to place bet… after estimatestake < 5 unitsstake ≥ 5; pass tokenDecimals
400 Paid tournaments require clientTournamentIdstage-1 buy-in without vault depositapprove + vault deposit → create with creatorDepositTxHash
404 from getDuelByIdpassed database iduse duelId
403 GEO_BLOCKEDUSDC geo gateisGeoblockError; use credits
429 (rate_limited)builder-key budget exhaustedwait getRetryAfterMs(err); configure SDK rateLimit
sdk.onchain.* { success: false }on-chain helpers never throwbranch 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.

Builder API key

Security

  • Never hard-code private keys or Builder API keys
  • Markets sdk.user auth is Bearer JWT, not raw x-wallet-*
  • credits bypasses geo; usdc does not

Where to look next

NeedPage
Golden pathQuickstart
Method shortlistCookbook
Every SDK methodSDK API reference
JSON schemasOpenAPI specs
npm smoke testsTest the SDK

On this page