Bool
Credit metering, deploys, and reliability for an AI app builder.
A six-week internship at CodeHS on Bool — type a prompt, get a hosted web app. I merged 99 pull requests: generalized a prototype AI meter into priced app credits, rebuilt the upload-and-deploy path agents actually use, and fixed the failures that take the site down or lock people out without an error.
App credits
Bool was about to ship bool.ai: a generated app can call a model, and the owner pays. The meter was a prototype named after that one feature, charged a flat 3 credits regardless of size, and threw away the dollar cost on every call — the builder chat captured it, the battery plane recorded costUsd: null. That number was gone and could not be backfilled, which is what made capturing it a deadline rather than a backlog item.
I generalized the pool to app credits so every bool.* call shares one balance, then priced against the provider’s reported cost: hold a fixed amount pre-flight so parallel calls cannot oversell, settle the difference after. The first version of that change never priced a single call. The SDK hangs usage on an aggregate that discards the raw provider block; the tests put the raw block on the aggregate, a shape the real SDK does not produce, and all 66 live rows under the branch came back null. The regression test now plants a decoy cost 100× the real one, so a revert charges the wrong amount instead of failing silent.
A 60-calls-per-minute rate limit looked like it protected the owner’s balance. At 3 credits a call it empties a 2,000-credit paid pool in eleven minutes, and the pool is workspace-wide, so one public app being hammered takes down every other app the builder owns. The per-project daily ceiling lives in the shared credits module so every future battery inherits it. When the plane returns 402, generated apps used to console.error or toast out_of_app_credits — pixel-identical to a feature that was never finished, or the owner’s billing state shown to a stranger. The fix was a component seeded into every new app, because a rule the model can ignore is weaker than a file it can import. The visitor sees that AI is paused; the owner gets mail at the moment of the block.
Bool Drop
The agent deploy tool streamed base64 file content through one JSON call with an 800-second timeout, and you had to create an empty project first. It is now one tool, a single-use signed upload URL, a ZIP, and a poll. The extractor rejects path traversal and bounds actual decoded bytes; the browser Drop and the MCP path share one processor. A drops table replaced a hack that stored an anonymous claim token inside the project’s description field.
A VIP customer’s project list came back empty because “active workspace” was a browser cookie, and a headless MCP request never sends one. Listing now covers every workspace the caller belongs to. A free workspace already at its deploy cap failed with “Something went wrong… Please try again,” so agents retried a doomed drop five times in one session — the typed error, the friendly message, and the relay to the agent all already existed; the cap check was throwing a plain one.
When it stays broken
On July 7 the site went down for several minutes. Every 500 had the same cause: the database pooler at its 200-client cap. Each warm serverless instance held its own pool, the idle timeout defaulted to zero — never close — and connections never drained after the spike. The first fix also clamped max connections to 3, which serialized the admin pages past the function timeout and got reverted; the idle drain stayed.
Editor loads stranded on the skeleton forever: 293 ms to first byte, then 36 seconds receiving 47 KB of shell and never a load event. The render fanned out about 14 queries into a pool of 10, and the pool queues past its maximum with no timeout — a query waiting for a slot has not reached the database, so the statement timeout never fires. 14 > 10 is arithmetic, not a timing problem. Reads were staged into waves and four dashboard queries moved off the critical path; collapsing back to one wave would have saved a round trip and pushed peak concurrency back to the cap that caused the stall.
The link that never worked
Confirmation verified a single-use token on GET. Corporate mail scanners fetch every URL in an inbound email. The scanner burned the token; the person who clicked got a dead link, and both error paths redirected with a query parameter that two routes wrote and zero read. A reporting user described it, twice, as “it takes me to the sign-in page.” It also explained a number that had gone unexplained: 5 of 12,878 imported users had ever set a password.
GET now renders a button; POST verifies. Scanners issue GETs and do not submit forms, so the token survives the scan. Password-reset links inside generated apps had a separate pair of bugs — they dropped the path prefix that identifies the app, then 404ed because the link was built from the request host rather than the app’s canonical URL — and the SDK sign-in hook left the reset token in the URL forever, so logging out bounced you back to “create a new password.”
Stack
- TypeScript
- Next.js
- Drizzle
- PostgreSQL
- Redis
- Bun
Links
- App credits: one pool priced on real provider cost, with a daily ceiling so one public app cannot empty a workspace.
- Bool Drop rebuilt so an agent or a browser uploads a ZIP and gets a live URL — and hard plan limits stop looking like something to retry.
- The silent failures: a site-wide outage from a pool that never drained, and an editor that never left the loading skeleton.
- Login links that mail scanners burned before a person could click, which is why almost nobody from the import ever set a password.