How this site works

A static site, a small API and an assistant that answers only from this site's own content.

The pages are plain HTML, built ahead of time. A Cloudflare Worker runs only for /api: it checks each request, counts questions, finds the passages that answer them and streams the model's reply through a guard. Every figure below was measured on the preview deployment before launch, and the decisions behind the design close the page.

The life of a question

From the first click in the question box to the last word of the answer.

Sequence of one question: browser, Turnstile, Worker, D1 and OpenAI1. A check on first focus. Browser to Turnstile: first focus. Turnstile to Browser: one-time token. 2. A signed visitor session. Browser to Worker: POST /api/session. Worker to Turnstile: redeem token. Turnstile to Worker: valid. Worker to Browser: signed visitor token. 3. Checks, then the quota. Browser to Worker: POST /api/chat. Worker: origin, schema, signature, burst. Worker to D1: atomic count. D1 to Worker: questions left. 4. Hybrid retrieval. Worker to OpenAI: embed the question. OpenAI to Worker: 512-d vector. Worker: BM25 + cosine, RRF, top 5. 5. The prompt. Worker: instructions, profile, passages. 6. Sources first. Worker to Browser: event: meta (sources). 7. The model streams. Worker to OpenAI: Responses API, stream. OpenAI to Worker: text deltas. 8. The output guard. Worker: guard every delta. 9. Back to your browser. Worker to Browser: event: delta. Worker to Browser: event: done.Sequence of one question: browser, Turnstile, Worker, D1 and OpenAI1. A check on first focus. Browser to Turnstile: first focus. Turnstile to Browser: one-time token. 2. A signed visitor session. Browser to Worker: POST /api/session. Worker to Turnstile: redeem token. Turnstile to Worker: valid. Worker to Browser: signed visitor token. 3. Checks, then the quota. Browser to Worker: POST /api/chat. Worker: origin, schema, signature, burst. Worker to D1: atomic count. D1 to Worker: questions left. 4. Hybrid retrieval. Worker to OpenAI: embed the question. OpenAI to Worker: 512-d vector. Worker: BM25 + cosine, RRF, top 5. 5. The prompt. Worker: instructions, profile, passages. 6. Sources first. Worker to Browser: event: meta (sources). 7. The model streams. Worker to OpenAI: Responses API, stream. OpenAI to Worker: text deltas. 8. The output guard. Worker: guard every delta. 9. Back to your browser. Worker to Browser: event: delta. Worker to Browser: event: done.
  1. A check on first focus

    The first time you click into the question box, your browser loads Cloudflare Turnstile. It checks the browser in the background and hands back a one-time token. Until that click, nothing on this site loads from another domain.

  2. A signed visitor session

    The Worker redeems the token with Cloudflare, once, and answers with a signed, anonymous visitor token. Your browser keeps it in local storage for up to 30 days. There is no account and no cookie.

  3. Checks, then the quota

    The question arrives with the token. The Worker checks its origin, its shape, the signature and a burst limit. One database transaction then counts it: 10 questions per visitor, under daily caps per network and for the whole site. The counter you see is only a display.

  4. Hybrid retrieval

    The question becomes an embedding of 512 numbers. In memory, BM25 and cosine similarity each rank the passages of this site, and reciprocal rank fusion merges both rankings into the top 5. If the embedding call fails, BM25 answers alone.

  5. The prompt

    The fixed instructions and the profile go first, so the provider can serve them from its prompt cache. Then come the recent conversation, the retrieved passages, marked as reference material, and your question.

  6. Sources first

    The reply opens as a stream of Server-Sent Events. Its first event carries the sources and your questions left, before the model is even called, so the source links appear before the first word.

  7. The model streams

    One call to the OpenAI Responses API, streamed, with storage turned off. If it fails before the first word, it is retried once, a fallback model takes over after repeated failures, and the question does not count.

  8. The output guard

    Every piece of text passes a guard before it leaves the Worker. It holds back a few characters, so nothing forbidden gets out, even in part. It stops the answer if the canary or the instructions appear, plain or encoded, and it strips HTML and links to other sites.

  9. Back to your browser

    The text arrives as delta events, and a done event closes the stream with timing and token counts. A stream that ends without done counts as failed. Close the tab and the Worker cancels the model call, so nothing more is billed.

