Challenge
Docs>Build>Install Challenge.js

Install Challenge.js

Two script tags, one call, and the island is on your page.

Runs in the browser

Challenge.js is distributed as script tags only. There is no npm package, for the browser or for the server, and there is nothing to bundle. The whole browser half of the integration is the two tags below plus one init() call.

The two script tags

Put both in the page that runs your game, in this order.

<script src="https://api.withchallenge.com/widget/dist/island-stub.js"></script> <script async src="https://api.withchallenge.com/widget/dist/island.js"></script>

The first is synchronous and tiny. The second is the rest of the SDK and loads out of the way.

What the stub does

The stub is on the critical path of someone else's game, so it does exactly three things and nothing else.

  1. It defines <challenge-island> as a custom element and paints the dormant pill into its shadow DOM immediately. Your page never shows a hole where the money surface is about to be.
  2. It queues init(). A call that lands before island.js has arrived is pushed onto Challenge._queue and replayed the moment the full bundle takes over, so Challenge.init(...) is safe to run from the first line of your boot code.
  3. It holds one early tap. If a player taps the pill before the bundle is live, the stub dims it and remembers a single tap, which is replayed once there is something to open. One tap, never a list: replaying a burst would run the arm flow several times over.

The stub makes no network request beyond its font, and it cannot know whether the player has anything armed, so it always paints the dormant costume and lets the bundle correct it.

If you want the pill to land in a specific corner before init() has run, name it on the stub's own tag:

<script src="https://api.withchallenge.com/widget/dist/island-stub.js" data-anchor="top-center"></script>

The anchor has to be spelled out. An empty data-anchor mounts nothing.

Initialise

Once, wherever your game boots:

Challenge.init({ program: '<<YOUR_PROGRAM_ID>>' })

program is the only required option. It is your program ID from the Developer Portal, it is public, and it belongs in the browser. Your sk_test_ and sk_live_ keys do not: see Programs and keys.

init() returns the Challenge object, so it chains. A call with no program, or with a non-string one, logs an error and mounts nothing.

Until your program has a live Challenge, the island draws a small dormant pill reading Coming soon. It offers nothing and charges nothing, but your players do see it, so decide deliberately whether to ship this line before a live Challenge exists.

The options you will actually set

The full option table is on the Challenge.js reference. These are the ones that come up in practice.

OptionTypeDefaultWhat it does
programstringrequiredYour program ID
anchorstring'bottom-right'An overlay corner, or a CSS selector for an inline mount
size'regular' or 'compact''regular'How large the pill draws
theme'light' or 'dark''light'The palette for every island surface
draggablebooleanfalseLets the player move the island, and remembers where they put it
challengestringnullPin the island to one Challenge id instead of letting it pick from your catalog
apiBasestringscript originLocal development only

Placement

Six names mount the island as a fixed overlay: bottom-right, bottom-left, bottom-center, top-right, top-left, top-center. Safe-area insets are handled, so the island clears a notch and a home indicator without you doing anything.

Anything else is read as a CSS selector, and the island renders inline inside your own layout:

Challenge.init({ program: '<<YOUR_PROGRAM_ID>>', anchor: '#challenge-slot', })

A selector that matches nothing falls back to the default overlay corner and says so loudly in the console. The island cannot simply not appear: while a player is armed it is the only thing on the page telling them money is at stake.

Which edge is pinned is which way the island grows. A top anchor expands downward, a bottom anchor expands upward, so an expanded island at any of the five corners is fully on screen by construction.

Theme

One choice reaches every surface: the dormant pill, the armed costumes, the arming card and the account panel. An unrecognised value renders light rather than erroring, because a theme is not worth refusing to render over.

dormantTheme is a deprecated alias of theme. It still works and logs one console warning per page; theme wins when both are given.

Pinning one Challenge

The option key is literally challenge, not challenge:

Challenge.init({ program: '<<YOUR_PROGRAM_ID>>', challenge: '<<YOUR_CHALLENGE_ID>>', })

Pinning is filtered against live Challenges only. If the id you named is not live, the island warns, falls back to the first Challenge that is, and keeps working. A partner with one Challenge in their game wants this; a partner with a catalog should leave it off and let the island offer the whole list.

Options that are gone

Passing any of apiKey, gameId, entryFee, mode, matchmaking, settlement, playerRating, position, variant, dashboardUrl, challengeDomain, ssoEnabled or ssoTimeout logs a warning and is ignored. They belong to the retired versus model. apiKey in particular must never appear in browser code at all.

Single-page apps

init() is safe to call on every route change, and that is the intended use.

  • init() never calls destroy(). That is a house rule, not an implementation detail. An implicit teardown inside init is how a re-render silently drops a player's state.
  • The <challenge-island> element is reused when anchor and draggable have not changed, so navigating inside your app does not blink the island out and back.
  • The player's armed session survives a re-init, and so does a paid attempt that is already running, because a route change is not the end of a round.
  • destroy() is explicit and total. It tears down the element, every listener and timer, the state machine, the settlement socket and its poll, any queued record() calls, and the remembered session. Call it when the game leaves the page for good, not on every navigation.

A complete React mount and unmount. Note that destroy() lives on the component that owns the game for the whole session, not on each route:

import { useEffect } from 'react' export function ChallengeProvider({ programId, children }) { useEffect(() => { // Safe on every route change. The element and the armed session are reused. window.Challenge.init({ program: programId, anchor: 'bottom-right', theme: 'dark', }) return () => { // Only on real unmount. destroy() also forgets the player's armed // session, so calling it between routes would disarm them silently. window.Challenge.destroy() } }, [programId]) return children }

With the two script tags in index.html:

<!doctype html> <html> <head> <script src="https://api.withchallenge.com/widget/dist/island-stub.js"></script> <script async src="https://api.withchallenge.com/widget/dist/island.js"></script> </head> <body> <div id="root"></div> <script type="module" src="/src/main.jsx"></script> </body> </html>

If you write <challenge-island> into your own markup rather than letting the overlay create one, an element that connects after init() has run is adopted and painted rather than replaced.

Local and self-hosted sandboxes

The API base is detected from the origin the script was loaded from, so a production page needs no configuration. Point it somewhere else only when the sandbox you are integrating against is a Challenge backend running somewhere other than api.withchallenge.com, such as a local one on port 4837.

Both script tags come from that same origin, and apiBase names it as well:

<script src="http://localhost:4837/widget/dist/island-stub.js"></script> <script async src="http://localhost:4837/widget/dist/island.js"></script>
Challenge.init({ program: '<<YOUR_PROGRAM_ID>>', apiBase: 'http://localhost:4837', })

Load the scripts and set apiBase together. A page that loads the bundle from one origin and talks to another is the one configuration the island cannot detect its way out of.

Everything else on that backend is at the same base, including these docs: a self-hosted or local backend serves every page at {your API base}/api/docs/<slug>.md, for example http://localhost:4837/api/docs/install.md. The agent index sits beside them under the same /api/docs path, named llms.txt.

Read Challenge.version in the console to confirm which bundle a page is running.

Next


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