Mundial de Clicks uses Cap — a proof-of-work captcha — to make bot automation CPU-expensive. Unlike image or click-based captchas, Cap asks the browser to solve a short computational puzzle before votes are accepted. There is no third-party tracking, no image labelling, and no iframes: the widget runs entirely in a WebWorker with a small WASM binary.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/midudev/mundial-de-clicks/llms.txt
Use this file to discover all available pages before exploring further.
How it works
The app acts as a proxy between the browser and the Cap standalone server. The browser never talks directly to Cap — all challenge and redeem traffic goes through/api/captcha/* on the same origin, so there are no CORS issues and no mixed-content warnings.
Page load: check session validity
The browser calls If
GET /api/captcha/session. The server reads the cap_session cookie, checks both the quota key (cap:sess:{id}) and the IP-binding key (cap:sess-ip:{id}) in DragonFly, and returns:valid is true, the client proceeds to vote immediately.Request a challenge
If the session is not valid, the Cap widget calls
POST /api/captcha/challenge. The server:- Checks the per-IP abuse limit (
CAP_CHALLENGE_MAX_PER_MINUTE, default 6/min). - Reads the IP’s daily vote count from DragonFly to calculate the current difficulty.
- Proxies a
POSTto{CAP_API_URL}/challengewith{ challengeDifficulty: N }. - Returns Cap’s challenge payload directly to the widget.
Client solves the puzzle
The Cap widget solves the PoW puzzle in a WebWorker using WASM. This is the CPU-bound step that makes automation expensive. The browser does not need to do anything else during this time.
Redeem the solution
The widget calls
POST /api/captcha/redeem with the solution token. The server:- Checks the per-IP abuse limit (
CAP_REDEEM_MAX_PER_MINUTE, default 12/min). - Proxies the solution to
{CAP_API_URL}/redeem. - If Cap returns
{ success: true }, creates a session in DragonFly and sets twoSet-Cookieheaders:cap_session(the vote session) andvoter_id(the persistent voter identity).
Vote with the session cookie
Subsequent
POST /api/vote requests include the cap_session cookie automatically. The atomic Lua script in DragonFly verifies the session, checks the IP binding, and decrements the quota — all in one round-trip.Session mechanics
A verified captcha session is stored as two DragonFly keys:| Key | Value | Purpose |
|---|---|---|
cap:sess:{uuid} | Integer (remaining vote quota) | Tracks how many votes remain |
cap:sess-ip:{uuid} | IP string | Binds the session to its origin IP |
cap:sess-ip:{id} does not match the IP of the current request, the script returns -1 immediately and no votes are recorded. A stolen or shared cookie from a different IP is therefore useless.
TTL and renewal:
The effective session TTL is Math.max(30, CAP_SESSION_TTL_SECONDS) — the value is floored at 30 seconds so that a misconfigured or very small CAP_SESSION_TTL_SECONDS cannot create a session that expires almost immediately. With the default of 120 s the floor has no effect. Every time a vote is accepted, both keys have their TTL reset to this value via EXPIRE. A user who votes continuously will never see the captcha dialog again. A session that goes idle for longer than the TTL expires naturally.
Vote quota:
The quota is the lesser of CAP_VOTES_PER_SESSION and CAP_SESSION_HARD_VOTE_CAP (both default 50):
CAP_VOTES_PER_SESSION to a very large number does not accidentally grant unlimited votes from a single solved captcha.
When quota reaches 0 after a vote batch, both keys are deleted with DEL inside the same Lua transaction.
Progressive difficulty
Each challenge request reads the IP’s current daily vote count (the higher of the IP counter and thevoter_id counter) and calculates difficulty:
| Daily votes cast | Difficulty |
|---|---|
| 0 – 249 | 4 (base) |
| 250 – 499 | 5 |
| 500 – 749 | 6 |
| 750 – 999 | 7 |
| 1 000 + | 8 (max) |
Deploying Cap standalone
Cap requires its own server process. The app does not bundle a Cap server — it only proxies to one.Deploy Cap in Coolify
In Coolify, click + New → Service and search for the Cap standalone template. Coolify will create the service and assign it an internal hostname (e.g.
cap).Get your site key
After Cap starts, open its dashboard and create a site. Copy the site key (a short alphanumeric string).
Configure the app
Add the following environment variable to the Mundial de Clicks service in Coolify, replacing the hostname and site key:The app will strip trailing slashes and proxy to
{CAP_API_URL}/challenge and {CAP_API_URL}/redeem.The app has a hard timeout on server-to-server calls to Cap (
CAP_HTTP_TIMEOUT_MS, default 5000 ms). If Cap is unreachable or slow, the challenge and redeem endpoints return 502 rather than leaving the client connection hanging.Client-side integration
If you are building a custom frontend rather than using the default Astro pages:Install the widget
<cap-widget> custom element. It handles WebWorker creation, WASM loading, and the challenge/redeem calls automatically.Session cookie
On a successful redeem, the server sets
cap_session as HttpOnly; SameSite=Lax. The browser attaches it to every subsequent same-origin POST /api/vote request automatically — no manual header required.POST /api/vote returns captcha_required multiple times (e.g. the session expired mid-batch), the client applies exponential backoff before re-triggering the widget:
Client-side gamified challenges
Separate from the Cap PoW captcha, the client (scripts/main.ts + scripts/challenge.ts) presents occasional in-UI game challenges to add friction without interrupting the invisible PoW flow. These are purely client-side and do not involve the server.
Every 60 to 110 accepted votes (chosen at random each time) a full-screen overlay appears and the user must complete one of two mini-games before voting can continue:
| Challenge | Description |
|---|---|
| ⚽ Penalty kick | Choose left, centre, or right. The goalkeeper dives to a random zone — score to continue. Appears with 2× frequency. |
| 🚩 Guess the flag | Pick the correct country flag from three options. |
CHALLENGE_MIN = 60, CHALLENGE_MAX = 110) so it is less predictable than a fixed threshold. Pending votes are flushed before the overlay appears and resume automatically once the user solves the challenge.
These challenges are independent of hasCaptcha — they run even when the Cap PoW captcha is disabled.
Disabling captcha
hasCaptcha in features.ts is true only when both CAP_API_URL is set and DragonFly is configured (REDIS_URL or DRAGONFLY_HOST). Sessions are stored in DragonFly, so captcha cannot function without it. Leave CAP_API_URL unset (or leave DragonFly unconfigured) and hasCaptcha evaluates to false — the Lua vote script skips session validation entirely.