Project Report — April 2026

Tock Reservation
Sniper Bot

An automated system that monitors Fuhuihua (馥薈華) on Tock for reservation releases and books a table in seconds — before they sell out.

Repository charlieyang1557/tock-reservation-bot
Stack Python · Playwright · asyncio
Runtime Mac Mini · PM2 · 24/7
Built with Claude Code
Project Overview
13
Python source files
4.8k
Lines of code
80
Unit tests passing
~3s
Sniper poll cycle
Architecture
🌐

Browser Engine

Playwright with stealth patches. Handles Cloudflare Turnstile, maintains session cookies, supports headed/headless modes.

src/browser.py
🔍

Availability Checker

Multi-selector fallback system tries 6 different DOM selectors. Clicks calendar days regardless of CSS class, extracts time from parent text.

src/checker.py

Sniper Mode

Two-phase sniper: Phase 1 (first 60s) is a lightweight no-op skip — slots haven't dropped yet. Phase 2 fires concurrent calendar scans with abort-on-first-slot. Page reuse across polls. Session pre-warm 15 min before window.

src/monitor.py
🏁

Concurrent Booker

Fires asyncio tasks for each available slot simultaneously. First successful booking sets an Event flag — all others abort instantly.

src/booker.py
🔔

Discord Notifications

Color-coded webhook embeds: green (confirmed), yellow (slots found), orange (sniper active), red (errors), blue (dry-run).

src/notifier.py
📊

Release Tracker

Logs every detected slot to JSON + CSV with timestamp, date, time, and day of week. Used to reverse-engineer the release schedule.

src/tracker.py
🎯

Sniper Mode — Booking Flow

