{"slug": "i-replaced-calendly-with-agents-here-are-the-receipts", "title": "I Replaced Calendly With Agents. Here Are the Receipts.", "summary": "A developer replaced Calendly with an agent-built open-source scheduling tool, punctual.sh, at $0 per month after Cal.com's MIT-licensed cal.diy stripped 411,000 lines of code and was deemed non-production-ready. The 10-day build produced 162 commits, 30,658 lines of TypeScript, 737 test assertions, and 60 Linear tickets, but a two-model review of the codebase history found 14 real issues among 15 candidate findings, including a 404 bug on team event types and broken Microsoft conflict-checking. The developer advocates adversarial code review and platform constraints over agent-written tests.", "body_md": "We paid $60 a month for Calendly. Three people, one job: a link that shows when I am free.\n\nI asked Claude for an open-source alternative and was told nothing serious was left. Half true. Cal.com did relicense to [cal.diy](https://github.com/calcom/cal.diy) under MIT in April 2026, but the same commit cut teams, workflows, insights and SSO: minus 411,000 lines. The second line of their own README says strictly recommended for personal, non-production use. No release since March, Docker images archived, and standing it up still wants a server with Node and PostgreSQL.\n\nSo I built one. Started on a Saturday, on Cloudflare, because at our volume it is free and I am still a fan.\n\nTen days later: 162 commits, 30,658 lines of TypeScript across 85 files, 737 test assertions, 60 Linear tickets, one Worker, $0 a month. Google Calendar and Microsoft 365, .ics emails and reminders, teams with round-robin and collective scheduling, REST API, webhooks, an embed widget, and an MCP server so agents can book meetings too. MIT, live at punctual.sh, currently 56 stars.\n\nThat is the part people screenshot. Here is the part they do not.\n\nTL;DRAgents write code fast. Nothing they produce tells you whether it is right. I bought that answer in two places: a review process adversarial by construction (two models, severity triage, hard time box), and a platform where whole classes of bug are not expressible. When a machine writes the code, prefer a constraint the database enforces over a protocol the code must remember.\n\n## 1. The number that matters is not 162\n\nOn day five I ran the two-model review from chapter 3 against the entire codebase history rather than the latest diff. It surfaced 15 candidate findings. 14 were real.\n\nThe headline one: team-owned event types, a flagship feature with green domain tests, returned 404 on every single HTTP request. One join in one query quietly dropped every row that belonged to a team rather than a person. Perfectly tested at the domain layer, completely unreachable through the product.\n\nUnderneath it: Microsoft conflict-checking silently broken (wrong identifier type fed to Graph's `getSchedule`\n\n), REST cancel and reschedule never firing webhooks, the per-day cap undercounting collective hosts across timezone boundaries. Plus two regressions from my own first round of fixes, caught by a second pass the same day.\n\nThat is what 737 passing tests look like from the inside. Tests written by the agent that wrote the code check what the agent believed it was building, not whether the feature is reachable from outside the process.\n\nSo the question stopped being whether agents can write code. It is what independent thing tells you they got it right.\n\n## 2. The constitution\n\nEvery repo has a `CLAUDE.md`\n\n. This one is not documentation, it is enforcement, and four rules earn their place.\n\n**The public boundary.** `engine/`\n\nis public the moment it is pushed, so strategy, pricing and customer data never go in, commit messages and code comments included. An agent holding your strategy doc in context will paste it into a public README as helpful background, and git history keeps it forever.\n\n**A ticket before code.** Every non-trivial change gets a Linear issue first, 60 of them in ten days. It is the only place the reasoning survives a context window ending.\n\n**Commits are per repo, English everywhere.** Four repos under one workspace, so one `git add`\n\nnever covers it, and private notes stay in English because translating at the boundary is where meaning gets lost.\n\nNone of this is for the agent's benefit. It is blast radius.\n\n## 3. Two reviewers that fail differently\n\n`tools/review.sh`\n\nputs Codex and Claude on the same diff, in parallel. Two models because they fail differently: Codex tends to catch driver-level and ordering problems, Claude catches domain-invariant and consistency ones. On that 15-finding run, 8 came from Codex, 7 from Claude, exactly one overlapped.\n\nThree rules make it usable instead of a tax.\n\n**Fix P0 through P2. Never fix P3.** P0 is data loss, a security hole or a broken invariant. P1 is a user-visible feature broken or silently wrong. P2 is wrong under a realistic edge case: a DST boundary, a concurrent request, a provider error. P3 is style, naming and \"consider\", and both reviewers are told to omit it entirely rather than list it. A review that reports everything gets ignored wholesale, which is worse than one that reports less.\n\n**The time box is hard.** 600 seconds each, killed at the deadline, partial report delivered rather than blocking the branch. Re-running until clean is the failure mode this replaces.\n\n**The script fixes nothing.** It emits a report a human acts on. Auto-applying review findings is how a one-line change becomes a forty-file diff.\n\nOnly P0 exits non-zero. An exit code that fires on everything teaches everyone to ignore the exit code.\n\nThe brief both reviewers receive fits in a paragraph, and you can paste it into any model today:\n\n```\nReview this diff for DEFECTS ONLY. You are not a style checker.\n  P0 - data loss, a security hole, or a broken invariant. Ship-blocking.\n  P1 - a user-visible feature is broken or silently wrong.\n  P2 - wrong under a realistic edge case: a timezone boundary,\n       a concurrent request, a provider error.\n  P3 - style, naming, structure, hypotheticals, \"consider\".\nReport P0-P2. Do NOT report P3. Zero findings is a valid result.\nEach finding: SEVERITY | file:line | what breaks | the input that\ntriggers it | the minimal fix.\nBefore reporting anything, try to REFUTE it by tracing the code path.\nA wrong finding costs more than a missed one.\n```\n\nThe detail I did not see coming: an unauthenticated reviewer returns zero findings, on screen indistinguishable from a clean review. The script now detects that and prints NOT AUTHENTICATED. Silent success is the most expensive output in an automated pipeline, and this was the first of two places it showed up.\n\n## 4. Six agents, six worktrees\n\nOne evening I dispatched six backlog tickets to six agents in parallel, each in its own git worktree, a separate checkout of the same repository so no two agents ever touch the same files. Each wrote a fix plus a regression test, ran typecheck and the full suite, committed independently. I reviewed every diff, rebased onto main one at a time, re-verifying after each merge.\n\nTwo things only surface at that scale.\n\nThe agent on ICS sequence numbers found that its ticket's literal scenario was unreachable through any current call path, said so, and hardened the underlying primitive defensively anyway. Better than a fix, and no test would have told me.\n\nAnother agent's new test file landed outside vitest's configured include globs. It would have silently never run. That is the second silent success: a test that does not execute is worse than no test, because it reports green.\n\nA third pass then found a structural problem in the most invariant-sensitive code in the project: the slot grid anchor depended on which busy data a query happened to load. I filed it rather than attempt a third same-session patch there. Knowing when to stop patching is the one thing in this workflow no agent offered to do.\n\n## 5. Why the whole thing is one Worker\n\nCloudflare shows up twice in this story, and the marketing reason is the weaker one.\n\n**There is almost nothing to misconfigure.** One `wrangler deploy`\n\nships the whole product: Workers for compute, D1 for the database, Durable Objects for booking locks and rate limits, Queues for email and webhooks, KV for the freeBusy cache, R2 for avatars, Cron for reminders. No server to harden, no Postgres to tune, no Docker, no Kubernetes manifests, no deploy pipeline to build. Each is a surface where an agent produces plausible, locally-working, subtly wrong config. The cheapest defense against that is not owning the surface. Self-hosting takes fifteen minutes into your own account, and at three-person volume the bill is zero.\n\n**Guarantees instead of protocols.** This is the real reason, and it took a redesign to learn.\n\nThe obvious way to never double-book a host is one Durable Object per host. A Durable Object [handles one request at a time](https://blog.cloudflare.com/durable-objects-easy-fast-correct-choose-three/), so routing every attempt through the host's own DO puts them in a queue and the platform hands you correctness for free. That holds until a collective meeting needs three hosts at once. Now one booking has to change three independent objects at once, which is the problem banks have when money moves between two accounts: partial success leaves two hosts booked and one free, and two racing bookings can interleave into the exact double-booking the DO existed to prevent.\n\nThe fix was to stop asking the DO layer to be correct and start asking it to be fast. The invariant moved into D1:\n\n```\nCREATE TABLE slot_locks (\n  host_user_id  TEXT NOT NULL,\n  bucket_start  INTEGER NOT NULL,   -- UTC epoch ms, 5-minute grid\n  booking_id    TEXT NOT NULL,\n  PRIMARY KEY (host_user_id, bucket_start)\n);\n```\n\nEvery booking writes one row per five-minute bucket per participating host across its buffered range. A 60-minute collective meeting for three hosts is 36 rows. The booking row and all of its lock rows go into a single [ D1Database::batch()](https://developers.cloudflare.com/d1/worker-api/d1-database/#batch). If any bucket is already taken, the primary key constraint fails and the entire batch rolls back.\n\n\"These two meetings overlap\" is not something a database can be told to forbid. \"This exact five-minute box is already taken\" is. Double-booking, for any number of hosts at once, stops being a rule the code has to remember and becomes something the database physically will not store.\n\nThat distinction is the whole argument when a machine writes the code. A protocol is a promise every future code path must keep, including the ones an agent adds next week while you sleep. A constraint does not care who wrote the query. The DO still earns its place - serializing attempts, running the mandatory freeBusy re-check right before commit, holding five-minute advisory locks while a guest fills the form - but its worst failure is now a wasted API call or a clean 409, never a double-booked calendar. Leases are always taken in the same order, sorted by host ID, so two bookings can never end up waiting on each other forever, and a failed lease just falls through to the batch.\n\nI verified that rollback against production D1 with a deliberate constraint violation before any booking code shipped, because the entire invariant rests on that one behaviour holding in practice.\n\nThe same reasoning produced a rule an agent can check mechanically: every database read goes through one helper, and a raw query anywhere in the codebase blocks the branch. No judgement call, no debate about whether this one is fine. Rules a machine can check are the only rules that survive contact with a machine.\n\n## 6. What only a human found\n\nThe bugs that reached production got past tests, reviewers and agents alike. I found them by using the product.\n\nThe booking page did not show today's slots on first load. New accounts had no availability until the dashboard form was saved once, so connecting a calendar did not make you bookable. The confirm page asked \"What would you like to discuss?\" twice. CI stayed red for days on a flaky wall-clock test in the rate limiter, unnoticed because local runs were green. That became its own rule: after every push, confirm the run finished green rather than inferring it locally.\n\nEach is invisible to a test suite and obvious within thirty seconds of real use.\n\nWhich settles the division of labour. Agents write and review each other. The platform enforces what it can make impossible. The human uses the product, and decides when to stop patching.\n\nA month ago I wrote that you own every word your agent sends under your name. The engineering version is the same sentence with one noun changed: you own every line your agent commits. Ten days of that gave me something my company now runs its meetings on, and a subscription I cancelled.\n\n**MIT, no seat limits:**[github.com/CCCrafts/punctual](https://github.com/CCCrafts/punctual),[self-hosting guide](https://github.com/CCCrafts/punctual/blob/main/docs/self-hosting.md)included.`wrangler deploy`\n\ninto your own account, fifteen minutes, $0 at small volume.**Steal the review brief** from chapter 3. It works on any diff and any model, and P3 silence is the part that makes it stick.**Live at**, which is also where you can book time with me.[punctual.sh](https://punctual.sh)** Replaced a SaaS with something an agent built?**Tell me. I am collecting the ones that survived production.\n\nNext issue: the DST rules almost nobody writes down, and the five timezones that break scheduling code structurally rather than numerously.", "url": "https://wpnews.pro/news/i-replaced-calendly-with-agents-here-are-the-receipts", "canonical_source": "https://www.bulaev.net/p/i-replaced-calendly-with-agents-here", "published_at": "2026-08-23 16:53:38+00:00", "updated_at": "2026-08-23 17:13:29.836613+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-agents", "developer-tools", "ai-research"], "entities": ["Calendly", "Claude", "Cal.com", "cal.diy", "Cloudflare", "Codex", "Linear"], "alternates": {"html": "https://wpnews.pro/news/i-replaced-calendly-with-agents-here-are-the-receipts", "markdown": "https://wpnews.pro/news/i-replaced-calendly-with-agents-here-are-the-receipts.md", "text": "https://wpnews.pro/news/i-replaced-calendly-with-agents-here-are-the-receipts.txt", "jsonld": "https://wpnews.pro/news/i-replaced-calendly-with-agents-here-are-the-receipts.jsonld"}}