imn@dev:~/oss/reservio-bot$ cat readme.md

gh://reservio-bot

OSS

booking automation · telegram + trigger.dev

a padel club in batumi releases its courts through reservio one hour at a time, on a rolling seven-day horizon, and the evening slots are gone in under a second. before any code could compete, two questions had to be answered by observation alone: when exactly does a slot appear, and which courts are actually winnable.

the answers, found by polling the api to the second across several evenings: h:59:00 ±2s, seven days before the slot's own end — and two of six courts are quietly held back by the club, so they never surface in the public feed no matter how often you ask. both findings live in the repo as a measurement journal.

the rule the project calls path a: the booking core is deterministic code, cron plus direct api calls, with no llm and no browser automation anywhere near a real booking. a trigger.dev cron plans the evening at 20:30 tbilisi, enqueues one delayed run per profile-hour, and that run polls availability every 2s and fires exactly one POST — never retried, because a second attempt risks a real duplicate.

count-up to the 21:59:00 slot drop ending in one booked POST

imn@dev:~/oss/reservio-bot$ cat scheme.md

daily-planner

cron 20:30 tbilisi

heartbeat

cron 22:12 · watchdog

delayed run per profile · hour

booking engine

poll @2s · one POST, never retried

h:59:00 ±2s

reservio api v2

guest checkout · reverse-engineered

state: supabase ⇄ telegram bot (grammY) — they meet only in the db

// infra: trigger.dev cloud crons · Supabase Postgres · the bot process still runs by hand

imn@dev:~/oss/reservio-bot$ cat services.md

Reservio API v2
the booking target — an undocumented json:api reverse-engineered by the project: guest checkout, no auth, cancel gated by a one-time token.
Supabase
shared state for the cloud jobs and the bot, through a hand-rolled postgrest client, no sdk; row-level security, service-role key only.
trigger.dev
cron and delayed-run orchestration: daily-planner at 20:30, one book-slot-drop per profile-hour, a 22:12 heartbeat, and a T-2h reminder.
Telegram / grammY
inbound wizards, buttons and an allowlist; outbound drop reports and heartbeat alerts, over long polling.
Anthropic API
claude haiku parses free text into a structured intent through forced tool use; it never writes to the user and never decides a booking.
Railway
the documented hosting target for the bot process, chosen over serverless so long polling never has to be rewritten as a webhook.
GitHub Actions
typecheck plus the full vitest run on every pull request and every push to main.

imn@dev:~/oss/reservio-bot$ cat stack.md

TypeScript strictNode 20 · native fetchgrammYtrigger.devSupabase / PostgRESTbetter-sqlite3Vitesttsx

imn@dev:~/oss/reservio-bot$ cat highlights.md

CLOCK
the drop time was measured, not guessed. polling the availability endpoint to the second across several evenings put it at h:59:00 ±2s, and the engine sleeps to that window rather than hammering the api all day.
ONE POST
idempotency is checked three separate times per run — at start, after sleeping to the drop window, and immediately before each POST — because the store call is networked and a double trigger inside that gap would book the same court twice.
FAILURE
an ambiguous POST (timeout, 5xx, or a 2xx with no id) is handled differently from a clean rejection: the booking may already exist with nothing to show for it, so the engine flags that court instead of retrying. cancels are read back for the same reason — state "canceled" works, "cancelled" silently 200s with the old state.
STATE
one async StateStore interface with three interchangeable backends — memory for tests, sqlite locally, supabase in the cloud — all keyed on (profile, date, time, court), so idempotency behaves identically everywhere. the bot and the crons meet only in the database.
GUARD
the one llm in the repo is boxed in: tool choice is forced to a single tool so the model physically cannot return prose, and the user's free text is only ever placed in the user message, never the system prompt.
TESTS
around 900 vitest cases across the engine, the scheduler, all three state backends and every bot wizard, plus a documented adversarial pass per feature — prompt injection, drop-timing off-by-one, silent booking loss, invite-link brute force.