---
title: "Start a paid attempt"
description: "Arming on the island, await Challenge.startAttempt(), the five frozen fields it resolves, the tournamentRound variant, and a complete error table for every code it can reject with."
url: https://www.withchallenge.com/docs/start-attempt
canonical_markdown: https://www.withchallenge.com/docs/start-attempt.md
docs_index: https://www.withchallenge.com/llms.txt
api_version: v1
lastUpdated: 2026-09-22T16:56:18.000Z
---

# 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

```javascript
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

| Field | Type | Meaning |
|---|---|---|
| `attemptId` | string | The id your server reports the outcome against |
| `feeCharged` | number | What this player actually paid, frozen |
| `prizeOffered` | number | What this attempt pays on a win, frozen. `0` on a calibrating Challenge |
| `threshold` | number or null | The bar this attempt is judged against. `null` when there is none |
| `thresholdDirection` | string 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

```javascript
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](https://www.withchallenge.com/docs/report-score.md) 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.

```javascript
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](https://www.withchallenge.com/docs/tournaments.md).

## 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.

```javascript
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

| Code | Cause | What to do |
|---|---|---|
| `not_initialised` | `init()` has not run, or the island was destroyed or re-initialised while the call was in flight | Call `init()` before `startAttempt()`. Do not start attempts during teardown |
| `not_armed` | Nothing is armed and no `tournamentRound` was named | Not an error. Run the round for free |
| `attempt_in_progress` | A previous attempt has not settled yet | One at a time. Wait for settlement or the one-hour expiry |
| `client_reporting_removed` | `Challenge.reportScore()` was called. It never comes from `startAttempt()` | Delete the call. Report from your server instead |
| `network` | The request never reached the server, after its retries. Nothing was charged | Nothing. The player can retry by playing again |
| `unauthorized` | The player's session is not valid | Nothing. The island handles sign-in |
| `insufficient_funds` | The balance does not cover the fee. Nothing was charged | Nothing. The island offers a deposit |
| `challenge_not_available` | The Challenge is suspended or closed, its prize is over your tier ceiling, or the server has nothing armed for this player | Do not run a paid round. The island resyncs itself |
| `blocked` | A responsible gaming break or self-exclusion on the account, or a state where the player may not play this Challenge for money | Nothing, ever. Do not offer an alternative paid path |
| `unknown` | Anything unrecognised | Log 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

- [Report the score from your server](https://www.withchallenge.com/docs/report-score.md) is the request that settles the attempt you just started.
- [Real-time settlement](https://www.withchallenge.com/docs/realtime.md) explains why nothing is relayed back to your game and you never draw the result.
- [How a round works](https://www.withchallenge.com/docs/how-a-round-works.md) tells the whole sequence once, in order.

---

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