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
eoaLoginoncreateBentoSdk(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 },
);| Step | Who | SDK |
|---|---|---|
| Create private duel | Creator | user.createDuel (privacyAccess: 'private') |
| Create invite | Creator | user.duelInvitations.create |
| Validate (optional) | Invitee | user.duelInvitations.validateInvite |
| Join (required to bet) | Invitee | user.duelInvitations.userJoin |
| Bet | Invitee (member) | estimateBuy → placeBet / 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 duelIdRequest bodies are defined in the Markets OpenAPI schema (CreateDuelDto, parent-market DTOs).
After creation
| Task | SDK call |
|---|---|
| Resolve outcome | sdk.user.duels.resolve |
| Contest / dispute | sdk.user.duels.submitContest |
| Public reads | sdk.public.getDuelById({ duelId }) (private: pass inviteCode and/or userAddress) |
| Invite / join | sdk.user.duelInvitations.* |
HTTP acceptance does not mean the chain has finalized. See Mutation semantics.
Related
- Place a bet: trade on an existing market (private: join first)
- Money: testnet faucet before betting
- SDK API reference:
sdk.user.duels.createDuel,sdk.user.duelInvitations - Markets OpenAPI: full request/response schemas