back to the résumé

How the Ask box works

A short, honest description of what's behind the question box on my résumé — including the parts I chose not to build.

The shape of it

It answers questions about my background from a corpus of markdown files I write and control. Not the open web, not a general assistant.

your browser │ question + a verification token ▼ Cloudflare edge ──tunnel──▶ nginx ──▶ a small TypeScript service │ corpus + system prompt ▼ the model ──streamed back──▶

The service is a Hono app on Node running on a VPS. Nothing listens on a public port — traffic arrives through a Cloudflare Tunnel that dials outbound, so the machine's address is never published and the firewall stays closed.

Why there's no vector database

The obvious move is retrieval-augmented generation: chunk the corpus, embed it, search at query time. I didn't, and the reasoning is the interesting part.

My whole corpus fits comfortably in the model's context window. When that's true, sending all of it is both simpler and more accurate — retrieval introduces a failure mode that stuffing doesn't have: the searcher misses, and the model confidently says it doesn't know something that is sitting right there in the corpus.

So retrieval is a cost optimisation with a quality cost, not an upgrade. It's worth taking when the corpus outgrows what I want to pay per question — somewhere north of 150k tokens. The corpus is split into core/ (always sent) and detail/ (the part that will grow), and the function that selects detail already takes the question as an argument. When the day comes, its body becomes an embedding search and nothing else changes.

Building the seam is cheap. Building the vector database before you need it costs you a failure mode you didn't have.

Keeping it honest

Everything it says is grounded in those files. The system prompt tells it to decline rather than guess, and to ignore instructions embedded in a visitor's question.

That matters more here than on most demos. This thing speaks as me, to people deciding whether to work with me — an invented employer would be worse than an admitted gap. If you ask it something I haven't written about, it will tell you so. That's the intended behaviour, not a limitation I'm apologising for.

Not being a money faucet

A public endpoint that calls a paid model is an open invitation. The controls are layered, cheapest first:

On the verification: checking that a Turnstile token is valid isn't enough. The sitekey is public — it's in the page source — so anyone can embed the widget on their own site, solve it honestly, and replay the token. The server also checks the hostname the token was minted on and the action it was bound to.

When a provider has a bad day

Which model answers is named in a small config file the service watches. Edit it and the next question uses the new provider — no restart, and anything mid-stream finishes on the model it started with.

Callers pass an alias, never a model name. A public endpoint that accepted arbitrary model strings would be a proxy for spending my budget on anything the provider hosts.

The same thing, for agents

The box on the résumé is one doorway. There's another at ask.anthonyrizk.me/mcp — an MCP server exposing a single tool, ask_about_anthony, over the same corpus and the same prompt.

If you're evaluating me and you work in Claude, you can add it as a connector and interrogate this material from inside your own tooling rather than through my text box.

It also gets multi-turn for free, in a way the web box can't. Your agent holds the conversation and decides what to look up next; each call stays independent, with no session state on my side to keep, expire or leak. The tool is a lookup primitive, and the composition happens where the context already lives.

Letting agents in without opening a faucet

This is the part I found genuinely interesting, because it has no settled answer yet.

Turnstile gates the web endpoint by proving a human rendered a page. There is no equivalent for an agent. And the obvious workaround — issue a token, publish it in the docs — is theatre: a secret printed on a public page is not a secret, it's a speed bump with extra steps.

So the endpoint is open, and the controls are the ones that actually work on something unauthenticated:

Only the last one bounds anything; the rest reduce the odds of reaching it. The honest worst case is that a stranger reads about my career until the credit runs out — which I can live with, and which is a clearer thing to reason about than a token I'd have had to pretend was private.

The tool's description is written for the model calling it rather than for a person: each call is independent, prefer several narrow questions to one broad one, and treat "not covered" as an answer rather than something to rephrase around. That last one is a safeguard — an agent retrying a refusal from another angle is exactly how a grounded system gets pressured into speculating.

Knowing whether it works

A corpus that changes and a model that can be swapped need some way to answer "is this better or worse than yesterday" that isn't a feeling. There's a fixed set of thirteen questions I can run against any provider, recording latency, length, whether anything came back at all, and whether expected facts appear.

Five of them are negatives — things it must refuse. Salary, whether I'm an ML researcher who could pass an interview on transformer internals, the capital of France, a prompt injection, an employer I never worked for. Declining rather than inventing is the whole proposition, so it gets more tests than the happy path.

The instructive part was getting it wrong. My first version checked whether the answer contained a refusal phrase and failed three cases whose answers were perfect refusals — "No —", "out of scope", "I can't do that". That list never ends. Asserting the absence of the wrong answer is narrow and stable: no dollar figure, no "Paris", no invented role.

It also picked the model. Three configurations, same thirteen questions, all passing — and the numbers rather than my preferences decided which one runs.

What it deliberately doesn't do is judge quality. Whether an answer reads well, hedges appropriately, or is something I'd want a hiring manager to see — that's me reading two runs side by side.

Things it doesn't do