LMArena uses reCAPTCHA Enterprise (v3 and v2) to gate chat submissions and Cloudflare Turnstile to protect its anonymous signup flow. The bridge handles all of this automatically through its browser transports.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/cloudwaddie/lmarenabridge/llms.txt
Use this file to discover all available pages before exploring further.
Challenge types
| Challenge | Where it appears | How the bridge solves it |
|---|---|---|
| reCAPTCHA v3 | Every chat submission | Mints a token via grecaptcha.enterprise.execute() in a live browser page |
| reCAPTCHA v2 (invisible) | When v3 score is rejected | Renders an invisible v2 widget and mints a token via grecaptcha.enterprise.render() |
| Cloudflare Turnstile | Anonymous signup (/nextjs-api/sign-up) | Renders a Turnstile widget, polls for token, then calls signup |
| Cloudflare “Just a moment” | Initial page load | Calls click_turnstile() and waits for the challenge to clear |
reCAPTCHA sitekeys
| Constant | Value | Purpose |
|---|---|---|
RECAPTCHA_SITEKEY | 6Led_uYrAAAAAIP_9E8Ais_67Z6Vp4vdf40p8SQU | Default v3 sitekey for chat_submit action |
RECAPTCHA_V2_SITEKEY | 6Ld7ePYrAAAAAB34ovoFoDau1fqCJ6IyOjFEQaMn | v2 sitekey used when v3 scoring fails |
TURNSTILE_SITEKEY | 0x4AAAAAAA65vWDmG-O_lPtT | Cloudflare Turnstile for anonymous signup |
RECAPTCHA_ACTION | chat_submit | Default action name sent with every v3 token |
The sitekey and action are re-discovered dynamically on startup and every 30 minutes via
get_initial_data(). The constants above are the fallback defaults used when scraping fails. If LMArena rotates its sitekey, the bridge will pick up the new value automatically after the next refresh.Token caching
reCAPTCHA v3 tokens are valid for approximately 2 minutes. The bridge caches the most recently minted token and reuses it across requests to avoid launching a browser session for every call.| Constant | Value | Description |
|---|---|---|
RECAPTCHA_TOKEN_EXPIRY_SECONDS | 115 s | Cached token lifetime (5 s safety margin before the 120 s Google limit) |
RECAPTCHA_V3_TOKEN_LIFETIME_SECONDS | 115 s | Same value, used in the minting path |
get_cached_recaptcha_token(). When it is expired or missing, the bridge mints a new one via refresh_recaptcha_token().
Startup discovery
On startup, the bridge callsget_initial_data(), which launches a Camoufox browser session to:
Scrape LMArena JavaScript
Parses the LMArena page JavaScript to extract the current reCAPTCHA sitekey and action using regex patterns. Falls back to the constants above if scraping fails.
Discover the Supabase anon key
Finds the Supabase JWT embedded in the page bundle. Used for auth token refresh via Supabase.
Fetch models and action IDs
Retrieves the available model list and LMArena arena action identifiers.
periodic_refresh_task() repeats this cycle every PERIODIC_REFRESH_INTERVAL_SECONDS (1800 s / 30 minutes).
Token flow per request
When the Chrome or Camoufox transport sends a request:- The transport mints a fresh reCAPTCHA v3 token using
grecaptcha.enterprise.execute(sitekey, { action }). - The token is added to the request payload as
recaptchaV3Tokenand to the request headers asX-Recaptcha-Token/X-Recaptcha-Action. - If LMArena returns
403with{"error": "recaptcha validation failed"}, the transport retries up tomax_recaptcha_attempts(default: 3) times. - On the first retry, the transport attempts to mint a reCAPTCHA v2 invisible token and substitutes it for the v3 token.
- If all attempts fail, the bridge returns the error response to the caller and increments the transport failure counter.
Cloudflare cf_clearance cookie
The cf_clearance cookie proves that a browser has solved a Cloudflare challenge. If you have a valid one from a recent manual browser session, you can set it in config.json to improve reliability:
config.json
The bridge prefers cookies already present in the persistent Chrome profile over values from
config.json, to avoid overwriting a valid Cloudflare cookie with a stale one from a different browser fingerprint. The config.json value is only injected when the profile does not already have one.