A loop with tools
Decides, calls a tool, reads the result, decides again, and carries state between turns. Right for open-ended work: searching, booking, fixing a build. Costly, slow and non-deterministic by nature — every turn is another roll.
Engineering note · AI CV review
FlowGT scores a CV with one model call, no memory, no tools and no loop. This page is the whole design — what it is, what it deliberately is not, and the four rules that keep it cheap, private and auditable.
Every review — from the public page, the member portal or the admin desk — rides the same line. The doors differ; nothing after the door does.
Three words get used for "the AI bit". They are different things, and only one fits a grader.
Decides, calls a tool, reads the result, decides again, and carries state between turns. Right for open-ended work: searching, booking, fixing a build. Costly, slow and non-deterministic by nature — every turn is another roll.
A packaged brief that changes how an agent behaves. Here the closest thing is the rubric — and it is data, versioned in the database, never in git. Nobody runs it; the grader reads it.
One call. Same rubric, same text, same answer (temperature 0). No memory between runs, no tools, no loop. That is exactly what makes it cacheable, swappable between providers, cheap to run and easy to audit — the properties a grader is judged on.
The harness does seven things, in this order. Move any two and something breaks — usually a wallet.
Is the feature on, is a provider configured, is a rubric loaded, can we read the CV. Any "no" is a named 503 — and no ledger row, because no call happened.
Emails, phone numbers and links become [email] [phone] [link] before the text leaves our server. Year ranges are shielded first, because 2019–2023 is eight digits with a separator — exactly what a phone regex eats. Name and history stay: a CV minus its history is not reviewable. That limit is written down, not hidden.
sha256(rubric version + redacted text). Nothing about the person is in the key, so it can live in a database that must not hold pasted CVs.
Seen this hash with a finished verdict? Return it. No model call, no quota spent — a repeat costs nobody anything, so it must not cost the member their day's slot either.
One guarded INSERT … WHERE COUNT(today, this identity) < limit. Zero rows written means the day is used. The database cannot forget the way an if can. Identity is the account for members, and IP hash or machine id for the public door.
System message: fixed head + rubric. User message: the redacted CV. That order is the entire prefix-cache strategy — providers bill the longest prefix they have seen before at a fraction of the price. Put a timestamp or a name before the rubric and every call pays full price forever, silently.
Every call gets a row: provider, model, tokens, cache hits, cost, latency, channel, error. A failure that billed nothing gives the slot back; one that burned tokens keeps it. The quota guards money, not luck.
The ledger is the point. From the second call on, cache-hit tokens should be about the size of the rubric; if they are not, the prompt order is wrong and nothing else will tell you.
| channel | provider | prompt | cache hit | out | cost µCNY | ms | status |
|---|---|---|---|---|---|---|---|
| anon | minimax | 3,812 | 0 | 412 | 0 | 8,904 | done |
| runway | minimax | 3,655 | 3,140 | 398 | 0 | 6,120 | done |
| runway | cache | 0 | 0 | 0 | 0 | 0 | done |
| member | deepseek | 3,702 | 3,140 | 405 | 4,934 | 7,388 | done |
| anon | minimax | — | — | — | — | 50,012 | failed · timeout · refunded |
Each one is enforced somewhere a person cannot forget it.
Redaction runs before every call, on every door. Figma, a model provider and a contractor are all third parties. _grader.js · redactPII · test suite
The full verdict has a deduction list with quoted evidence. Members and the public get score, band, one line and top fixes. The deduction detail stays on the admin desk — one function shapes every non-admin response, so the two doors cannot drift. _cv-review.js · memberView
Versioned, append-only, loaded through an admin endpoint, never committed. Every ledger row records which version scored it, so an old score can always be explained. cv_rubric · rubric_version
Cache before quota, quota before the call, refund on an unbilled failure, fall back to the next provider only when the first billed nothing. _cv-review.js · run()
The pasted CV is untrusted input to a model, and the model's answer is untrusted input to a browser. Both edges are guarded, and every guard has a test that puts the attack back and watches it fail.
The CV travels between data markers, and the cached instruction says nothing inside them is a command. Attack phrases are counted and written to the ledger as flags — the score still ships, and the score is clamped to 0–100 by code, not by the model's good will.
Any output line that shares eight words with the rubric is dropped before it leaves. The rubric lives in the database and is never sent anywhere but the model.
Every string in a verdict is scrubbed: links removed, markup stripped, control characters gone, lengths capped. The pages render text, never HTML.
Every knock costs the network one attempt, refused or not; forty a day. A site-wide daily budget closes the door for everyone before a token plan can be drained, and the page greys out first so nobody pastes for nothing.
A request that names a foreign origin is refused before your session is even read. The pages cannot be framed by other sites, and nothing on them can use a camera, microphone or location.
Not a CV? The model says so, the harness refunds your slot, and you get the sentence in your own language. The same code that guards against abuse is the code that keeps a mis-paste from costing you a day.
Not a hiring decision. It is a coaching score against a rubric a person wrote. Temperature 0 makes it consistent, not correct. A green band means "send it", not "you will be shortlisted".
Not private from the model. Redaction removes contact details, not the story. If your CV says where you worked, the provider sees where you worked. The consent line on upload says exactly this.
Not free to run at scale. The public door is one a day per person for that reason. Runway members get ten because a member is a relationship, not a request.
Public repository; URL is the file path.
functions/api/_grader.jsThe stateless grader: providers, redaction, parsing, fallback chain.
functions/api/_cv-review.jsThe harness: config, cache, quota, ledger, audit, member view.
functions/api/cv-review.jsPublic door — pasted text, signed out allowed, Turnstile.
functions/api/me/cv-review.jsMember door — the CV kept in the portal.
functions/api/admin/cv-review.jsAdmin door — full verdict and the spend ledger.
migrations/0028-grader-channels.sqlWhy the ledger accepts a row with no stored CV behind it.
migrations/0029-grader-guard.sqlThe flags column and the attempt / budget counters.
docs/CV-GRADER.mdThis page, as a document, with the decisions and their dates.