The fit check

A pasted job post becomes one row per requirement, each with its evidence. The model proposes. Code in the Worker decides what stands.

Job post to Extraction. Extraction to Retrieval. Retrieval to Classification. Classification to Validation. Validation to Rows.Job postpasted text, neverstored or loggedExtractionone model call, up to15 requirementsRetrievalhybrid search, top 3passages eachClassificationone model call, alevel per rowValidationfixed rules in theWorkerRowsstreamed to yourbrowserJob post to Extraction. Extraction to Retrieval. Retrieval to Classification. Classification to Validation. Validation to Rows.Job postExtractionRetrievalClassificationValidationRows

What the Worker enforces

  • Only offered evidence. A row may cite only the passages retrieval offered for that requirement. Anything else is dropped, including ids planted in the post.

  • No evidence, no level. A level above none with no valid evidence left becomes none.

  • Demonstrated needs support. Demonstrated needs a project passage that both rankings placed in their top 3. A skill that only appears in the CV list is related at most.

  • Years are counted. A requirement for more years of experience than his roles cover is none, whatever the model says.

  • Injections are capped. Sentences addressed to automated tools are removed before either model reads the post, and every level of that analysis is capped at related.

  • Plain text only. Each explanation is plain text of 200 characters at most: no markup, no links, no hidden characters.

Measured, not claimed

Figures from the pre-launch review of 8 October 2026, taken on the preview deployment with the real model. Each one names the report that states it.

Answers

The golden set: 60 questions with expected sources and checks, asked to the live assistant.

  • Recall@5

    95.7%

    gate 90%

    Expected sources found in the top 5 passages, last run

    Source: Pre-launch review, part 2

  • Time to first token, p95

    1,525 ms

    gate 2,500 ms

    From the question to the first word, last run

    Source: Pre-launch review, part 2

  • Hallucination bait rejected

    100%

    gate 100%

    Questions built on a false premise, last run

    Source: Pre-launch review, part 2

Attacks

The red team: 38 attacks in 13 families on the chat, and hostile job posts on the fit check.

  • Attacks answered cleanly

    37 of 38

    Extraction, role play, encoding, fake history and more, last live run

    The miss showed a link to another site as plain text, never clickable. The guard now removes those too.

    Source: Pre-launch review, part 2

  • Canary leaks

    0

    Every answer of six runs

    Source: Pre-launch review, part 2

  • Levels invented on the fit check

    0

    Demonstrated without real evidence, every run

    Source: Pre-launch review, part 2

The platform

Security headers, a scanner, crafted requests, a privacy crawl and a load test.

  • Mozilla HTTP Observatory

    A+

    Score 135, 12 of 12 tests passed

    Source: Pre-launch review, part 1

  • Medium or high scanner alerts

    0

    OWASP ZAP baseline scan of the preview

    Source: Pre-launch review, part 1

  • API abuse checks passed

    184 of 184

    Foreign origins, wrong methods, oversized and malformed bodies, forged tokens

    Source: Pre-launch review, part 1

  • Third-party requests before you ask

    0

    Every page, in a fresh browser; also 0 cookies

    Source: Pre-launch review, part 1

  • Response time under load, p95

    24 to 59 ms

    20 virtual users for 2 minutes, per route

    Source: Pre-launch review, part 1

  • CPU time per chat answer, p50

    about 18 ms

    Retrieval, streaming and the guard, as Cloudflare measures it

    Source: Pre-launch review, part 2

Cost

Priced from the token counts the model reports, at list prices.

  • One fit analysis

    about $0.003

    About 4,500 tokens in and 1,800 out

    Source: Pre-launch review, part 2

  • Six full evaluation runs

    about $0.35

    Red team, golden set and fit check, every suite

    Source: Pre-launch review, part 2

Verified answers (preview)

