imn@dev:~/oss/reservio-bot$ cat scheme.md
daily-planner
cron 20:30 tbilisi
heartbeat
cron 22:12 · watchdog
booking engine
poll @2s · one POST, never retried
reservio api v2
guest checkout · reverse-engineered
// 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
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.