BELLWETHER · Deep dive · Independent project, not affiliated with Netflix
Level 0 is a small, production-shaped ads platform: four services that take an ad request, decide what to serve, record what was delivered, and can be broken on purpose. It exists so that everything built on top of it — the context layer, the dev agents, the ops agents — has real traffic, real metrics and real incidents instead of mock data.
Contents
Read top to bottom for the full picture, or jump. Sections 3 through 6 are the system itself; 7 through 9 are the reasoning; 10 is the honest comparison to the real thing.
01 · Backstory
BELLWETHER targets one job: Netflix's Staff AI Engineer role on the Ads Platform team, requisition AJRT30201. That posting asks for five things — a centralized context layer, dev lifecycle agents, ops agents, multi-agent orchestration, and evaluation frameworks — and its underlying tension is stated plainly: ship faster with AI without accumulating slop, regressions, and accountability gaps.
The obvious way to build a portfolio for that role is to demo agents. The problem is that almost every agent demo has the same hole in it: the agent operates on nothing. An "incident triage agent" reads a hand-written log file. An "RCA agent" is handed a fixture describing the answer it is supposed to find. Those demos prove the prompt works. They prove nothing about whether the agent could function against a real system.
So Level 0 inverts the usual order. Before any AI is written, build the thing the AI will operate on:
Ads was chosen as the domain because it is the domain of the job. Using the posting's own vocabulary — ad insertion, brand safety, frequency capping, pacing — in the code, the metrics and the logs means a reviewer reads the repo in their own language. It also happens to be a genuinely good domain for this exercise: ad decisioning is latency-sensitive, rule-dense, config-driven and money-adjacent, which produces exactly the kind of failure modes that make ops agents interesting.
Level 0 is Days 1–5 of a six-level, 30-day build. It is the only level that contains no AI at all. Everything after it consumes it.
| Day | Deliverable | The thing that made it non-trivial |
|---|---|---|
| 1 | Repo, architecture docs, running doc, ADR-0001 | Deciding to write decisions down from hour one, with the condition that would reverse each one. |
| 2 | campaign-service — CRUD, Postgres, OpenAPI | Tests passed locally and failed CI: they were secretly reaching a real database. |
| 3 | ad-decision-service — the serving path | Two services in one test process collided on a Prometheus metric name. |
| 4 | event-service + Grafana dashboards | Making ingestion idempotent without a check that races. |
| 5 | traffic-simulator + failure injection + quality gate | Injecting failures that are real enough to be diagnosable — and finding three genuine defects in the process. |
02 · Domain primer
None of the design decisions parse without the vocabulary. This is the whole domain model in one page.
A viewer is watching something. The player reaches a point where an ad can run — an ad break. The system is given a fraction of a second to answer one question: which ad, if any, should play here? That question is an ad request, and it carries two things:
On the other side sit campaigns. A campaign is an advertiser's booking: a budget, a flight window (start and end dates), targeting rules, brand-safety exclusions, a frequency cap, and one or more creatives — the actual video assets, each with a duration.
The job of the decision path is to narrow the campaign set to the ones that are allowed to serve into this specific slot, then pick one. Four constraints do most of that narrowing, and all four appear verbatim in the job posting:
true-crime is the canonical case. This is the advertiser protecting itself, and getting it wrong is a real-world incident.If a campaign survives all of that and has a creative that fits the slot's duration, it is eligible. If one is chosen, that is a fill; the resulting play is an impression. If nothing survives, it is a no-fill, and the share of requests that fill is the fill rate — the single most-watched number on the serving path.
Money is stored in micros — millionths of a currency unit — as integers. Money is never a floating-point number in an ad system, because floats accumulate rounding error and ad systems perform billions of tiny arithmetic operations against budgets. 2_000 micros is $0.002, a plausible price for one impression.
Every rejection has a name. In BELLWETHER a filter never returns a bare false. It returns a reason: targeting_mismatch, brand_safety_excluded, frequency_capped, pacing_throttled. That single decision shapes the whole rest of the project, and section 9 explains why.
03 · Architecture
The decomposition follows the natural seams of an ads platform rather than technical layers. Each service owns one question, and each owns its own state.
Signal flow — four services, two stores, one metrics path
One trip through the system, from opportunity to recorded fact.
One trip — from opportunity to recorded fact
Postgres holds configuration and history: campaigns, budgets, creatives, and every delivered impression. Low write rate, long lived, relational, auditable.
Redis holds the serving path's working set: how many times this member saw this campaign today, and how much this campaign has spent today. Written on every single impression, scoped to one member, and worthless once the day rolls over. That is a counter with an expiry, not a row with a history — so the keys are day-scoped and expire themselves after 48 hours.
These two now deliberately disagree, and the disagreement is meaningful: Redis spend is what the pacer believed at decision time; the Postgres event table is what was actually delivered. The gap between them is reporting loss, and it is a panel on the dashboard.
04 · The services
CRUD over campaigns and creatives. Owns budgets, flight windows, targeting, brand-safety exclusions and frequency caps. Two tables, campaigns and creatives, with a cascade delete.
The interesting bit: it is the only writer of its schema. Nothing else touches those tables — which is precisely what lets the project keep deferring database migrations.
One endpoint that matters: POST /ad-request. Runs the six-rule chain over the active campaign set and returns a fill or an explained no-fill, with a full trace of which campaign lost to which rule.
The interesting bit: decisioning.py performs no I/O at all. It takes a store and a clock as arguments, so every rule is unit-testable and the whole suite runs with no Redis and no campaign-service.
Ingests impressions and clicks into an append-only table, then aggregates delivery — impressions, clicks, CTR, spend — on read with a single GROUP BY.
The interesting bit: the caller supplies the event_id and it is the primary key. A retried delivery report collides with itself instead of double-counting. Idempotency is the constraint, not a check.
Drives seeded, reproducible traffic through the whole stack, and injects five failure modes switchable at runtime over an HTTP control plane.
The interesting bit: it breaks things by changing real configuration through public APIs. No service contains a branch that knows it is being tested.
Three tables in total across the platform. Small on purpose — every field earns its place by being read by a rule.
Three tables — every field is read by a rule
campaigns ―< creatives · one campaign carries many creatives, cascade delete
ad_events.request_id → the decision that produced it · event_id is the caller’s idempotency key
Note what the event table stores: request_id. Every impression can be traced back to the exact decision that produced it, including the trace of which campaigns lost and why. That link is the reason a Level 3 root-cause agent will be able to answer "why did fill rate drop at 14:20" against evidence rather than by guessing.
05 · The core
This is the heart of Level 0. Every candidate campaign walks the same chain and is dropped at the first rule it fails, carrying the name of that rule out with it. The order is not arbitrary — cheap, purely local checks run before anything that requires state, so the expensive work is done on the smallest possible candidate set.
Below: three campaigns entering the chain for one slot, and where each one dies.
active. A paused or draft flight never reaches the auction floor.
not_active
3 / 3
starts_at and ends_at. Pure date arithmetic, no I/O.
outside_flight_window
3 / 3
If the winner were simply the first eligible campaign, whichever campaign happened to sort first would drain its daily budget by mid-morning and then go dark, while under-delivering campaigns stayed under-delivered. Choosing the campaign with the most daily budget left is a crude but real delivery-balancing heuristic: it keeps campaigns tracking toward full delivery of what the advertiser paid for.
In a production system this slot is where an auction lives, and the winner is chosen on expected value — bid, predicted click-through, pacing multipliers, and so on. Level 0 has no auction and no bidding at all. That is the single largest simplification in the whole substrate, and section 10 is explicit about it.
Pacing releases budget linearly across the day: by noon, half the daily budget is available. Taken literally, that means at 00:00:01 the allowance is effectively zero and nothing can ever serve, because the first impression would exceed the allowance. So the allowance is floored at the price of a single impression — enough for the day to open. It is a one-line detail that is invisible until the day rolls over and the platform mysteriously stops filling.
The response is not a boolean and not just an ad. It is the full audit trail of the decision.
{
"request_id": "6f1c…", "slot_id": "slot-241",
"filled": false,
"no_fill_reason": "frequency_capped",
"candidates_considered": 3,
"trace": [
{ "campaign_name": "Wide-reach snack launch", "reason": "frequency_capped" },
{ "campaign_name": "Premium sedan, CTV only", "reason": "targeting_mismatch" },
{ "campaign_name": "Sports drink, mobile", "reason": "brand_safety_excluded" }
],
"decision_latency_ms": 1.84
}
Three campaigns, three different named reasons, one response. A human debugging a fill-rate drop does not have to reproduce anything — the answer is in the payload. And because those same names are Prometheus label values and structured log fields, the same answer is available in aggregate, over time, on a dashboard.
06 · Failure injection
The traffic-simulator drives about two ad requests per second through the full stack, reporting impressions and clicks back as delivery events. The population is seeded, so a run is reproducible and an incident can be replayed — which matters more than statistical realism, because ops agents will be scored against runs that must repeat.
It can also break the platform, on demand, over HTTP. The load-bearing rule: failures change real configuration through public APIs. Nothing is mocked, and no service contains a branch that knows it is under test.
| Scenario | What it actually does | What you see |
|---|---|---|
| steady | Restores seed targeting, budgets and caps — this is also the rollback | Fill rate ~43%, flat latency |
| error_burst | Sends genuinely malformed ad requests (30% of them) | Error-ratio panel lifts; typed 422s attributable to the caller |
| traffic_surge | Raises the request rate ten-fold | Throughput and p95 latency climb together — load, not a fault |
| bad_config_deploy | PATCHes every campaign's targeting to a country with no traffic | Fill rate 43% → 2%; targeting_mismatch takes over the loss breakdown |
| budget_runaway | Inflates one campaign's daily budget and cap | pacing_throttled vanishes; one campaign takes every slot it can |
The reason this matters is downstream. A Level 3 RCA agent evaluated against a mocked failure can only ever discover the mock — the evaluation would measure the fixture, not the agent. Here, when fill rate collapses, the decision path has behaved perfectly; the cause is a wrong value sitting in a real table, reachable by exactly the query a human would run.
Making the change real forces something else that turned out to be the better design: recovery has to be real too. Returning to steady is not a flag that turns the incident off — it restores the seed configuration. Reverting is a rollback, which is precisely the remediation a guided-resolution agent should be recommending.
Level 0's exit criterion, from the design spec, was "all services healthy under simulator load; failure injection works — 100%". That sentence became a script that exits zero or names the check that failed.
[PASS] campaign-service healthyHTTP 200[PASS] ad-decision-service healthyHTTP 200[PASS] event-service healthyHTTP 200[PASS] traffic-simulator healthyHTTP 200[PASS] prometheus healthyHTTP 200[PASS] prometheus scrapes every service4 targets up[PASS] steady serves ads15 fills across 56 requests[PASS] error_burst produces rejections16 requests rejected[PASS] traffic_surge raises the rate2 -> 20 requests/sec[PASS] bad_config_deploy collapses fill ratefill rate 2% across 58 requests[PASS] budget_runaway changes configuration1 campaign(s) mutatedLEVEL 0 GATE: 11/11 (100%)
The gate's decision logic is pure and unit-tested; the live run only supplies numbers. That split is what makes it trustworthy in both directions — it cannot pass by accident with the substrate switched off, and it cannot fail because a test double drifted away from reality. Two deliberate details: "not enough traffic" is never a pass (a fill-rate collapse needs at least ten requests of evidence), and the gate leaves the platform healthy when it exits.
On its first run the gate failed itself: the traffic-surge check read its baseline from the response of the call that switched the scenario, which already reflected the new rate — so it compared 20 requests/sec against 20 requests/sec and never moved. Exactly what a gate is for.
07 · Decisions
Five decision records cover Level 0. Each follows the same shape — context, decision, alternatives, consequences — plus one field that is unusual and is arguably the most valuable practice in the project: the trigger. Every decision names, in advance, the specific observable condition that would make it wrong.
That converts "we'll revisit this later" into a falsifiable statement. Two of the five triggers have now fired on schedule.
create_all over Alembic migrations, for nowBIGINT is exactly the change that now needs migrations.freq:{member}:{campaign}:{date}, 48h TTL). Campaign configuration stays in Postgres, read over campaign-service's HTTP API rather than its tables.event_id and it is the primary key. The insert either wins or raises a constraint violation, and the second outcome is a 200 duplicate acknowledgement — not an error, because from the caller's point of view the event is recorded. Delivery numbers are then a single GROUP BY, with no rollup table./delivery scans it — fast because the table is small, not because the query is clever. An honest constraint rather than a hidden one./delivery p95 crossing ~200 ms under sustained load, or the events table outgrowing a day of retention. Then a rollup table earns the copy-of-the-truth it costs.CHAOS=true flag, no debug endpoint, no synthetic metrics.08 · Tool selection
Every choice here was made against one constraint that outranks the others: the AI layer being built on top has to be able to read, reason about and modify this code. That biases hard toward explicit types, machine-readable contracts and boring, well-documented tools.
| Choice | Why this one | What was turned down, and why |
|---|---|---|
| Python 3.11+ | The language the whole AI ecosystem lives in. Levels 1–4 need LangGraph, ChromaDB, sentence-transformers and the Anthropic SDK — all Python-first. One language across substrate and AI layer means the context layer parses everything with one AST. | Java/Kotlin, which is closer to Netflix's actual backend stack, would have split the repo in two and made the AST knowledge graph a two-parser problem for no demonstrative gain. |
| FastAPI | Generates OpenAPI from type hints for free. That spec is not decoration — it becomes ingestible context on Day 6 and the contract a code-gen agent writes against on Day 11. Dependency injection gives clean test seams. | Flask (no typed contracts, no schema generation), Django (an ORM-and-admin framework for four endpoints). |
| Pydantic v2 | Validation at the boundary, in the type system. Its models are the API contract, so an invalid ad request is rejected before a single rule runs. The le= bound on budgets is documentation, validation and OpenAPI in one line. |
Hand-rolled validation, which drifts from the docs within a week. |
| Postgres | Relational data with real constraints. Level 0 leans on it twice for correctness: cascade deletes, and the primary-key collision that makes event ingestion idempotent without a race. | SQLite — used in tests, deliberately, but it does not enforce integer widths, which is why the 32-bit budget ceiling could only be caught against real Postgres. |
| Redis | The access pattern picked it: atomic INCRBY plus a TTL is exactly "a counter that expires". Key expiry means no cleanup job ever has to be written. |
Postgres counters (wrong shape, needs a nightly delete), in-memory counters (wrong the moment there are two replicas). Netflix's equivalent layer is EVCache, memcached-based; Redis is the closest thing with the same role. |
| Prometheus + Grafana | Pull-based scraping and a label model that fits the domain exactly: one counter, ad_candidates_filtered_total, labelled by reason, is a complete fill-rate diagnosis. Dashboards are checked-in JSON, provisioned from files, validated by a test. |
Netflix runs Atlas internally, not Prometheus. Atlas is not meaningfully self-hostable for a project like this, and the practice — dimensional metrics, dashboards as code — transfers unchanged. |
| Docker Compose | One command from clone to running stack; the Level 5 quality gate is a ten-minute fresh-clone target. | Kubernetes — see ADR-0001. |
| uv | Fast, lockfile-based, and one tool for environments, dependencies and running scripts. A frozen lockfile makes Docker builds reproducible. | pip + venv (slow, no lockfile by default), Poetry (slower resolution, more ceremony). |
| ruff + mypy --strict | The strictest reasonable static floor, because AI-generated code is coming. --strict across substrate, tests and platform means a code-gen agent's output has an objective bar to clear before a human ever reads it. Ruff replaces flake8, isort, black and more in one fast binary. |
Loose typing. Gradual typing would have made the Level 2 "does the generated code pass lint and types" metric close to meaningless. |
| pytest, hermetic | The entire suite passes with Docker stopped. In-memory SQLite, an in-memory decision store, protocol stubs for HTTP. 141 tests in about 5 seconds. | Integration-first testing. It is slower, flakier, and — proven on Day 2 — silently passes for the wrong reason. |
| httpx | One client library, sync and async, with a pluggable transport — so the campaign-service client is tested against a mock transport instead of a socket. | requests (no async path, no transport seam). |
| Mermaid for diagrams | Diagrams as code: versioned, diffable, reviewable, and reusable in videos. A diagram that lives in a PNG is wrong within a week and nobody notices. | Drawn diagrams in any GUI tool. |
Non-default host ports. Postgres publishes on 5433 and Redis on 6380, because 5432 and 6379 are usually already taken on a working machine. Ten seconds of thought that prevents a whole class of "works on my machine" confusion — and the kind of detail a reviewer notices.
One shared observability module. Prometheus keeps a single registry per process, so two services each defining http_requests_total collide the moment both are imported — which the test suite does. The metric is defined once in substrate/shared/observability.py, and the service label is what separates them. This was found by a test failure on Day 3, not by design.
09 · Design thinking
These recur across all four services. Most of them exist because of what has to be built on top.
A filter that returns false tells you nothing at 2am. pacing_throttled is simultaneously a field in the API response, a Prometheus label, and a key in the JSON log line. One naming decision produced the response trace, the diagnostic dashboard panel, and the evidence an RCA agent will cite — three artifacts from one idea.
The decision chain takes a store and a clock as arguments and performs no I/O. Aggregation is a query builder. Failure scenarios are data, not branches. Everything that touches the network or the clock lives at the edge behind a protocol. This is what makes a hermetic test suite possible rather than aspirational.
On Day 2 the suite passed locally and failed CI: the test client was entering a context manager that ran the app lifespan, which created tables against a real Postgres. The tests only passed because Docker happened to be running. Now every service's suite runs with the stack stopped, and that is checked before every push.
Idempotent ingestion is not a check, it is a primary key. Two concurrent reports of the same impression cannot both survive an insert. Any rule expressible as a database constraint should be one — application-level checks race, constraints do not.
Every decision record names, in advance, the observable condition that would reverse it. This turns architectural debt from a vague feeling into a tripwire. When Day 4's trigger fired on schedule, the answer — narrow the decision rather than reverse it — was obvious in minutes because the question had been framed a day earlier.
Every non-2xx response uses one envelope. An unreachable campaign-service is a 503 naming the upstream, not a stack trace. Day 5 found a genuine violation — a Postgres range error escaping as a 500 — and it was treated as a real defect, because failure modes are a design surface and machine-readable failure is the whole point when an agent is the caller.
The simulator breaks the platform by changing real configuration. When fill rate collapses, the decision path has behaved perfectly against a wrong value in a real table. Anything less and Level 3's evaluation is scoring an agent's ability to find a fixture.
Metrics and log fields were chosen at the same time as the rules, not bolted on afterwards. That is why one stacked panel — candidates lost, by rule — answers "why did fill rate drop" directly, instead of requiring three dashboards and a hunch.
Almost every Level 0 choice was made for a consumer that does not exist yet. The decision trace exists for the RCA agent. The runbook exists for the context layer. The OpenAPI specs exist for the code-gen agent. Strict typing exists so generated code has a bar to clear. Level 0 is infrastructure for the argument, not just for the app.
10 · Fidelity
I have no visibility into Netflix's internal architecture, and nothing here should be read as describing it. This comparison is against two things only: publicly reported facts about Netflix's ads business, and standard practice in connected-TV ad tech, which is well documented across the industry.
What is public and well reported: Netflix launched an ad-supported tier in November 2022; Microsoft (Xandr) was its initial ad-serving and sales partner; Netflix subsequently built and rolled out its own ad platform, branded the Netflix Ads Suite, from 2025. Netflix's general engineering stack is also publicly documented through its tech blog and open-source projects — JVM-heavy services, Cassandra, EVCache, Kafka, Flink, Titus, Spinnaker, and Atlas for telemetry.
Everything in the "production reality" column below is either one of those public facts or a statement about how CTV ad systems generally work. Where I am inferring, the row says so.
| Concern | What Level 0 does | Production reality | Fidelity |
|---|---|---|---|
| Domain vocabulary | Ad request, slot, campaign, creative, flight, targeting, brand safety, frequency cap, pacing, fill rate, no-fill, micros | The same words, used the same way. This is industry-standard terminology, not invented. | High |
| Service decomposition | Campaign management / decisioning / event collection split into separate services with separate stores | The same three responsibilities are separated in essentially every ad platform, though each is many services rather than one. | High shape |
| Filter-then-select | Eligibility filtering, then a selection step over survivors | Candidate selection → filtering → ranking is the standard shape. The kind of pipeline is right. | High shape |
| Frequency capping | Per member, per campaign, per day, in a low-latency KV store with TTL | Same idea and same class of store. Production adds cross-device identity, multiple cap windows (hour/day/week/campaign/advertiser), household-level capping, and distributed-consistency concerns this has none of. | Medium |
| Budget pacing | Even pacing: budget released linearly across the day | Even pacing is a real, named strategy. Production runs closed-loop controllers with delivery forecasting, catch-up logic, and ASAP/front-loaded modes. | Medium |
| Brand safety | Advertiser excludes content categories; case-insensitive set intersection | Right concept. Production involves classification pipelines, third-party verification vendors, per-title review, and contractual guarantees. | Medium |
| Observability | Dimensional metrics, structured JSON logs, dashboards as provisioned code | The practice is standard and transfers directly. The tools differ — Netflix uses Atlas rather than Prometheus/Grafana. | Practice matches |
| Event pipeline | Synchronous HTTP POST into Postgres; aggregate on read | Real platforms stream: a log/Kafka-style bus, stream processing, columnar warehouse, and separate real-time and batch paths. The idempotency idea is right; the transport and scale are not. | Low |
| Ad selection economics | Flat price per impression; winner is whoever has the most daily budget left | An auction. Bids, expected-value ranking, price computation, guaranteed vs biddable inventory, deal IDs. This is the single largest simplification in the substrate. | Low |
| Latency & scale | ~2 requests/sec, one replica, single region, sub-millisecond rule evaluation | Enormous request volume, strict end-to-end latency budgets, multi-region, aggressive caching of the candidate set. | Low |
| Delivery mechanics | Returns an asset URL and considers the job done | Server-side ad insertion, manifest manipulation, transcoding to many renditions, CDN delivery, player beacons, VAST/VMAP. | Absent |
| Identity, consent, privacy | A member id string. Nothing else. | Identity resolution, consent frameworks, regional privacy regimes, data clean rooms, audience segments and match rates. | Absent |
| Measurement & billing | Spend is an explicit estimate at a flat price; the docs say so repeatedly | Verified impressions, viewability, invalid-traffic detection, third-party measurement, reconciliation, actual invoicing. | Absent |
Level 0 is structurally faithful and operationally miniature. The concepts, their names, their ordering and the shape of the services would all be recognisable to someone who works on CTV ad serving. The economics, the scale, the delivery mechanics and the entire privacy surface are either absent or reduced to a placeholder.
That trade is deliberate and it is the correct one for this project, because the substrate is not the deliverable. It exists to generate real traffic, real metrics, real logs and real incidents for an AI platform to operate on. Adding an auction would make the ads platform more impressive and the AI demonstration no better. Every hour spent on ad-tech sophistication is an hour not spent on the thing the job posting actually asks about.
The claim being made is precise: this person can build a production-shaped distributed system in the ads domain, and reason clearly about what they simplified and why. Not: this person rebuilt Netflix's ad server.
11 · Scope
Naming omissions explicitly is part of the argument. An omission you can name is a scoping decision; an omission you cannot is a gap.
Each of these would be a reasonable thing to build. None of them would make the AI foundation on top of it any more convincing, which is the only test that mattered.
12 · Evidence
The project's stated thesis is "AI velocity with provable quality", so Level 0 publishes its own numbers before any AI exists to be measured.
This is the strongest evidence that the substrate is not theatre. On its first day of running, the simulator found three genuine defects in the platform it was pointed at.
Integer, which is a Postgres INTEGER — capping a daily budget at 2,147,483,647 micros, about $2,147. The budget-runaway scenario tried to exceed it and Postgres raised a range error. The ceiling is now stated in the API contract as a validation bound; the real fix, widening the column to BIGINT, is exactly the change that ADR-0002 is waiting on migrations for.targeting_mismatch. Seeding is now idempotent per campaign, by name.And a fourth, in the gate itself: it failed on its first run because it read its baseline from the response of the call that changed the state it was measuring.
None of these would have been found by a test suite. All four came from running the real thing under real load and looking at the numbers — which is the entire argument for building a substrate.
13 · Reference
| Term | Meaning in this system |
|---|---|
| ad request | One opportunity to serve an ad: a member context plus a slot. |
| slot | The ad break being filled — duration, content rating, content categories. |
| campaign | An advertiser's booking: budget, flight window, targeting, exclusions, cap, creatives. |
| creative | One ad asset belonging to a campaign, with a duration that must fit the slot. |
| flight window | The period between starts_at and ends_at during which a campaign may serve. |
| targeting | Country, device type and content rating the campaign will accept. Empty means unrestricted. |
| brand safety | Content categories the advertiser refuses to appear beside. |
| frequency cap | Maximum impressions of one campaign to one member per day. |
| pacing | Spreading a daily budget evenly across the day so it is not exhausted early. |
| fill / no-fill | Whether an ad request produced an ad. Fill rate is the share that did. |
| impression | One ad actually played. The only event that costs the advertiser anything here. |
| CTR | Click-through rate: clicks divided by impressions. |
| micros | Millionths of a currency unit, stored as integers. Money is never a float. |
| trace | The per-candidate audit trail on every decision: which campaign lost, to which rule. |
| scenario | An injectable failure mode, applied by changing real configuration. |
| trigger | The pre-declared condition that would reverse an architectural decision. |
14 · Downstream
Every artifact built in Days 1–5 becomes an input to something later. This is the payoff for choices that looked like over-engineering at the time.
| Level | What it consumes from Level 0 |
|---|---|
| 1 · Context layer Days 6–10 | The corpus itself: four services of typed Python, five decision records, five devlogs, the Level 0 runbook, four OpenAPI specs. All written because the project needed them — which is why the retrieval evaluation means something. |
| 2 · Dev agents Days 11–15 | A real codebase with enforced conventions. Generated code must pass ruff and mypy --strict; generated tests must survive mutation testing; the PR reviewer is grounded in the ADRs and the standards doc. |
| 3 · Ops agents Days 16–20 | The whole reason the substrate exists: structured logs with named rejection reasons, dimensional metrics, and five injectable incidents with real causes. Triage, correlation and root-cause analysis against evidence, not fixtures. |
| 4 · Orchestration Days 21–25 | A system small enough for parallel agents to change coherently, with a test suite fast and hermetic enough to gate every one of their proposals. |
| 5 · Platform & launch Days 26–30 | The one-command stack behind the ten-minute fresh-clone target, and the Level 0 gate as the first published row on the eval scoreboard. |
Level 0 ends with four services running, real traffic flowing, dashboards live, five incidents stageable on demand, and a gate that says 11 out of 11. From here the substrate stops being the thing under construction and becomes the thing the AI knows about.