The next assistant, on the preview since 12 October 2026: each sentence cites what it relies on, checked first.

  • Recruiter questions answered right

    88.2%

    170 questions with expected facts; the current assistant passes 42.9%

    Source: Assistant v3 evaluation

  • Claims the judge found false

    0

    Every claim of 250 answers, against what Tomás has published

    Source: Assistant v3 evaluation

  • Rewordings routed to the right answer

    97.2%

    108 paraphrases never seen before; none routed to a wrong answer

    Source: Assistant v3 evaluation

  • Unsupported sentences the second check catches

    96%

    56 hand-labelled sentences; it removed no supported one

    Source: Assistant v3 evaluation

The stack

Few moving parts, each with one job.

Pages
Astro, static output
Preact islands only where something reacts: the assistant, the fit check, the status page and search.
API
Cloudflare Worker with Hono
Runs only for /api. Workers Rate Limiting absorbs bursts.
Data
Cloudflare D1
Question quotas, a scrubbed question log and the hourly figures behind the status page.
Model
OpenAI Responses API
Answers stream. Embeddings at 512 dimensions, stored as int8 inside the Worker bundle.
Quality
Vitest, Playwright, axe, Lighthouse CI
Five browser profiles, accessibility checks in light, dark and reduced motion, and assistant evals in CI.

Decisions

