Challenge
Docs>Build>Start a paid attempt

Start a paid attempt

One call turns an ordinary round into a paid one, and it is the only browser call that costs a player anything.

Runs in the browser

The player arms first

Before your code is involved at all, the player taps the island, sees the fee, the prize and the bar they would have to beat, and confirms. That confirmation is what arming means, and it is the whole of the transaction the player agreed to.

Challenge renders every part of it. You never draw a fee, a balance, a prize, a bar or a verdict. Sign-up, the age check and a deposit, if the player needs one, all happen inside the same flow on the same surface.

There is no API that arms on a player's behalf, on any key, and there will not be one. Agreeing to a price is the player's act, so it happens in front of numbers Challenge is quoting.

Your job starts the moment a round begins after that.

The call

const attempt = await Challenge.startAttempt() // { attemptId, feeCharged, prizeOffered, threshold, thresholdDirection }

Three things happen on the server before the promise resolves.

  • The fee is charged. Starting an attempt is the money event.
  • The bar and its direction are copied onto the attempt. A later recalibration cannot reach back and change what this player was playing for.
  • The prize on offer right now is copied onto the attempt. If the Challenge is inside its go-live ramp, that is the ramped prize, which is the number the player was shown.
  • The attempt is given an expiry one hour out.

It resolves only once the money has actually moved. While the request is in flight the island dims and stays armed and idle, because showing "round live" for a charge that then failed would be the island lying about someone's balance.

What it resolves with

FieldTypeMeaning
attemptIdstringThe id your server reports the outcome against
feeChargednumberWhat this player actually paid, frozen
prizeOfferednumberWhat this attempt pays on a win, frozen. 0 on a calibrating Challenge
thresholdnumber or nullThe bar this attempt is judged against. null when there is none
thresholdDirectionstring or null'higher_is_better' or 'lower_is_better'

These five are this attempt's terms. If you show the player a target during the round, read attempt.threshold and attempt.thresholdDirection and nothing else. Do not read the Challenge's current threshold from a list you fetched earlier: the two can differ, and only the frozen pair decides the outcome.

In place

async function playPaidRound() { let attempt try { attempt = await Challenge.startAttempt() } catch (error) { handleStartFailure(error) return } const moves = await runTheRound({ target: attempt.threshold, direction: attempt.thresholdDirection, }) await fetch('/round-finished', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ attemptId: attempt.attemptId, moves }), }) }

Send your server whatever it needs to derive the score. The point of that hop is that the number Challenge is paid against is worked out somewhere the player cannot edit. Report the score from your server is the other half.

Tournaments use the same call

A tournament round is entered by naming it. Nothing is armed for one, by design: the entry is the attempt, so the round id is what says which deal this round is under.

const attempt = await Challenge.startAttempt({ tournamentRound: '<<YOUR_ROUND_ID>>' }) // { attemptId, feeCharged, prizeOffered: 0, threshold: null, thresholdDirection }

prizeOffered is 0 and threshold is null, and neither is a bug. A tournament attempt is not judged against a bar and pays nothing on its own: it buys a place on a rank table, and the purse is paid by rank when the round settles. thresholdDirection is the tournament's own direction, and it tells you which end of the table is winning. Everything after the call is identical, settlement included.

A player who armed a tournament round on the island enters it with the plain startAttempt() they already shipped. The explicit form is for a game whose own server picks which round a player is in.

See Tournaments.

Branching on failure

A rejection is an Error with a .code. Branch on the code, never on the message: the wording is not part of the contract and gets reworded.

The island has already told the player whatever they need to know, including a refusal, so most branches are a log line and a return. Your job is to not start the round.

function handleStartFailure(error) { switch (error.code) { case 'not_armed': // The player has not agreed to a Challenge. Run the round for free. playFreeRound() return case 'insufficient_funds': // Nothing was charged. The island is already offering a deposit. return case 'blocked': // A responsible gaming limit the player set for themselves. The island // shows their own words. Never work around this. return case 'challenge_not_available': // Suspended, closed, or the island and the server disagree about what is // armed. The island resyncs itself. Offer a free round instead. playFreeRound() return case 'attempt_in_progress': // A paid round is already open and has not settled. Do not start another. console.warn('Challenge: an attempt is already running') return case 'not_initialised': // init() has not run, or the island was destroyed while this was in // flight. Fix the mount order. console.error('Challenge: init() must run before startAttempt()') return case 'unauthorized': // The player's session expired. The island puts a sign-in in front of // them. Nothing for your code to do. return case 'network': // The request never reached Challenge, after its own retries. Nothing // was charged. The player can tap and try again. return case 'client_reporting_removed': // Only ever thrown by the removed Challenge.reportScore(). Delete that // call: scores enter the money system from your server, on /v1. console.error('Challenge: scores are reported by your server, not the browser') return default: // 'unknown', and anything else the server may add later. console.warn('Challenge: attempt not started:', error.code) playFreeRound() } }

The code table

CodeCauseWhat to do
not_initialisedinit() has not run, or the island was destroyed or re-initialised while the call was in flightCall init() before startAttempt(). Do not start attempts during teardown
not_armedNothing is armed and no tournamentRound was namedNot an error. Run the round for free
attempt_in_progressA previous attempt has not settled yetOne at a time. Wait for settlement or the one-hour expiry
client_reporting_removedChallenge.reportScore() was called. It never comes from startAttempt()Delete the call. Report from your server instead
networkThe request never reached the server, after its retries. Nothing was chargedNothing. The player can retry by playing again
unauthorizedThe player's session is not validNothing. The island handles sign-in
insufficient_fundsThe balance does not cover the fee. Nothing was chargedNothing. The island offers a deposit
challenge_not_availableThe Challenge is suspended or closed, its prize is over your tier ceiling, or the server has nothing armed for this playerDo not run a paid round. The island resyncs itself
blockedA responsible gaming break or self-exclusion on the account, or a state where the player may not play this Challenge for moneyNothing, ever. Do not offer an alternative paid path
unknownAnything unrecognisedLog it and fall back to a free round

Server-side conflict codes pass through unchanged, so a few more can reach you on a Challenge that a player has a history with: challenge_already_claimed (a Challenge pays one prize per player, and that refusal is permanent for the pair), rung_locked (a ladder rung whose parent this player has not claimed), attempt_cap_reached (ten paid attempts on one Challenge in a day, or a tournament round's own cap), eligibility_incomplete (the account is missing something a paid round requires) and, on a tournament round, entries_closed.

Treat any code you do not recognise the way you treat unknown. The list of codes only ever grows, and the default branch is what keeps a new one from breaking your game.

If nothing is ever reported

An attempt expires one hour after it was created. Challenge voids it, refunds the fee in full, tells the player's client and fires the attempt.refunded webhook with reason: "expired". The fee buys one adjudication against a fixed bar, so an adjudication that never happened is a fee that was never earned.

Next


Source: https://www.withchallenge.com/docs/start-attempt.md Docs index for AI agents: https://www.withchallenge.com/llms.txt

Ready to integrate?

Put a Bounty in your game in minutes.

Challenge for Players

Create an account, deposit funds, and compete for real money in games you already play.

Challenge for Developers

Add Challenge to your game and start earning from every match. Integration takes minutes.