Kingshot Tracker

Jan-May 2026

Multi-tenant React and Supabase application to manage alliances in the Kingshot mobile game. ~25 users supporting 1,400 players. Auth, row-level security, edge functions, and OCR-powered score extraction. Supports mobile and desktop.

React 18TypeScriptSupabasePostgresRLSEdge FunctionsVercelPlaywright

A planning and coordination app built for four 100-person KingShot alliances.

How it’s built

  • Multi-tenant data model. Alliances, members, events, scores. Every query scoped through Postgres row-level security policies. Auth wired through Supabase with JWT claims.
  • Edge functions. Server-side logic for OCR score extraction (parse a screenshot, return structured event scores) and privileged operations that can’t run client-side.
  • End-to-end tests. Playwright suite covering the golden paths (auth, plan creation, event flow, score entry).
  • Deploy and observability. Vercel for frontend, Supabase for the database and authentication.

Joining an alliance

Each alliance has an invite code shared by R5 (alliance leader) or R4 (officer) accounts. Sign-in is email + 8-digit OTP through Supabase Auth; on first sign-in, a new user enters the invite code to attach to an alliance. RLS policies key off the resulting alliance_id and role claims, so a user only sees data for alliances they belong to and only mutates data their role permits.

Most active sign-ins come from a handful of alliance leaders doing planning, roster updates, and event score entry. The other ~390 alliance members never sign in - their game IDs get entered once by the leaders, and the cron-driven redeemer runs against them on every new code. The website is small; the system serves a much larger group.

Planning canvas

The Bear Trap event has alliances surround a fixed map location and call “rallies” to attack it. Players closest to the trap reach it fastest, so leaders position members in the “hive” around it based on event contribution. Most alliances plan this in a spreadsheet, but KingShot’s map is a diamond, not a square. The planning canvas is a React component with a custom SVG grid that supports drag-and-drop placement, and entities carry labels and attributes that make positioning legible at a glance.

Alliances with historical data can use the “auto-fill” feature to position players based on configurable weighted averages of their previous scores. Alliances have two traps on the map, so this also differentiates attendance to either event. For example, players who play both traps are placed in the middle of the traps.

OCR features

Event scores

The app extracts event scores from in-game screenshots. A player uploads a screenshot, an edge function runs OCR on it to extract scores.

Roster updates

Manually entering 100 players is tedious. Users upload screenshots of in-game roster pages, the app extracts names and stats (power level, city level) automatically, and the same flow updates existing players when their stats change.

Human-in-the-loop

The UI surfaces the parsed result for the user to reconcile before saving. The app flags suspicious results (e.g. players with negative scores) so the user can verify them before saving.

Promotional code redemption

KingShot occasionally releases promotional codes for in-game rewards. They go out across different channels (social media, newsletters), and there’s a community-maintained API at kingshot.net that aggregates active codes. An edge function polls that API every 15 minutes and queues new codes for redemption. The app tracks per-code status (active, expired) and per-player-per-code redemption status to prevent double-use.

Redemption calls the same signed endpoints KingShot’s web redemption page uses. The contract is a two-step flow: register the player against the gift-code service, wait for state to propagate, then submit the code. Each request is form-encoded with an MD5 signature derived from the payload plus a shared secret. KingShot’s API is undocumented, so the signature was grabbed from the Javascript files loaded by the browser.

The game server’s error model is half numeric, half string-coded, with non-obvious semantics. 40008 means “already redeemed.” RECHARGE_MONEY ERROR. means the player needs to make a real-money purchase before that code is eligible (permanent, no point retrying). STOVE_LV ERROR. looks permanent but isn’t - the server returns it intermittently for players who do meet the level requirement, so the right behavior is to retry on the next cron run rather than mark it dead. That distinction only shows up after watching the same player succeed on a code they “couldn’t” redeem the day before.

The redeemer runs on a Vercel cron with a 280-second deadline budget, a lease-based lock to prevent overlapping runs from clashing, bounded exponential backoff (3s/5s/8s) on transient HTTP failures, idempotent upserts against a redemptions table, and a 500ms inter-player pacing delay to stay under the game server’s rate limit. Redeeming a new code went from 400 manual actions to a scheduled job nobody has to think about.

Tech

React 18, TypeScript, Vite, Tailwind, shadcn/ui, Wouter, Zustand for grid state, React Query for server state, Supabase (Postgres, Auth, Edge Functions), Playwright, Vercel.