🔥
Pre-warm
T-15 min
🎯
Sniper On
7:59 PM
🔄
Rapid Poll
Every 3s
📅
Click Days
Fri/Sat/Sun
🔎
6 Selectors
Fallback chain
🏁
Race Book
Concurrent
Confirm
+ CVC fill
🔔
Discord
Alert sent
Build & Fix Timeline
Mar 10, 2026
Initial Build — 14 files, 2,400 lines
Full project scaffolded by Claude Code in one 17-minute session. Login flow, availability checker, concurrent booker, Discord notifications, slot tracker, and sniper mode all built from a single detailed prompt.
Build
Mar 10–20
Selector Verification & Stealth Fixes
playwright-stealth v2 API migration (stealth_async → Stealth().apply_stealth_async). Login timeout extended to 120s for manual Cloudflare CAPTCHA. DOM selectors validated against live Tock site — several were outdated from GitHub reference bots.
Fix
Mar 20
Sniper Slot Count Bug
Notifier displayed "239 slots detected" but checker found 0. Root cause: monitor.py passed poll_count (cumulative poll number) as the slots_found argument to sniper_mode_ended(). Added dedicated _sniper_slots_found counter.
Fix
Mar 27
Release Schedule Confirmed
Debug screenshot captured the key text: "New reservations will be released on March 27, 2026 at 8:00 PM PDT." Confirmed Friday 8PM release schedule. Sniper windows consolidated.
Discovery
Mar 30
Checker Selector Mismatch
Real Tock UI uses button:has-text("Book") with hashed CSS classes (css-dr2rn7) — not button.Consumer-resultsListItem.is-available. Ported multi-selector fallback from --test-booking-flow into checker. Now tries 6 selectors in order.
Critical Fix
Mar 30
_click_day Selector Fix
_click_day used available_day_button (requires is-available CSS class). Fuhuihua never has this class — days show is-sold or is-disabled. Changed to all_day_button — clicks by day number regardless of class.
Critical Fix
Apr 3, 20:00
First Live Detection — 4 Slots Found!
Bot successfully detected 4 slots (2× Saturday Apr 11, 2× Sunday Apr 12) the moment Fuhuihua released them. The multi-selector fallback and day-clicking fixes worked perfectly in production.
Milestone
Apr 3, 20:01
Booker Failed — Still Using Old Selector
Checker was fixed but booker.py still used available_day_button for its own day-clicking. Detected slots but couldn't navigate to them for booking. Also: sniper fired at 20:01 instead of 19:59 due to int() truncation of sub-second timing delta.
Critical Fix
Apr 3
Three-Fix Deploy
1) Booker selector: available_day_button → all_day_button + multi-selector fallback. 2) Sniper timing: int() → float precision with absolute-time scheduling. 3) Discord token moved from start-claude.sh to .env.
Fix
Apr 3
PM2 + Chromium Crash Loop
Playwright's headless Chromium failed with MachPortRendezvousServer error under PM2. Root cause: PM2 God Daemon entered bad state after 30+ crash-restart cycles. Fixed by pm2 kill to recreate the daemon entirely.
Infra Fix
Apr 4
Adversarial Review — Security Hardening
Codex adversarial review flagged two [high] trust-boundary issues: debug screenshots (page_debug*.png, test_booking_flow.png) capturing authenticated Tock UI state unignored in repo root, and .claude/ local config with machine paths and permission allowlists. Both gitignored. Live Playwright dry-run test confirmed full booking flow works end-to-end with new selectors.
Security Fix
Apr 10
Speed & Logic Overhaul — 7 Improvements
Seven targeted improvements shipped in one session: (1) Adaptive concurrent/sequential switching gated on sniper_age ≥ 60s so Phase 1 never toggles mode. (2) abort_event mechanism — first slot found instantly cancels all remaining concurrent date scans. (3) Generic Book button guard checks parent element text before clicking to prevent booking wrong slot. (4) _wait_for_checkout() rewritten as 2s-polling 30s loop instead of fragile single-selector wait. (5) Optional debug screenshots capture checkout state on booking timeout. (6) Discord notifications suppressed during sniper window to eliminate false-alarm pings when slots are found but not yet booked. (7) --test-sniper-phases test mode added to drive and validate two-phase sniper behavior. Unit tests grew from 39 → 80.
Build
Apr 11
Phase 1 No-Op & Saved Card Detection Fixes
Live integration tests caught two bugs: (1) --test-sniper-phases never set monitor._sniper_active, so keep_pages stayed False throughout all polls and the Phase 1 early-return never fired — all polls did full calendar scans. Fixed by setting _sniper_active and _sniper_concurrent before each poll call. (2) _has_saved_card() returned False despite a card being on the Tock account because the saved_payment_card selector didn't match the live UI. Fixed with CVC-presence fallback — Tock only renders the CVC re-entry field when a saved card is on file, confirmed by --test-booking-flow.
Fix
Bugs Found & Fixed
Severity Bug Root Cause Status
Critical Booker can't click days booker.py used available_day_button (requires is-available class), Fuhuihua never has this class ✓ Fixed
Critical Checker finds 0 slots on live site Only tried Consumer-resultsListItem.is-available selector. Real UI uses hashed CSS classes with "Book" text buttons ✓ Fixed
Critical Sniper fires 2 min late int() truncation: 0.5s delta → 0 → hold check bypassed → non-sniper sequential poll ran through the window ✓ Fixed
High 3+ min poll during sniper Unreleased dates triggered 60s pagination timeouts. Each of 3 unreleased dates = 180s wasted ✓ Fixed
High Notifier shows wrong slot count self._poll_count (cumulative poll #) passed as slots_found to sniper_mode_ended() ✓ Fixed
Medium PM2 Chromium crash loop MachPortRendezvousServer failure. PM2 daemon corrupted after 30 rapid restart cycles ✓ Fixed
Medium playwright-stealth v2 API break stealth_async() renamed to Stealth().apply_stealth_async() in v2 ✓ Fixed
Medium Discord token in plaintext Hardcoded in start-claude.sh, tracked by git ✓ Fixed
High Debug PNGs + .claude/ config unignored Authenticated page captures and machine-local Claude settings could be accidentally staged and pushed ✓ Fixed
High Generic "Book" button clicks wrong slot _click_time_slot() clicked the first visible Book button without verifying the parent element contained the target time text ✓ Fixed
High _wait_for_checkout() times out early Waited on a single DOM selector for checkout — timing mismatch caused premature timeout before checkout page rendered ✓ Fixed
Medium Phase 1 still doing full calendar scans test_sniper_phases never set monitor._sniper_active, so keep_pages=False in every poll — the Phase 1 early-return never fired ✓ Fixed
Medium Saved card detection false negative saved_payment_card selector didn't match Tock's live UI; _has_saved_card() returned False despite card being on file ✓ Fixed
Test Infrastructure
🔬

--verify

Validates DOM selectors against the live Tock site. First line of defense when Tock updates their frontend.

🧪

--dry-run

Full flow without booking. Logs what it would have booked. Safe for production testing.

💳

--test-booking-flow

Navigates to checkout on a test restaurant (Benu). Verifies saved card, CVC field, confirm button. Screenshots the state.

🔔

--test-notify

Sends all 4 Discord notification types. Verifies webhook URL is correct and messages render properly.

--test-sniper

Validates session pre-warm, page reuse with reload, and confirm retry logic. Tests all sniper components in isolation.

🔁

--once

Single poll cycle then exit. Fast validation that the full scan works without committing to the monitoring loop.

🎛️

--test-sniper-phases

Drives N polls straddling the sniper window start. Validates Phase 1 (pre-release no-op, no calendar scans) transitions correctly to Phase 2 (aggressive concurrent scan).

# Full validation pipeline after any code change: python main.py --verify # selectors ok? python main.py --test-notify # discord ok? python main.py --test-booking-flow --test-restaurant benu # checkout ok? python main.py --test-sniper # sniper ok? python main.py --test-sniper-phases # phase 1/2 transition ok? python main.py --once # full scan ok?
Key Lessons Learned
01

Never trust mock-based selectors

Unit tests with mocked DOM passed 39/39 while the live site used completely different CSS classes. Always validate against real pages with --test-booking-flow.

02

Fix the checker AND the booker

Fixing detection without fixing booking means you can see slots but can't grab them. Every selector change must be applied to both code paths.

03

Seconds matter in sniper mode

A 2-minute timing drift (int truncation) and 3-minute poll cycles (pagination timeouts) meant missing slots that sold out in under 60 seconds.

04

Debug screenshots are invaluable

The screenshot showing "New reservations will be released on March 27 at 8:00 PM PDT" confirmed the exact release schedule and revealed the modal calendar structure.

05

Plan before coding

Every successful feature started with research → plan → implement → test. Skipping the plan led to assumptions that broke on the live site.

06

Infrastructure is part of the product

PM2 daemon corruption, Chrome/Playwright port conflicts, conda vs system Python paths — production environment issues cost as much time as code bugs.

Next Steps
🎯

Live Booking Run

All critical fixes deployed and validated. Two-phase sniper, concurrent abort, correct saved-card detection, and checkout polling all confirmed. Waiting for next Friday 8PM release window.

🔒

Post-Click Verification

Add date validation after clicking a calendar day — confirm the UI actually transitioned to the target date before attempting to book.

⏱️

Slot-Time Targeting

Match AvailableSlot.slot_time to the specific button clicked, so the bot books the preferred time slot rather than just the first available.

☁️

Cloud Migration

Move from Mac Mini to a cloud VM for true 24/7 uptime without Chrome conflicts or PM2 daemon issues.