BentoSDK

Create a market

Submit a new prediction or versus market on the Bento markets host.

By the end: you'll have submitted a new market and read it back from the catalog.

Prerequisites

  • Markets host URL (baseUrl)
  • Bearer JWT from eoaLogin on createBentoSdk (creator wallet)
  • Creator wallet authorized on createBentoSdk (creation submits an on-chain transaction via your managed wallet)

Flow

import { createBentoSdk, walletAuthProvider } from '@bento.fun/sdk';

const sdk = createBentoSdk({
  baseUrl: process.env.BENTO_URL!,
  // markets auth is a Bearer JWT from eoaLogin (see Authentication)
  auth: walletAuthProvider(() => ({ Authorization: `Bearer ${token}` })),
});

// 1. Create (HTTP 201: on-chain submission accepted)
// startTime must be at least 5 minutes ahead (the on-chain minimum). Public markets
// also need time to bootstrap (reach their liquidity threshold before startTime, or
// they are cancelled), so give them a wider window (~30 min). Private markets skip
// bootstrapping, so ~5 minutes is enough. A value near the floor can pass HTTP
// validation but revert on-chain; the managed wallet's pre-flight simulation surfaces
// that as a 500 ("Pre-flight simulation failed").
const start = Date.now() + 31 * 60_000; // public: bootstrap window; private: ~5 min
const result = await sdk.user.createDuel(
  {
    question: 'Will Team A win?',
    type: 'prediction', // or 'versus' with optionA / optionB
    category: 'Football', // one of: Cricket, Football, Basketball, American Football, Tennis, Baseball, Hockey, Formula 1
    description: 'Optional context for traders',
    // Optional fields you can also send:
    // optionA / optionB (required for versus), coverImageUrl, tags (e.g. ['Premier League'])
    startTime: new Date(start).toISOString(),
    endTime: new Date(start + 2 * 3600_000).toISOString(), // required; must be after startTime
    privacyAccess: 'public',
    collateralMode: 'usdc', // or 'credits'
    tags: ['Premier League'],
  },
  { requestId: `create-${Date.now()}` },
);

// `result.kind` is always 'accepted' when createDuel resolves; real failures THROW a
// BentoSdkErrorException: wrap createDuel in try/catch and inspect err.sdkError instead.

const { duelId, txHash } = result.raw;
console.log({ duelId, txHash });

// 2. Poll until the market appears in catalog (eventual consistency)
let detail;
for (let i = 0; i < 10; i++) {
  try {
    detail = await sdk.public.getDuelById({ duelId });
    break;
  } catch {
    await new Promise((r) => setTimeout(r, 2000));
  }
}

Use duelId from the response, not the id field from list rows. See Common patterns.

Private markets (invite → join → bet)

Private markets are hidden from public catalogs. Anyone with a valid invite can view the market; only members (or the creator) can bet. Membership is on-chain: the invitee must join before placeBet.

1. Create with privacyAccess: 'private'

Private markets skip the public bootstrap window, so startTime ~5–8 minutes ahead is enough (still at least 5 minutes for the on-chain floor):

const start = Date.now() + 8 * 60_000;
const created = await sdk.user.createDuel(
  {
    question: 'Private: will Team A win?',
    type: 'prediction',
    category: 'Football',
    startTime: new Date(start).toISOString(),
    endTime: new Date(start + 2 * 3600_000).toISOString(),
    privacyAccess: 'private',
    collateralMode: 'usdc',
  },
  { requestId: `private-${Date.now()}` },
);
const { duelId } = created.raw;

Creating a private duel does not auto-create an invite. The creator creates one next (the web app does this from the share modal).

2. Create an invite code (creator)

Invite codes are 4–32 alphanumeric characters (_, - allowed). The web UI uses a 10-character uppercase code:

const inviteCode = Math.random().toString(36).substring(2, 12).toUpperCase();

await sdk.user.duelInvitations.create({
  duelId,
  inviteCode,
  inviterAddress: managedAccountAddress, // creator's managed account
  expiresAt: 0, // never
  maxUses: 0, // unlimited
});
// Share: https://your.app/market/{duelId}?invite={inviteCode}

3. Invitee: view, join, then bet

// Optional pre-check (public)
await sdk.user.duelInvitations.validateInvite({
  inviteCode,
  userAddress: inviteeManagedAddress,
});

// View with invite (membership not required yet)
await sdk.public.getDuelById({
  duelId,
  inviteCode,
  userAddress: inviteeManagedAddress,
});

// Join — JWT must be the invitee's session (on-chain membership + DB)
await sdk.user.duelInvitations.userJoin({ inviteCode });

await sdk.user.duelInvitations.checkMembership(duelId, inviteeManagedAddress);

// Same bet flow as public markets (invitee must be funded — testnet faucet)
const stake = '10000000000000000000'; // 10 units
const est = await sdk.user.bets.estimateBuy({
  duelId,
  optionIndex: 0,
  betAmountUsdc: stake,
  slippageBps: 100,
});
await sdk.user.placeBetFromEstimate(
  {
    estimate: est.estimate,
    duelId,
    duelType: 'PREDICTION',
    bet: 'YES',
    optionIndex: 0,
    betAmount: stake,
    betAmountUsdc: stake,
    slippageBps: 100,
    tokenDecimals: 18,
  },
  { idempotencyKey },
);
StepWhoSDK
Create private duelCreatoruser.createDuel (privacyAccess: 'private')
Create inviteCreatoruser.duelInvitations.create
Validate (optional)Inviteeuser.duelInvitations.validateInvite
Join (required to bet)Inviteeuser.duelInvitations.userJoin
BetInvitee (member)estimateBuyplaceBet / placeBetFromEstimate

userJoin is the preferred join path (matches the web app). Legacy duelInvitations.join still exists but is not what the frontend uses.

Parent markets (grouped events)

For multi-market events (e.g. a match with several child markets):

const parentStart = Date.now() + 31 * 60_000; // public: bootstrap window (see above)
const parent = await sdk.user.createParentMarket(
  {
    parentQuestion: 'Match outcome?', category: 'Football',
    startTime: new Date(parentStart).toISOString(),
    endTime: new Date(parentStart + 2 * 3600_000).toISOString(),
    privacyAccess: 'private', // private parent: backend auto-creates an invite code
    markets: [/* ParentMarketChildDto[]: child question / type / options */],
  },
  { requestId: `parent-${Date.now()}` },
);

// Extra invites (creator), or use the code returned on private create:
await sdk.user.createParentMarketInvitation({
  parentMarketId: parent.raw.parentMarketId,
});

// Invitee joins the parent (propagates membership to private children):
await sdk.user.joinParentMarket({ inviteCode: 'PARENTCODE' });
// then placeBet on a child duelId

Request bodies are defined in the Markets OpenAPI schema (CreateDuelDto, parent-market DTOs).

After creation

TaskSDK call
Resolve outcomesdk.user.duels.resolve
Contest / disputesdk.user.duels.submitContest
Public readssdk.public.getDuelById({ duelId }) (private: pass inviteCode and/or userAddress)
Invite / joinsdk.user.duelInvitations.*

HTTP acceptance does not mean the chain has finalized. See Mutation semantics.

On this page