URBANKIT/STUDIO
    Sign inFREE TOOLS · NO SIGNUP
    URBANKIT/STUDIO · EST. 2026 · ONLINEFREE · BROWSER-ONLY · NO TELEMETRY
    Architecture notes

    Architecture

    This is the reasoning behind the product, kept as one document in the repository and rendered here from that same file, so the site and the source can never disagree. It covers the one decision everything else follows from, how the code is split so the logic that matters is testable, and why endpoint liveness, answer correctness and the legality of a field are three separate guarantees, each proven on its own surface: liveness on status, correctness on benchmarks, legality in the atlas itself.

    This is a working note on how UrbanKit Studio is built and why, written for someone who might have to operate it. It is organized around the decisions that were hard, not around the directory tree. Where a decision turned out to be wrong, the wrong version is here too.

    1. What it is

    UrbanKit Studio indexes the public ArcGIS REST endpoints that US counties run for their parcel data: 155 counties across 50 states, 155 endpoints, with each county record carrying the layer URL, its searchable field names and human labels, a working sample query, contact details and license (public/data/atlas/index.json, version 0.3.0). That registry is served through four surfaces that share one implementation: a website with a page per county, a metered REST API described by public/openapi.json, an offline npm SDK (@urbankitstudio/atlas, 0.6.0), and an MCP server exposing eight tools to agents (hosted at POST /api/mcp, plus a stdio build as @urbankitstudio/mcp-atlas, 0.1.6). Nobody scraped these endpoints; each one was found and queried by hand, which is the reason the registry is 155 counties and not 3,000.

    Rough map of the code:

    PathWhat lives there
    public/data/atlas/*.jsonThe registry. Source of truth for everything else. 51 files, 596 KB.
    src/lib/*-core.tsPure logic: no I/O, no clock, no network. Vitest covers it.
    api/_lib/*.tsThe impure half: Supabase, Upstash, Census, county ArcGIS servers.
    api/atlas/*.tsThe five REST handlers.
    api/cron/liveness.tsThe endpoint prober. No shared imports.
    packages/atlas, packages/mcp-atlasThe two npm artifacts.
    scripts/validate-atlas.mjsThe registry's schema and content gate, run in CI.
    public/data/benchmarks/*.jsonCorrectness reference parcels and the last run's observations.

    2. The core decision: index metadata, proxy the query

    The atlas stores what a county's endpoint *is*, not what it *says*. Permanently stored: URLs, field names, labels, sample queries, contact info. Not stored: parcel records. A lookup resolves an address to a county, then queries that county's own server at request time and returns its answer.

    The alternative was the obvious one: crawl 155 counties nightly into Postgres and serve from there. That is what a parcel data company normally does, and it would have made everything below section 4 unnecessary.

    What indexing buys:

    • No data broker exposure. There is no owner name database to breach, subpoena, or accidentally resell. The compliance surface is a list of URLs.
    • No staleness by construction. Cook County reassesses on a three year triad cycle and updates its layer when it updates it. A proxy always returns whatever the county has right now, so "how old is this" is the county's answer, not ours.
    • The county stays the system of record, which is the position that survives a county changing its mind about publishing.

    What it costs:

    • Availability is inherited. If Prince William County's MapServer is slow, the product is slow for Prince William County. There is no local copy to fall back on.
    • Latency is a round trip to a government server that was not built for this.
    • There is no history. I cannot answer "who owned this in 2019" because I never kept 2019. A crawler would have that asset and I traded it away.

    The marketing version of this claim is not true. The paid paths do route through our servers and do cache the county's response: 1 day fresh with a 7 day serve-stale window for parcel results, 7 days and 30 days for geocodes (api/_lib/enrich.ts, api/_lib/radius-query.ts). That cache exists so a flaky county endpoint degrades a row to stale: true instead of failing a paid batch, and so metered usage can be reconciled. It is operational and short lived, and it is never compiled into profiles. In the free browser lookup the parcel query itself does not pass through our servers: the browser contacts the county's layer directly, and only the Census geocode is proxied (api/geocode.ts).

    3. Pure core, impure glue

    Every file named *-core.ts under src/lib/ is pure. No fetch, no clock, no environment, no edge globals. The I/O sits in api/_lib/ and imports it.

    • src/lib/ratelimit-core.ts owns window math, IP parsing, and result shaping. api/_lib/ratelimit.ts owns the Upstash pipeline call and the Lua.
    • src/lib/enrich-core.ts owns field classification, address parsing and match scoring. api/_lib/enrich.ts owns the Census call and the county spatial query.
    • src/lib/capability-core.ts owns per field capability. Nothing in it does I/O.
    • src/lib/radius-core.ts owns request parsing and the geometry. api/_lib/radius-query.ts and api/_lib/radius-charge.ts own the rest.

    The reason is narrower than "testability" in general. Vitest is configured over src/, and the money path decisions are exactly the ones I need to test without a network: whether a reservation clamps to remaining quota, whether a row counts as sellable, whether a missing column means the law forbids something. clampReservation in ratelimit-core.ts is the clearest case. It is nine lines. It is shared by POST /api/atlas/radius and the MCP radius_owners tool, and it lives in one place because the previous version was duplicated clamp arithmetic at two call sites, which is how a defect shipped on one radius surface while the other looked fine.

    4. The three guarantees

    Most data products collapse three different questions into one green checkmark. They are not the same question, they fail independently, and two of them cannot be answered by a machine at all.

    • Liveness. Does this endpoint answer right now?
    • Correctness. Is the answer right?
    • Legality. Are we allowed to serve this field for this county?

    4.1 Liveness

    api/cron/liveness.ts probes every county with a public endpoint twice: a metadata read (?f=json) and a real single row query. It compares the layer's live field list against the registry's declared searchFields, so a county silently dropping a column registers as field-drift rather than as success.

    The cadence is scheduled by pg_cron jobs inside the Supabase database (migration 20260816120000_atlas_liveness_pg_cron.sql, applied by hand alongside an atlas_cron_secret Vault entry), which call the same endpoint through pg_net with the bearer read from Vault at call time. A full sweep every two hours, split into four shards via ?shard=i-of-4 a minute apart, because the single call full sweep rode the function's 60 second maxDuration and tipped over it on a slow morning. The shard split is a deterministic modulo, so the union of the four is exactly the full set and slow hosts spread across shards instead of clustering by file order. Separately, at :15 and :45, ?only=degraded re-probes only counties whose stored status is currently non-ok, so a failure gets rechecked every 30 minutes while healthy counties keep the polite two hour cadence. Roughly 1,800 requests a day total. Vercel's own daily cron at 08:00 UTC fires shard-less as an independent backstop, and the old GitHub Actions scheduler (.github/workflows/atlas-liveness.yml) stays as a manual dispatch only. It moved because GitHub bills each Actions job as a full minute, so sixty sub-minute pings a day were most of a free account's monthly allowance, and when that allowance ran out on 2026-08-15 the probes simply stopped; a trigger that lives next to the data it writes has no such failure mode.

    Most of that file handles the ways a probe can be wrong about a healthy server:

    • returnCountOnly=true forces a full layer scan. Pinellas FL took 22.6 seconds on the count path and under 0.4 seconds on a resultRecordCount=1 field query. Three healthy counties were chronically flagged query-timeout by our own probe shape.
    • Some ArcGIS Enterprise instances reject a tautological where=1=1 with a 400 while the county's real search works fine (Charleston SC). resolveQueryPredicate prefers the endpoint's own declared sampleQuery predicate, falling back to 1=1, and refuses a sampleQuery pointing at a different host.
    • Orleans Parish LA and Hinds County MS reject the identical query when outFields is a named field or omitted, and accept it with outFields=*.
    • Shelby County TN requires legacy TLS renegotiation, which Node refuses by default. It gets a node:https client with SSL_OP_LEGACY_SERVER_CONNECT, scoped to an exact hostname allowlist.
    • Orleans Parish sends an incomplete certificate chain. curl succeeds because it fetches the missing intermediate via AIA; Node does not do AIA fetching. The fix pins that one Sectigo intermediate alongside tls.rootCertificates, per request, on a dedicated client. Not by weakening verification.
    • Three statewide shared layers (NY, NJ, Prince William VA) are genuinely slow while fully correct, so they get a per host 15 second budget instead of the 5 second default. Per host, never a blanket bump, or the budget stops detecting anything.

    Failures retry up to three attempts with a 400 ms delay, but only when transient: a 4xx or a field drift is a real failure and burns no retry budget. The whole retry budget is capped at 40 seconds of wall clock so a bad morning cannot push the run past maxDuration.

    Results land in two Supabase tables. endpoint_status is upserted per county and holds only the current state. endpoint_status_history is append only, one immutable row per county per run, roughly 57k rows a year, with RLS on and no public policy. It is not backfillable: the value is the length of the series, so it accrues or it does not exist. Auto remediation opens a GitHub issue after three consecutive failures and closes it after three sustained ok observations, and the close path reads the history table rather than adding a counter column, because the signal is already there.

    Two design rules matter more than any of the above.

    The registry's `status` field is not health. It is a curation stamp with a date on it, set whenever a human last touched that entry. On 2026-08-04 the REST API and the MCP server both reported Will County IL as status: "live", lastVerified: "2026-06-27" while the cron had it down with three consecutive failures that same afternoon. A five week old assertion was being served as current fact, on the exact surface whose selling point is that endpoints are verified. The probe results had existed the whole time. Nothing read them. api/_lib/endpoint-health.ts exists because of that, and every served response now carries both, labelled, never merged.

    A null observation never means healthy. loadEndpointHealth() returns an empty map when the table is unreachable, never a throw and never a guess, and callers must read "absent" as "unknown". Three places enforce that, and each was a real bug:

    • The health cache treats zero rows as failure, not as an empty atlas. {} is truthy, so an earlier rows ? check cached a PostgREST 200 [] as a success. That is reachable: an RLS policy flipped to using (false) with the GRANT intact answers 200 [], where a revoked GRANT answers 401.
    • loadEndpointHealthBounded(800) caps the caller's wait on the billed lookup and county paths, where health is additive rather than the point of the call. Giving up yields an empty map, which reads downstream as no observation. A read I abandoned tells me nothing about the endpoint.
    • The published contract says so. openapi-schemas.test.ts asserts that the ObservedHealth schema is nullable and that its description matches /never means healthy/i, because a client that reads null as healthy has the original defect back with extra steps.

    /api/atlas/status is ungated, cached 60 seconds with a 240 second stale window. Putting "is your data actually up right now" behind an API key would undermine the answer it gives.

    4.2 Correctness

    This is the weak one and I would rather say so plainly than let the liveness work imply coverage it does not have.

    Liveness proves an endpoint answered. It does not prove the answer is right. Nothing in this system currently checks that the owner name returned for a given address is the owner of record, that the parcel the buffered spatial query picked is the correct parcel, or that the APN matches the assessor's. Those are three separate failure modes and all three are unmeasured today.

    The disambiguation logic is where correctness lives. The Census geocoder returns a street interpolated point sitting in the road right of way, so a naive point in polygon query against a parcel layer misses. enrich-core.ts buffers 30 meters, pulls up to 40 candidates, then scores each candidate's site address against the geocoded address by house number and street tokens, returning a confidence of exact, house-only, ambiguous or none. house-only exists for layers that publish no street name at all (Travis County's "Situs Address" field holds the house number alone): the match rests on a house number unique within the buffer, and the value is kept distinct from exact so a caller needing street-level proof can refuse it. An "Owner Address" field was once classified as the site address and the match ran against it, returning a wrong owner, so SITE_EXCLUDE now bars any field matching mail|owner|own[_\s]|grantor from being read as the situs address.

    Ground truth now exists, in one county. public/data/benchmarks/ground-truth.json holds a seed of hand-sourced reference parcels, each recording the address, the parcel identifier an independent county source publishes for it, a URL that returns that identifier, and the exact layer the row grades. scripts/run-benchmarks.mjs puts every row through the real pipeline and records what came back; it scores nothing. src/lib/benchmarks-core.ts is the scoring, pure and unit tested, and /benchmarks publishes the result. The split matters: the observations ship next to the page, so the published numbers are arithmetic anyone can redo over evidence they can read.

    Three rules are enforced rather than described. A metric with an empty denominator is null, never zero, because zero is a measurement and absence is not. A row whose parcel identifier was only ever established by the endpoint it grades is excluded from every comparison against that identifier, not flagged and counted anyway. And every metric carries the rows it could not measure and why, because a score computed over the easy remainder with the hard cases dropped in silence reads as coverage.

    What that buys is narrow and the page says so. The seed is almost entirely Cook County IL, so a pooled percentage is a Cook County percentage; owner name correctness is unmeasured on purpose, since holding a file of owner names to check against would be the owner database this product does not keep; and 144 of the 151 monitored counties have no reference rows at all. CI replays the recorded observations on every push rather than hammering county servers, through validate-ground-truth, which fails when an observation drifts from the row it measured. The live re-run is scheduled weekly and watched by a staleness check that goes red past ten days; both were merged on 2026-08-18 and neither runs until the account's monthly CI allowance resets on 1 September 2026, so today's snapshot was still produced by hand.

    The declined column in that table is the part worth reading twice. A county can score zero by refusing every row, which is not the same failure as answering every row wrongly, and only one of the two puts a stranger's parcel in front of somebody about to serve a legal notice. Across the scored rows the pipeline has never named a wrong parcel. Where it cannot verify a candidate against a published site address it returns nothing, and some of that is structural, because a county whose layer publishes no site address gives it nothing to check against.

    4.3 Legality

    This is the part I got wrong.

    A trial user subscribed, ran Los Angeles County for owner data, and got nothing back. The endpoint was healthy the entire time and the atlas correctly said so. What the data model could not express was that the field he paid for was never going to arrive. The registry tracked availability. It did not track permission. He cancelled seven minutes after subscribing, and there was no request log to reconstruct what he had called, so the reason had to be worked out by hand from the registry.

    Auditing that turned up something worse. Six California county records asserted in prose that California Government Code 7928.205 was the reason their layer had no owner column, and Los Angeles County's note claimed the section "broadly restricts owner name and mailing address from public REST endpoints across California". Reading the section at primary source: 7928.205 protects the home address of elected and appointed officials. It is not a general bar on assessor owner data. That claim had shipped through the SDK, the REST API, and every MCP response carrying notes, and nothing in CI could see it, because it was prose.

    Our own registry disproves it three ways, inside one state, under one statute:

    CountyBehaviourWhat that is
    San DiegoPublishes OWN_NAME1, OWN_ADDR1 and real names on a public keyless layerThe statute does not bar owner data
    San BernardinoPublishes an OwnerName column where every row's value is the literal string Protected Per CA Gov Code 7928.205The county's own over-broad reading, applied server side
    Los AngelesPublishes no owner column at allA publication choice, not a legal one

    One statute, three behaviours. A state level rule would have been wrong about all three counties.

    So src/lib/capability-core.ts splits the answer into seven fields (apn, owner_name, owner_mailing_address, situs_address, geometry, land_use, zoning) and four statuses, and the statuses are not shades of one idea. They are different claims with different evidence and different durability:

    • not_published is a fact about a service. The field is absent from the layer's documented field list. Derived mechanically, recomputed whenever the registry changes, cheap to be wrong about and cheap to fix.
    • county_cited_statute is the county's claim, attributed to the county. San Bernardino cites 7928.205; we report that it does, we do not adopt it. The schema enforces the attribution: basis.type = "county_cited_statute" without attributedTo fails validate-atlas. Without that rule, "San Bernardino cites 7928.205" decays into "7928.205 says", which is precisely how a narrow statute became a statewide bar in our own shipped copy.
    • restricted is a legal claim we are making ourselves. Entered by a human, carrying a citation that human has read. Never derived, never inferred from an empty response, never copied from a neighboring county's label. status: "restricted" with neither a citation nor an attribution fails validation, because a legal claim with no source is an opinion.
    • available means the field is in the documented list so a query can ask for it. It is not a promise that every row is populated.

    The invariant that holds all of this up is one line in a test:

    deriveFieldStatuses can never return "restricted", asserted across all 155 counties in the shipped registry (src/lib/__tests__/capability-core.test.ts:52).

    A machine reading a list of column names cannot read a statute. The day it starts guessing is the day the product tells a paying customer the law forbids something when the county chose not to publish it. That is the mistake that started this, encoded so it cannot recur silently.

    The obvious fix for the six California counties was wrong. The obvious fix was to author six capabilityOverrides records. That would have encoded a legal claim no verified statute supports. The derived answer we already produce, not_published from each endpoint's own field list, is factually correct for all six. So the prose was corrected to state only the fact, and the explanation of why California counties differ now lives once, on the state page, instead of as six copies drifting apart. Exactly one county in the whole registry carries a reviewed override today: San Bernardino, on owner_name. That number should stay small. A model whose exception table is growing is a model that has become a lookup table.

    5. Billing and metering

    The principle is one sentence: a customer is never charged for a field the county cannot provide.

    Quotas are per user, keyed on the immutable user_id, never on api_keys.id, because keying on the key let a user reset their counter by revoking and reissuing. The window is tier aware: the Individual plan is sold per calendar month, free and pro are per UTC day, and one function (quotaWindow) decides which.

    Bulk enrichment charges one unit per address up front (keyQuota with cost = addresses.length) and refunds after. shouldRefundPlanEnrichment in src/lib/enrichment-billing.ts makes that decision once, shared by REST bulk and the MCP enrich_address tool. It refunds our side and transient failures (geocode-unavailable, atlas-unavailable, county-unavailable), and it refunds a row that returned nothing sellable from a county that structurally cannot serve owner names. The order of those checks is load bearing: a row that *did* release the sellable deliverable is billed even in an owner-name-unavailable county, because Pima AZ and Thurston WA return mailing addresses without owner names, and refunding those gave the product away free.

    The radius product is harder, because the parcel count is not known until after the query, and the query is the expensive part. The shape is reserve, query, settle (api/_lib/radius-charge.ts):

    1. Reserve up front, before the county call, so no billable unit is handed out uncounted. The reservation is sized by clampReservation(cap, peek), which is min(cap, max(1, remaining)). Without it, an Individual subscriber got roughly one radius query a month: the flat reservation is 500 and the monthly quota is 500, so before + 500 <= 500 holds only at before === 0.
    2. Every non billable exit settles the operation to zero before returning: over quota, no geocode, county not covered, our own failure.
    3. A covered answer settles down to the rows actually served, then releases the data.

    Settlement is a Lua script with a per operation marker written before the INCRBY, so an indeterminate reserve can always be reconciled: a settlement sees the marker if and only if the reservation reached the mutation. Retries are idempotent. Every completed call leaves an s: marker, including exact cap calls that refund nothing, because short circuiting the zero refund case parked those at r:<n> forever and made them replayable.

    I wrote one trade into that file explicitly. If Redis is reachable at reserve and unreachable across both settle attempts, the rows are still released. A review flagged withholding them as a blocking defect: the reservation is already confirmed and greater than or equal to the amount billed, so the customer has already paid, and refusing the data charges them for nothing. The reported charge falls back to the full reservation with zero credited. The accepted cost is that a retry during that outage window can re-serve data the caller already paid for, plus one extra upstream county call. That is the right side of the trade against billing someone for a response they never receive.

    Limits are layered by what they protect. bulk puts a 60 requests per minute per IP cap *in front of* API key validation, because the gate's failure path performs an awaited service role INSERT into usage_events, so credentialless spam would otherwise convert directly into unbounded database writes. That limiter fails open, and when it degrades the endpoint tag is dropped so the gate's insert does not fire at all: the denied attempt observability is sacrificed to keep the bound. Everything else fails open, because these are abuse prevention on public endpoints and not a security control. The one exception is the metered bulk path, which fails closed with a 503 when the limiter is degraded, because you must never hand out billable units you cannot count.

    6. Guard integrity

    The recurring failure in this codebase is the check that stopped checking and stayed green. Three examples.

    The contract test that pinned two schemas and not the third. src/lib/__tests__/openapi-schemas.test.ts asserts that public/openapi.json documents exactly the county fields the handler returns, no more and no fewer, in both directions, by shaping a real county through shapeCounty and diffing the key sets. The same rule existed for Endpoint. It did not exist for ParcelRow, which drifted to documenting 7 of its 15 fields. Among the eight missing was owner_data_available, which is what a caller branches on to know whether owner data is possible at all, and what plan quota refunds key off. A paying integrator reading the published spec could not discover the field that determines both what they get and what they are charged. A contract test that covers every schema except the one on the money path looks complete and is not.

    That file also carries vacuity guards for itself. On an OpenAPI parameter, pattern sits under schema while example sits one level up beside it, so a walker looking for both in the same object finds zero pairs and passes green. The test asserts a minimum count of pairs found, so that specific emptiness fails loudly. The defect that prompted it: the FIPS pattern shipped as ^d{5}$ after a lost backslash, so the published contract rejected 17089 and accepted ddddd, the exact inverse of the handler, and every existing test passed the whole time because none of them ran a value through a pattern.

    An exemption list that could only shrink. I added a rule to validate-atlas.mjs: prose citing law must be backed by a structured, validatable record, since structured records are checked and prose is not. Six counties already violated it, so the rule shipped with a PENDING_LEGAL_REVIEW allowlist that could only shrink: a county left by gaining a reviewed record, and a *new* county citing law failed immediately rather than joining the queue. When those six were corrected the list reached zero and the allowlist was deleted, making the rule unconditional. It is gone because it emptied, not because it was waived. Sabotage verified by injecting a law citation into Sonoma, a formerly exempt county, and watching it fail.

    A detector for the class of bug that makes a rule go quiet. The commit correcting those six county notes used a regex ending in [^.]*\. and 7928.205 contains a period, so each replacement stopped at 7928. and left 205 restricts owner and mailing-address data from public REST endpoints. dangling in five counties. Sonoma's fragment resurrected almost the entire debunked claim. validate-atlas stayed silent, because its law-in-prose rule anchors on the citation phrase, and the botched replace had removed exactly that phrase. The rule went quiet because the edit was broken. A passing check on mutated content proves the check no longer matches, not that the content is right. The independent signal added is that county prose may not contain a close paren immediately followed by a digit, which is the precise signature of a replacement that stopped inside a citation. The first version allowed whitespace and false-positived on a phone number in Stark County OH, so it was tightened to the no-whitespace form and confirmed at zero matches across all 155 counties.

    The same instinct runs through CI (.github/workflows/ci.yml). tsc --noEmit exits 0 just as happily over an empty file list, so the step that typechecks the test suite is followed by a step that counts how many test files the program actually loaded and fails under 100. That guard exists because tsconfig.app.json excludes src/**/__tests__/**, so the main typecheck had never looked at a single test file and exited 0 with 15 real type errors in the suite, including 10 fixtures missing a field production code had added. Lint is a ratchet against a recorded baseline of 92 rather than a gate at zero, because a gate that must be ignored on every pull request teaches people to ignore gates, and the ratchet fails loudly if it cannot parse its own metric.

    Current state on main: typecheck clean, 1,727 tests, lint at baseline, validate-atlas passing 155 of 155.

    7. Surfaces and consistency

    Four surfaces, one registry. The things that keep them from drifting:

    • One classification, one biller. The SDK exposes reviewedCapability and isReviewedUnservable, which read the human entered override. It does not derive the mechanical half from searchFields, even though it ships the data to do so. A second implementation of that classification would drift from the one the API bills against, and a consumer would have no way to tell which was right. The SDK says to read endpoints[].searchFields for the mechanical answer and treat a reviewed entry as outranking it.
    • One copy for one refusal. radiusNotCoveredMessage in radius-charge.ts returns the not-covered explanation used by both the REST handler and the MCP tool. The handlers differ only in shape: REST maps outcome kinds onto HTTP statuses and headers, MCP maps the same kinds onto sentences an agent can read.
    • Derived counts, never typed ones. COUNTY_COUNT, ENDPOINT_COUNT and POPULATED_STATE_COUNT in src/lib/atlas-data.ts come from atlasIndex.totals, after an audit found hardcoded 117 and 128 counts drifting across /developers and /mcp.
    • Derived artifacts rebuilt on push. .github/workflows/atlas-artifact-sync.yml regenerates the npm package's bundled data, the download directory, llms.txt, the search index, sitemaps and the prerender route list whenever public/data/atlas/** changes on main. Vercel rebuilds the site from source every deploy, so the site was always current, but the npm package is built from the committed copy, which is how @urbankitstudio/atlas@0.5.1 shipped Montana pointing at a host that had stopped resolving.
    • The published health URL is a value, not a string in a doc. The SDK exports LIVE_STATUS_URL so consumers do not hardcode it, and the bundled status field is documented as a publish-time claim with a pointer to where to ask what is true now.

    8. What I would do next, and what I would do differently

    Correctness benchmarks now exist, and cover one county. This was the largest gap and section 4.2 says how far it has closed. /benchmarks publishes a real measured number per metric, with the sample size on every one, which is enough to stop the liveness work implying a guarantee it does not make. It is not enough to speak for the atlas: the seed is concentrated in Cook County IL, today's snapshot was produced by hand, and owner name correctness has no number at all. Recorded fixtures now replay in CI, and the scheduled re-run plus its staleness check are merged and waiting on the September CI reset. What is still missing is reference rows in more states, so a pooled figure means something beyond Cook County, and a fix for the rows where a site address exists and the match still could not be made.

    The atlas is statically bundled into the client, and that caps growth. src/lib/atlas-data.ts imports 51 JSON files synchronously so react-snap prerenders real per county content instead of a loading state. That is the right call for SEO and for a page per county at 155 counties and 596 KB. It is the wrong call at 1,500 counties. The fix is a per state code split or a data route with the prerender reading from disk, and it should happen before the next large county batch, not after.

    The usage ledger now records every keyed surface, and for a while it did not. logUsageEvent was first called only from api/atlas/bulk.ts and from the gate's rejection path, so api/admin/usage.ts, which aggregates that table into a 30 day chart, showed an empty chart for any customer who was not using bulk. That was the direct descendant of the incident in 4.3: the whole reason usage-log.ts was written is that a cancelled trial could not be explained. Radius (REST and the MCP radius_owners tool), lookup (free, plan, anonymous, and the PAYG single lookup) and the MCP enrich_address tool now write one row per gated request, placed after the billing outcome is final and derived from the same variables the response reports. Units mean one thing on every surface: what the caller's meter was debited for the service delivered, after refunds. A PAYG answer is one metered unit; a keyed free or plan answer is the quota-window allowance units it drew (per day on free and pro, per month on individual; one for a lookup, the parcel count for a ring); an anonymous answer under the per-IP throttle is zero, because that throttle is not an allowance anyone holds, and so is a keyed answer the gate let through under a degraded limiter, where the draw is indeterminate rather than absent (the increment can commit before the reply fails), so that row claims nothing and carries its own reason, ok-unenforced, to stay countable instead of passing as a confirmed no-draw answer. A PAYG replay records not-billed rather than a second billed unit. A settlement that cannot be confirmed records refund-failed: with the full reservation as units when a ring was served, since the caller may really be short that many, and with zero units when the settle-to-zero of a failed request did not confirm, since the true figure is unknown and phantom units would overstate the chart during the incident it should explain. county is not wired yet. It is a metadata read of our own index, but a keyed call does draw one allowance unit through the shared gate, so until it writes a row a per-key unit sum understates allowance use by the number of county calls; that gap is recorded here rather than papered over. The admin read that draws the chart orders newest first under its 20,000-row cap and reports truncated when it hits it, and a window made only of zero-unit discovery rows counts as data, not as an empty chart. With those in place the chart can carry a pricing decision.

    What I would do differently. I would have modeled capability as more than a boolean at the start. The registry answered "does this county have an owner field" from day one, and every downstream feature (billing, refunds, the radius not-covered message, the SDK surface) was built on that answer before anyone asked what the field's absence *meant*. Retrofitting a four state model with typed evidence through four surfaces and a billing path cost far more than getting it right once would have, and the trial user paid the tuition. The general lesson is narrower than "model things carefully": when a field's absence has more than one possible cause, and the causes have different consequences, the type has to carry the cause. A boolean that collapses "the county did not publish it" and "the law forbids it" will eventually be read as the second when it meant the first, and it will be read that way by a customer.

    The build checks this page against the repository's ARCHITECTURE.md on every deploy, and fails rather than publish a difference.