Challenge
Docs>Test and go live>Go-live checklist

Go-live checklist

Everything that has to be true before you swap sk_test_ for sk_live_, in the order it is worth checking.

There is one mistake on this page that is worse than all the others, so it goes first: a live key in a browser bundle. Anyone who opens devtools can then create attempts, report outcomes and move your players' money in your name, and a key that has been in a client bundle is public forever. Rotation is the only remedy and it is a remedy after the fact. Everything else here is a thing you fix; this is a thing you survive.

Going live is not a switch. It is approval, one environment variable, and a short list of behaviors your code has to already handle.

The checklist

  • Your program is approved.

    Approval is a manual review of your program by Challenge, not a form that returns instantly. It is what makes an sk_live_ key work at all: a live key presented for an unapproved program answers 401 with "This program is not approved for live use." Real players, real money and the live key arrive together, and the same review is what raises your integrity tier. See Programs and keys.

  • The live key is in your server environment config, and nowhere else.

    One variable changes. The host, the paths, the payloads, the headers and the error codes are the ones you already tested. Set CHALLENGE_API_KEY to the sk_live_ value in your server's environment, your serverless function's secret store or your CI secret store, and read it as process.env.CHALLENGE_API_KEY or Deno.env.get('CHALLENGE_API_KEY'). This is the moment the catastrophic mistake happens: a live key pasted into the wrong file is a live key on the internet, so put it where the test key already was and change nothing else.

  • No key appears in any client bundle, NEXT_PUBLIC_ variable, or repository.

    Grep the thing you are about to ship for sk_live_ and for sk_test_. A key must never be in page JavaScript, a minified bundle, a mobile or desktop build, a NEXT_PUBLIC_* or VITE_* variable, a committed .env, a screenshot or a support ticket. Only the program ID goes in the browser, and it is public by design. If a live key has ever been in a client bundle, treat it as public and rotate it before you launch.

  • You know your integrity tier ceiling.

    Your tier caps what a single prize may be: C is $100, B is $1,000, A is $10,000. Every program starts at C, and a tier is raised by review, never at signup. The ceiling is checked when a prize is paid rather than only when a Challenge is priced, so a prize over it answers challenge_not_available and names the limit. The same ceiling is what stops a Challenge ladder climbing, since each rung doubles the prize below it.

  • You know what the prize ramp will show your players.

    A Challenge calibrated from observed play, and a rung priced from the players who claimed the Challenge below it (calibration_source of observed or rung_derived), pays half its configured prize for the first 200 paid attempts while the bar is confirmed against real money. Attempt 201 is the first at the full prize, and the change is automatic. Show the prize field exactly as the API returns it, or prizeOffered from startAttempt(). Never hardcode the number you saw in the portal: during a ramp the ramp object carries { active, fullPrize, attemptsSeen, attemptsRequired }, and fullPrize is what it will become, not what to advertise now.

  • You know whether your Challenge is provisionally capped.

    A bar that came from a claim rather than from observed play is provisional: calibration_source of listened or partner_seeded may offer at most 20% of your tier ceiling until real attempts confirm it. Provisional and ramped are mutually exclusive, so a Challenge is on one list or the other and never both. The cap lifts on its own when the drift monitor sees the realised pass rate land inside its band. See Calibration.

  • Your webhook endpoint is registered and verifying signatures in production.

    Register the production URL in the Developer Portal and select the events you consume. Every delivery carries X-Webhook-Signature: sha256=<hmac>, computed over the raw request body, plus X-Webhook-Event and X-Webhook-Delivery-Id. Compute the HMAC over the bytes you received, before any JSON parsing or reserialisation, and compare in constant time. A handler that trusts an unverified payload is a handler anyone on the internet can settle attempts through. Answer 2xx within 5 seconds; a failed delivery is attempted 3 times in total, the retries due 1 minute and then 5 minutes after each failure. See Webhooks.

  • Duplicate deliveries are handled, and attempt.settled is re-fetched rather than reconstructed.

    There is no ordering guarantee and no exactly-once guarantee. Dedupe on X-Webhook-Delivery-Id, which is stable across every retry of the same delivery, and make your handler idempotent on top of that. When attempt.settled arrives, call GET /v1/attempts/{id} and trust that, rather than rebuilding your own copy of the settlement from the payload fields. The fetched attempt is the truth; the event is only the news that there is something to fetch.

  • The expiry refund path is exercised.

    An attempt with no outcome expires one hour after it was created, and the sweep that reaps it runs every five minutes. The fee is refunded in full and attempt.refunded fires with reason: "expired". Reporting a score after that point answers attempt_expired. This is the branch that only fires when your server is already having a bad day, so run it in sandbox on purpose before you need it: start an attempt, report nothing, and watch the event arrive.

  • Every failure has a branch.

    In the browser, a rejected startAttempt() carries a .code: not_armed, insufficient_funds, attempt_in_progress, challenge_not_available, blocked, not_initialised, unauthorized, client_reporting_removed, network, unknown. The island has already told the player whatever they need to know, so your job is not to draw a message, it is to not continue the round. On the server, every /v1 error is { "error": { "code", "message" } }, and the codes you branch on are the codes, never the message text. See Errors.

  • Your logs keep the identifiers that let you reconstruct one attempt.

    Log the attempt id on every call you make about it, the Idempotency-Key you sent, the receipt_id the outcome returned when you sent evidence with it, and the X-Webhook-Delivery-Id of every delivery you accepted or rejected. Challenge does not return a request id header today, so the attempt id is the join key between your logs, your webhook deliveries and anything support has to look up on our side. A settlement dispute that arrives a week later is answerable with those four values and not answerable without them.

After you are live

Calibration does not stop when a Challenge goes live, because populations do not stop moving. Every live Challenge is checked against its last 500 settled outcomes, with no judgement made below 30 of them, on two separate questions: whether the realised pass rate is inside the band around its target, and whether the expected payout has overtaken the fee.

Fail either and the Challenge is set to suspended immediately. New attempts answer challenge_not_available, and challenge.suspended fires with the realised rate, the target and the reason. Attempts already open keep the bar frozen at their creation, so nothing changes retroactively. The response is suspension rather than an alert because an alert needs someone awake and a circuit breaker does not.

Pass both and an unconfirmed bar is promoted to observed, which is what lifts a provisional ceiling with no request to make.

Three places to watch it from:

  • challenge.suspended and challenge.calibrated, if you want to know without looking. Subscribe to both; the first is the one that stops your revenue, the second is the one that starts it.
  • GET /v1/challenges?player_ref=...&status=all, which is the partner view. The default list is player-facing and shows live Challenges only, so a suspended one simply vanishes from it. status=all is how you see the status, the calibration_source and the ramp position.
  • The Developer Portal, which shows each Challenge's status, threshold and calibration progress on the program.

If a Challenge of yours is suspended, do not try to seed a new distribution over it. Seeding is refused on anything that is not calibrating, deliberately, because a new bar is not a way to reset the breaker that pulled it. Reinstatement is a review.

Related

  • Sandbox: the money-free loop, and what does and does not differ between the two environments
  • Programs and keys: approval, integrity tiers, rotation, and where each value may appear
  • Calibration: the provisional ceiling, the ramp and the drift monitor in full
  • Webhooks: handler, registration, signature verification, retries and debugging
  • Errors: every code with its cause and its fix

Source: https://www.withchallenge.com/docs/go-live.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.