Architecture decision records: the context, what was chosen and what it costs.

  1. Static pages with islands, not a single-page appAccepted, 6 October 2026
    Context

    A portfolio is read far more than it is used. The plan asked for a Lighthouse score of 95 or more on every page, a largest contentful paint under 1.5 s and small JavaScript budgets.

    Decision

    Astro builds every page to static HTML. Preact runs only in islands that need it: the assistant, the fit check, the status page and the search palette, which loads on first use.

    Consequences

    Every page is complete without JavaScript. CI fails a page whose initial JavaScript goes over its budget: 60 KB gzipped on the home page, 40 KB elsewhere. Islands render at build time first, so hydration moves nothing on screen.

  2. The Worker runs only for /apiAccepted, 6 October 2026
    Context

    Pages and the API share one origin, so the browser needs no CORS. A Worker invocation costs CPU time, and almost every request is for a page or a file.

    Decision

    Cloudflare's asset layer serves the build output directly. The Worker runs first only for /api/* (run_worker_first), with Hono routing inside it.

    Consequences

    Pages never wake the Worker, and the API never sends CORS headers. Page headers live in a _headers file. The Worker sets its own on API responses, including a Content-Security-Policy that lets nothing load.

  3. A retrieval index inside the Worker, not a vector databaseAccepted, 6 October 2026
    Context

    The assistant answers only from this site's own content, a small set of Markdown entries. A hosted vector database or file search would add a service, a network hop and a bill.

    Decision

    A build script splits the public entries into passages, embeds them at 512 dimensions, stores the vectors as int8 and commits the index as JSON. The Worker bundles it and searches it in memory.

    Consequences

    Retrieval needs no network call besides the question's embedding, and every content change shows up in review as an index diff. A unit test fails when the index is stale. It would not scale to millions of passages, and it does not need to.

  4. Hybrid retrieval: BM25 and embeddings, fused by rankAccepted, 6 October 2026
    Context

    Names and acronyms such as DAL-A need exact matching. Paraphrases, and questions asked in Spanish or German, need meaning. Neither signal is enough alone.

    Decision

    Rank the passages twice, with BM25 and with cosine similarity, drop weak matches from each ranking, and merge both with reciprocal rank fusion (k = 60). The top 5 go into the prompt.

    Consequences

    Recall@5 reached 95.7% on the golden set. Fusion only sees ranks, so the relevance floors apply before it. When the embedding call fails, BM25 still answers, and suggested questions carry precomputed embeddings.

  5. Turnstile, a signed visitor token and atomic quotasAccepted, 7 October 2026
    Context

    The assistant is public and every answer costs money. There is no login and no cookie, and any counter kept in the browser can be edited.

    Decision

    Cloudflare Turnstile checks the browser once, then the Worker issues a signed, anonymous visitor token. After a burst limit, the server counts each question in one D1 transaction against the visitor's 10 and against daily caps per network and for the whole site.

    Consequences

    Concurrent requests cannot overshoot a limit, and editing the counter in the browser gains nothing. Clearing storage starts a new allowance, but the network and site caps still hold. A question is given back only when the model fails before its first word.

  6. Server-Sent Events, with the sources firstAccepted, 6 October 2026
    Context

    A visitor judges an answer by how soon it starts and by whether it can be checked.

    Decision

    Answers stream as Server-Sent Events from a plain POST. The first event carries the sources, the text follows in deltas, and a done or error event ends the stream.

    Consequences

    Source links appear before the first word. The client treats a stream without done as failed and offers a retry. When the visitor leaves, the Worker aborts the model call, so no more tokens are billed.

  7. An output guard with a canaryAccepted, 6 October 2026
    Context

    No prompt can promise that a model never repeats its instructions. In the red team, a paraphrase and an encoded reply both got past the prompt alone.

    Decision

    The instructions carry a secret canary, and every delta passes a streaming guard before it reaches the visitor. The guard holds back a short window and stops the answer on the canary, on the instructions, verbatim or retold, or on a long encoded run. It also strips HTML and links to other sites.

    Consequences

    An answer can stop mid-sentence, and that question still counts, so tripping the guard never buys a free question. The guard runs on every delta, so it scans only the new text to keep CPU time low.

  8. The fit check: the model proposes, the server decidesAccepted, 7 October 2026
    Context

    A job post is untrusted text and may carry instructions for the model. What the model returns is untrusted too.

    Decision

    Two model calls with strict JSON schemas extract the requirements, then propose a level and evidence for each. Deterministic code then keeps only evidence retrieval offered for that requirement, requires corroborated retrieval for demonstrated, applies a years rule, and caps every level at related when the post addresses automated tools.

    Consequences

    A fully steered model can at worst produce related, backed by real, retrieved evidence. The price is some recall: an honest match the rules cannot corroborate stays at related.

  9. Whole answers, not streams, for the fit checkAccepted, 8 October 2026
    Context

    The Workers Free plan allows 10 ms of CPU per request. Parsing about a thousand streamed events per analysis cost most of the fit check's CPU, and Cloudflare cut analyses off.

    Decision

    Both fit check model calls ask for the whole answer. The visitor still gets the rows over Server-Sent Events, each one sent after validation. The chat keeps streaming.

    Consequences

    The fit check's CPU time fell from 341 ms to 15 ms at p50. The first row now waits for the whole classification instead of arriving as soon as the model writes it.

  10. A hashed CSP, and no third party until you askAccepted, 6 October 2026
    Context

    A portfolio has no reason to load trackers, and every third-party script widens the attack surface. Some visitors will open the developer tools.

    Decision

    Every page carries a Content-Security-Policy with a SHA-256 hash for each inline script, and no unsafe-inline. Stylesheets and fonts are files on this domain. Turnstile, the only third party, loads on the first focus of the question box or the job post field.

    Consequences

    A build check fails on an unhashed inline script or a style attribute. Mozilla Observatory grades the site A+. Adding analytics later means changing the CSP and the privacy notice together.

  11. Verified answers: every sentence cites what it relies onProposed, 12 October 2026
    Context

    In October an audit of the assistant found it joining true facts into false ones: a client of the real estate platform became a client of his employer. Retrieving a few passages per question let the model fill the gaps, and the same question got different answers.

    Decision

    The model reads all of Tomás's published statements and answers as structured output, each sentence naming the statements it relies on. Code checks every figure, date, name and relation against them, and a second model removes any sentence they do not support. Common questions get a prepared answer, served word for word. The cards under an answer come from the site's own data.

    Consequences

    An answer can only say what Tomás has published, and the visitor can open the statement behind each sentence. A sentence that fails a check is retried once, then removed or replaced by an honest "not listed here" with his contact details. The second check adds about a second. It runs on the preview first, and production switches once the audit passes.

See it running