Opens every available card pack on wiki-masters.com on a repeating schedule, unattended.
The site is a Next.js app on Vercel with Supabase auth. The bot:
- holds a Supabase session (from a cookie you paste, or from a browser login);
- refreshes the access token when it expires — they last 60 minutes;
- reads the account profile to see whether there is anything to do (read-only);
POSTs/api/packs/openuntilpacks_remainingis 0, 3–4 s apart. Each open yields five cards.
No browser is launched on a normal run — it is a handful of plain HTTP calls, which is why an hourly cadence costs nothing.
The login form carries a Cloudflare Turnstile checkbox that is painful to automate. Handing the bot a session you already have avoids it entirely.
In the browser where you are logged in, open the devtools console and run:
document.cookieCopy the whole output into cookie.txt, then:
npm ci && npm run build
cp .env.example .env # nothing required in it for the cookie flow
npm run import-cookie -- --file cookie.txt
npm run probe # confirms the session; opens no packs
npm run run:once # drains the queueimport-cookie also reads from stdin (pbpaste | npm run import-cookie) or from
WM_COOKIE, which is how you feed a container without a shell.
You do not need to re-paste this every hour. The cookie carries a refresh token, and the bot mints a new 60-minute access token each run, persisting the rotated one.
Pack opening requires periodic human verification. The account profile carries
pack_human_verified_at, and the site gates opening behind it — observed holding for
roughly 12 hours. When it lapses, /api/packs/open returns a verification error, the bot
stops with a clear message, and you need to open one pack by hand in a browser and
re-import the cookie. The cookie flow defers this captcha; it does not remove it.
The site tracks and sanctions automation. The profile exposes cheat_strikes,
last_sanction_type and activity_blocked_until. The bot refuses to run against an
account showing any of these (override with WM_ALLOW_WHEN_STRUCK=true), but it cannot
stop you from earning one. Running this may get the account struck or blocked.
| Command | What it does |
|---|---|
npm run import-cookie |
Store a session cookie from your browser. Opens no packs. |
npm run probe |
Verify the session and print the pack count. Opens no packs. |
npm run run:once |
Open every available pack once, then exit. |
npm start |
Daemon: run now, then on a jittered interval. The container's default. |
npm run login |
Password + Turnstile login. Needs WM_EMAIL/WM_PASSWORD. |
Exit codes: 0 success, no-op, skipped or in backoff · 1 configuration ·
2 auth, session or verification problem · 3 API failure or stopped mid-drain.
Packs refill on a rolling hour, so a new pack is never more than an hour old, and a run
that finds nothing is a cheap no-op — checking often costs little. The daemon rolls a
fresh interval from WM_INTERVAL_MIN_MINUTES–WM_INTERVAL_MAX_MINUTES every cycle
rather than reusing one fixed period, so the cadence doesn't read as machine-generated
the way a metronomic "exactly every N minutes" would. (It used to be a single fixed
interval — 61 minutes, then 30 — before moving to a jittered range.) Cron cannot express
an interval anchored to the run start or jitter its own period, so the daemon schedules
itself: each wait is anchored to when the run started, so the period does not drift,
and is recomputed from the range on every iteration.
mkdir -p data && sudo chown -R 1000:1000 data # the container runs as uid 1000
cp .env.example .env # set WM_COOKIE
docker compose up --build -d
docker compose logs -fThe ./data volume holds the session, so restarts do not lose it. With WM_COOKIE set
the container imports on first boot and never launches a browser.
All environment variables; .env is loaded automatically and .env.example documents
every one. The ones that matter:
| Variable | Default | Meaning |
|---|---|---|
WM_COOKIE |
unset | Session cookie; imported on first boot |
WM_EMAIL / WM_PASSWORD |
unset | Only for login mode |
WM_INTERVAL_MIN_MINUTES / _MAX_MINUTES |
10 / 15 |
Daemon period, re-rolled each cycle |
WM_OPEN_DELAY_MIN_MS / _MAX_MS |
5000 / 10000 |
Random gap between opens |
WM_REQUEST_TIMEOUT_MS |
180000 |
Per request |
WM_MAX_PACKS |
200 |
Iteration cap |
WM_RUN_BUDGET_MS |
420000 |
Wall clock per run; must be under the shortest interval |
WM_STALL_LIMIT |
3 |
Give up if packs_remaining stops falling |
WM_PREFLIGHT |
true |
Check the profile before opening anything |
WM_ALLOW_WHEN_STRUCK |
false |
Run even if the account is flagged |
WM_TOKEN_SKEW_MS |
120000 |
Refresh this far before expiry |
WM_NOTIFY_WEBHOOK_URL |
unset | Alert on anything needing a human |
The Supabase URL and anon key are defaulted in src/config.ts. The anon key is public by
design — shipped to every visitor's browser — and is overridable.
no Supabase auth cookie found — the paste is missing the sb-<project>-auth-token
entries. Copy the entire document.cookie output; it is ~3.4 KB and usually splits into
.0 and .1.
refresh failed ... refresh token is probably spent — the chain is broken (the cookie
was used elsewhere, or you logged out). Re-import a fresh cookie.
the site wants a fresh human verification — expected periodically. Open a pack by
hand in a browser, then re-import.
the account shows cheat_strikes=N — the site has flagged the account. Deliberate
stop; think before overriding.
cannot find packs_remaining in the response — the payload changed shape.
readRemaining() in src/packs.ts is the only place to adjust.
another run holds the lock — a previous run is still going. Expected, not an error.
A network timeout to the site now shows as a short, clean retry log line
(kind: retryable), not a crash. It used to be possible for a raw connection
error to propagate uncaught — and Playwright's own error formatting for a
failed request embeds every header of that request, cookie included, in the
error message. A crash like that during development printed a live session
straight to a terminal. Every place that talks to the site (probeSession,
postJson, supabaseCall) now catches network failures and keeps only the
first line of the error — the human-readable part, never the header dump —
before it can reach a log line, a notification, or a terminal. safeErrorMessage
in src/util.ts is the one place this happens; use it at any new call site
that might see a raw error from a live request.
Automating an account — and especially automating around a bot check — may breach wiki-masters.com's terms, and the site has explicit machinery for penalising it. The conservative delays, caps, stall detection and the pre-flight keep the traffic modest and well-behaved, but the decision to run this is yours.