Record rounds
One line at the end of every round your game already has, from the day you install Challenge.js.
Runs in the browser
Challenge.record('moves', 34)Paid or free. Signed in or not. Practice included. There is no condition under which you should skip it, and it charges nothing, blocks nothing and renders nothing.
Ship this before you have a Challenge, before you have a server handler, before you have anything to charge for. It is the one part of the integration that gets better the earlier it lands.
Why it goes first
A Challenge's bar is measured, never chosen. A bar you picked would set the pass rate, which would set what Challenge expects to pay, so Challenge insists on measuring it from real rounds. The question is only where those rounds come from.
Recorded rounds are the answer that costs nobody anything. Your game reports what an ordinary round looks like while nothing is at stake, and when you later create a Challenge on a metric that already has a sample behind it, the bar is computed and the Challenge is live in the same request. No player ever meets it sitting in calibration unable to pay anyone.
A metric qualifies when it has 5,000 recorded rounds from at least 200 distinct player refs. Below either target nothing bad happens: the Challenge is created calibrating and collects its sample the slower way instead. Calibration covers all three paths to a bar.
The Developer Portal reports listening progress per metric, and beside it the metric key each of your Challenges is waiting on.
What it does not do
Worth being exact about, because the name invites the wrong assumption.
- Recorded rounds do not advance a calibrating Challenge's own counter. That counter is adjudicated attempts, and it is reported as
calibration.collectedonGET /v1/challenges?status=all. - Recorded rounds are never merged into a later recalibration. They place a bar once, at creation, and only at creation. A recalibration has to be able to remove one player's contribution whole, and a recorded round is keyed on a device ref that cannot always be attributed to an account.
- Recorded rounds do not feed the drift monitor. Drift is judged on settled outcomes from paid attempts, which is the only population that says what a bar is actually costing.
Keep the line in forever anyway. Every new metric you add, and every Challenge you create on one, is priced from the sample this call is building.
The contract
record() lives inside somebody else's game loop, which is the reason for every rule below.
It never rejects. A listener that throws is a listener that ends someone's round. It resolves { recorded: true } or { recorded: false }, and never anything else. A bad argument warns at the developer, a dead network writes one console.debug, and both resolve { recorded: false }.
It never floods. At most one request per second per metric. Rapid calls are not dropped and not batched into one body: they queue in that metric's own lane and go out in order, a second apart, so a game that finishes ten rounds in a burst still reports ten. The clock starts when a request goes out, not when it comes back, so a slow response cannot make the next round wait twice.
It never waits on the island. Calls made before init() has finished its bootstrap are held and flushed as soon as there is a program to file them under, including when the bootstrap itself failed. A program id is all a recorded round needs.
Limits, published
| Limit | Value | What happens at it |
|---|---|---|
| Throttle | 1 request per second per metric | Calls queue in order. Nothing is dropped |
| Queue depth | 50 rounds per metric | Over it, the round is dropped and a console.warn names the metric |
| Metric name length | 40 characters | Longer names are refused in the browser and resolve { recorded: false } |
player_ref length | 64 characters | Enforced by the server |
| Per device, per metric | 300 scores per rolling 24 hours | The call still answers success. The score is silently not stored |
The 50-round queue is only reachable by a game reporting far faster than it plays, which is an integration bug rather than a very quick player. That is why the overflow is dropped and said out loud instead of queued into a drain that would run for minutes.
The 300-per-day bound is a limit on junk from one device, not a security control. It answers success because a listener that can fail is a listener a partner eventually removes. At the 5,000-round target across 200 refs the average ref contributes 25 rounds, so the cap only binds on something automated.
After Challenge.destroy(), anything still queued resolves { recorded: false } rather than hanging.
Metric names
Pass your game's own metric name. Not the example's, not a generic one. It has to be the same string as the metric_name on the Challenge you eventually create, because that string is the key the sample is stored under.
Names are trimmed and lowercased before they are sent, so 'Moves', 'moves ' and 'moves' are one metric rather than three quarter-full ones. The server additionally collapses runs of internal whitespace, so 'combo length' and 'combo length' are also one.
A mismatched name fails silently. There is no error for recording under a metric no Challenge uses. record() resolves { recorded: true }, the server answers 202, and the sample accumulates under a key nothing is reading. The listening progress you are building goes to the wrong metric, and the Challenge waiting on the right one never sees a round. Nothing in the response, the console or the network tab tells you.
So verify it rather than trusting it. Fetch GET /v1/challenges?status=all with your server key, read the metric_name field off the Challenge, and compare it character for character, after trimming and lowercasing both sides, with the string your record() call passes. The Developer Portal shows the same key per Challenge if you would rather read it there.
The name identifies what is measured, not what it is measured in. Two Challenges on one program can share a unit while measuring unrelated things, so the unit is never the key.
// Good: the game's own vocabulary, one metric per quantity.
Challenge.record('moves', movesUsed)
Challenge.record('solve time', secondsElapsed)
// Wrong: a unit is not a metric. Two unrelated quantities would pool.
Challenge.record('seconds', secondsElapsed)The score must be a finite number. Anything else warns and resolves { recorded: false }.
In place
async function playRound() {
const result = await runTheRound()
// Every round. Not awaited, not branched on, not conditional on a
// Challenge existing or a player being signed in.
Challenge.record('moves', result.moves)
showRoundSummary(result)
}If you want to confirm it is landing, record() resolves:
const { recorded } = await Challenge.record('moves', 34)
console.log(recorded) // trueOn the wire this is one POST /api/island/record carrying the program, the metric, the score and a per-device ref, answered 202 with { "recorded": true }. No session is required. When a player happens to be signed in the round is stored as practice and carries their account; signed out it is stored anonymously. A client cannot name its own source, because a client that could would claim the stronger one.
Next
- Calibration explains what the sample is turned into, and the two other ways a Challenge can get a bar if you do not want to wait.
- Start a paid attempt is the other browser call, and the only one that costs a player anything.
- Challenges covers the status machine a new Challenge walks through.
Source: https://www.withchallenge.com/docs/record-rounds.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.