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: 303 counties across 50 states and DC, 322 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.28), and an MCP server exposing nine tools to agents (hosted at POST /api/mcp, plus a stdio build as @urbankitstudio/mcp-atlas, 0.2.23). Since 2026-09-21 neither package publishes from this repository: npm attaches a provenance attestation only on an OIDC publish from a public repo and this one is private, so each publishes from its own public repo. mcp-atlas also moved its source there; atlas is still authored here, because its data/ is generated from the parcel registry, and is mirrored out by .github/workflows/atlas-sync-push.yml. Nobody scraped these endpoints; each one was found and queried by hand, which is the reason the registry is 303 counties and not 3,000.

    Rough map of the code:

    PathWhat lives there
    public/data/atlas/*.jsonThe registry. Source of truth for everything else. 52 files, 1335 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 six REST handlers.
    api/cron/liveness.tsThe endpoint prober. No shared imports.
    packages/atlasThe npm SDK. Its data is generated from the registry above, which is why it still lives here. The MCP server's package moved out to urbankitstudio/mcp-atlas, where it publishes itself.
    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 303 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). On those paid owner paths a fresh cache hit saves a county round trip, but a stale entry served because the live call failed is withheld: the stage reports the refundable unavailable code and a refund is attempted (the radius HIGH-3 rule, applied to enrichment by #916). The settlement fields say whether that refund landed: bulk refunded, MCP plan_quota.refund, radius charged. The cache also lets metered usage 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), plus, in a county with a notice-list entry (or, for a statewide layer, while an entry exists in its state, and for a layer the atlas does not know, while any entry exists), a check of the parcel numbers on screen, never a name or an address, not stored or logged (section 4).

    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 2,100 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.

    POST /api/atlas/request (and the MCP request_county tool) is ungated and free: asking for a county we do not cover should never cost a key. It writes to county_requests, which can hold an email the requester chose to give, so that table is service-role only and kept apart from the identity-free demand tallies.

    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 233 of the 240 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 303 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. The reviewed overrides, the ones a human entered with a citation (basis.type of statute or county_cited_statute; the mechanical endpoint_schema ones are derived and do not count), are few: the twelve Kentucky counties whose owner and mailing columns are available by statute, nj-monmouth on owner_mailing_address, and the three county-cited restrictions (San Bernardino, Bergen, Essex). That number should stay small. A model whose exception table is growing is a model that has become a lookup table.

    Two protections sit beneath the capability model and apply to every row on every surface (bulk, radius, hosted MCP, the free tools). The pure rules are src/lib/withhold-core.ts; the notice list is looked up only in api/_lib/suppressed-parcels.ts. They change the served view only: the billing decision, refund and meter read the raw row first. On bulk and enrich_address a county-redacted row bills exactly as it would have (its parcel id is still served). On radius, which bills one unit per returned row that carries an owner name or mailing address, a county-redacted row is served with neither and so bills 0 (isSellableRow judges the served view). A notice-suppressed row is never charged (Leo, 2026-10-09): the server flags the raw row with the same lookup (withNoticeFlag) before billableOutcome reads it, which answers notice-suppressed, so bulk refunds the unit, radius settles the row at 0 and MCP enrich_address refunds the plan unit or never meters PAYG. (1) When a county publishes a redaction marker in the owner, mailing or situs value, the row carries withheld: "county-redacted". A marker is a value, after NFKC, invisible characters removed, whitespace collapsed and outer punctuation stripped, made only of the words CONFIDENTIAL, REDACTED, WITHHELD, PROTECTED, SUPPRESSED, RESTRICTED, NOT, DISCLOSED, OWNER, NAME, INFORMATION, INFO, PER, KRS, STATUTE, REQUEST, BY, SEE, PVA, RECORD, AND and section numbers (61.878(1)(a) and 61.878 (1)(a) included; a plural "(S)" belongs to its word, so OWNER(S) is OWNER), with at least one of CONFIDENTIAL / REDACTED / WITHHELD / PROTECTED / SUPPRESSED / RESTRICTED / "NOT DISCLOSED", and a NOT just before one of the six single words negates it ("NOT RESTRICTED" and "NOT CONFIDENTIAL" are not markers). So "RESTRICTED", "RESTRICTED OWNER" and "RESTRICTED PER 132-1.2" are markers; "CONFIDENTIAL INVESTMENTS LLC", "SEE BROTHERS LLC", "RESTRICTED ACCESS LLC" and "RESTRICTED INDIAN LAND 1234" are names and are served. One function (carriesRedactionMarker) does the scan for the paid rows and the free tools, over the same columns: owner, mailing and situs (a sale price "NOT DISCLOSED" withholds nothing). A mailing value that is wholly masked (after NFKC and the invisible characters removed: only asterisks and separators, at least one asterisk, at most a trailing ZIP or ZIP+4, or a run of three or more "#"; src/lib/mailing-mask.ts) names no one. The paid rows (enrich, bulk, MCP enrich, radius) carry it as null, so the row billing reads already has it as null; the free tools null it in a mailing column, one whose name or label contains "mail" or the registry's mailing field. It does not withhold the row. A withheld row keeps an ALLOWLIST: on a paid row the request metadata and (unless notice-suppressed) the parcel id; on a free-tool row numeric value, acreage and area columns, land-use / class / zoning columns and (unless notice-suppressed, or a value the check would refuse to send, such as "PO BOX 12") the primary parcel id. Everything else is null, including situs columns the classifier does not know, alternate ids, OBJECTID and a GeoJSON feature's id and bbox. (2) A notice-suppression list, src/data/suppressed-parcels.json, names parcels by county FIPS and normalized parcel id; a match serves nothing identifying. The process is: a written notice arrives at contact@urbankitstudio.com -> a human verifies it and adds { countyFips, column, parcelId, basis, received } -> node scripts/gen-suppressed-parcels.mjs (also run by prebuild) regenerates api/_lib/suppressed-parcels-data.ts -> deploy. An entry must be the value of the county's parcel-id column, the one the paid classifier reads on EVERY endpoint the county answers from (the primary one and each with an owner, a mailing or a parcel-id column: the radius and enrich answer from any of them and fail over between them) (Boone's PIDN, not its PIN), and column names it: an entry copied from another id column is matched nowhere, so validate-atlas and the generator both reject it (listEntryErrors). Notice entries are not accepted for these counties until their endpoints agree on one id column: Broward, Duval, Hillsborough, Lee, Manatee, Miami-Dade, Palm Beach, Pasco, Pinellas and Sarasota FL (the county's own id beside the statewide layer's PARCEL_ID), and Cumberland and York ME (the owner layer has no id column). No Kentucky county is among them. A test pins the generated file to the JSON, and validate-atlas checks the file's shape.

    The list is SERVER-ONLY. It lives outside public/, and nothing a browser bundle includes may reach it: a list in the browser bundle, hashed or not, is a public list of protected parcels (parcel ids are public and enumerable, so a hash does not hide them). suppressed-parcels-boundary.test.ts walks the import graph from every HTML entry (the site and the app workspace), page and component, reading static, dynamic (any quote), import.meta.glob, new URL(..., import.meta.url) and ?raw imports, and fails if any of them reaches it. The paid surfaces (Edge) call the lookup directly. The free tools query the county from the browser. The same generator writes one client-safe module, src/generated/notice-list-counties.ts: the FIPS of counties with at least one entry and whether any entry exists, never an id (Leo accepted that county-level presence is public; the boundary test names it as the one allowed module). A free tool asks the server ONLY where the list can matter: rows from a county in that set, rows from a statewide layer while a county in its state is in that set, or rows from a layer the atlas does not know while any entry exists. A pasted or converter URL is first looked up in the atlas (resolveLayerCounty, keyed by layerKey: host and path case, a trailing slash or /query, the query string, :443, //, /./ and http/https do not matter): a layer that one county registers, with no scope, is that county's, so an unlisted county's layer makes no call. A layer registered under a scope or by several counties (NC1Map, Nebraska's, New York's and Florida's statewide layers) also holds counties the atlas does not register on it, so it has no county. It does have a state: each of the 17 such layers is registered within one, so it is checked only while a county in that state is listed (a Kentucky entry never touches a Florida or North Carolina layer). Massachusetts registers its two with no county FIPS, so like a URL outside the atlas they are checked while any entry exists. Everywhere else it makes no call and shows the county's rows as published; the county's own marker still applies. With the list empty, as it ships, the free tools make no call at all. Where it does ask, after fetching rows it POSTs { countyFips | null, parcelIds[] } for the rows on screen that need it (each distinct id once) to /api/atlas/withhold-check (src/lib/withhold-client.ts), which answers with the INDEXES to withhold and nothing else; the page applies the pure rules. In a known county a row needs it when it carries an owner, mailing or situs value; with no county, only a parcel row does (an owner or mailing value backed by a parcel id, a mailing line or a situs), so address points, schools, hydrants, polling places and roads are never withheld for the notice reason. An owner column is read in its compact spellings too (OWNNAME, OWN_NAME, OWNNM1, O_NAME, NAMEREAL, BILLNAME); a FIRST_NAME or LASTNAME, like a bare NAME, names the owner only beside a parcel id and a filled situs or mailing column on a layer with no county (by the column name alone), and a FULLNAME (a road, a school) never does by its name alone (a registry label or role can still mark it). Which ids a row CARRIES (recognition) is decided apart from which may LEAVE (transmission), and a row that carries an id which was not sent can never be cleared: it FAILS CLOSED, whatever the sent ids answered (parcelIdCheck's unsent). Recognition is broad: the registry's id column, every column named an id, and every parcel-like column (ParcelJoin, CleanParcelID, GISLINK, HCAD_NUM, PARCEL_REF; Philadelphia's brt_id, "BRT Account Number") that is not a type, size, use, status, date, count, class, link, deed or plat folio, lot, block, row id or address, and the map, PVA, GIS, roll and assessor numbers no rule sends (MAP_ID, MAPNO, MAP_NUMBER, PVA_ID, PVANUM, TAXMAP, GIS_ID, UNIQUE_ID, LRSN, ASSESSOR_ID, ROLL_NO...). Transmission is narrow. On a layer the atlas registers, the id sent is the registry's parcel-id column (the one the server classifies), and the registry's owner, mailing and situs columns (by the paid classifier) count as identity too; on any other layer, every column whose NAME says it holds an id is sent and a match on any one withholds the row (isIdNamedColumn: the atlas registries' own id column names, generated at build time into src/generated/registry-id-columns.ts, so a mirror of Bradford's layer still sends ParcelJoin; TMS, TAXLOT, REID, AIN, UPI, PCN, MBL, MAPBLOCKLOT; and isIdShapedColumn, an allowlist grammar read in one linear pass: PIN, APN, PIDN, PAN, PARCEL_ID, PARCELNO, ACCOUNT, ACCT_NUM, BBL, TAXKEY, PROP_ID, GEO_ID, LOC_ID, PRINT_KEY, SWIS_SBL_ID, MAPTAXLOT, CAD_REFERENCE_NUMBER...; never PARCEL_TYPE, PARCEL_SIZE, a depth or width, a use code, a status, a date, a count, a class, a URL, PARCEL_BLOCK or PARCEL_SUB, never ACCT_LAST, ACCT_TENANT, PARCEL_STR or PARCEL_PRIMARY, and never a house number such as LOCNUM or PROPLOC_NUM). An alias that reads as a name, an address, a mailing line or a situs ("Mailing Address" on a PIN) keeps even a real id's name from being sent; the column is still recognised, so the row fails closed. The one exception is the county's own id column on its registered layer. On a statewide layer both are sent: a list entry is keyed on its county's own endpoint id, so NC1Map sends parno and altparno and Nebraska's layer Parcel_ID and State_PID. A match there depends on the statewide layer carrying the county's id in the format the county publishes; where it does not, the entry protects nobody on that layer. Polk OR's AcctTable_OBJECTID and AcctTable_In_Care_Of are not ids; AcctTable_Account_ID is. No value over 120 characters, none without a digit, none that starts like a street address ("100 OAK ST", "1234 N 5TH ST", "12-14 OAK ST", "1ST AVE", "12TH AV", "N123W12345 MAIN ST") or a grid address ("1234 E 5600 S", "350 S 200 E"), none with a street suffix after a number ("12 OAK DR"), none with a rural route or box word ("RR 2", "HC 61 BX 12"), none starting with "#", and none with a word of three letters or more that is not an id word (LOT, BLK, BLOCK, MAP, SEC, TWP, RNG, SSL, PAR, PCL; "_" separates words) is sent: "MAP 12 LOT 34" is sent, "SMITH 2", "UNIT 12", "P.O. Box 12" and Orleans Parish's address-built PARIDs ("622-SALEXANDERST") are not, and those rows fail closed. NO column that is an owner, mailing or situs column, whose name or label (with or without a join prefix) reads as an address, a place, a mailing line, a care-of or attention line, an agent, a holder or a name, or that is a legal description or a measure (isSendableIdColumn: PARCEL_ADDR, PARCELADDRESSCITY, AcctTable_MailZip, AcctTable_In_Care_Of, ACCT_HOLDER, a FLD7 aliased "Parcel Owner Name", NC1Map's parcellgl, Acct_Acres) is ever sent, the registry's id column included; the real ids LOC_ID, LOCATOR, PIN, PARCELID, PARCEL_ID, BBL, TAXKEY, PROP_ID, ACCOUNT and ACCT_NUM are kept. That is what makes "names and addresses are never sent" true. That endpoint requires our Origin (a request without one is refused), takes at most 500 ids and 64 KB (a bounded byte read), needs no key, is never billed and is not in the public OpenAPI. It logs no request content: no id, no county, no count. A 5xx answer (503 when the limiter is unreachable) writes the route, status and error code to the server-error log; the limiter keeps a per-IP id count in Upstash for its window; the platform's own request log records path, status and client IP, never the body. It FAILS CLOSED on the page, per chunk of 500 ids: rows with an id in a chunk the server did not answer, or a row naming someone with no parcel id to check, are shown without owner, mailing or situs, the rows from answered chunks are kept, and the page says owner details are unavailable and for how long. Pages with no county match the id in any county, which can only over-withhold. validate-atlas fails a notice-list entry whose county is not an atlas county, one where an endpoint the county answers from has no classifier parcel-id column (NYC's BBL, Waukesha's TAXKEY: the paid surfaces could never match it), one whose classifier parcel-id column reads as an address or a name (Onondaga, Suffolk and Westchester's PARCEL_ADDR: the free tools never send it), and one whose parcel id, as written, the free tools would never send ("SMITH 2", no digit, a street start, a PO box, over 120 characters). A row whose id column holds a value with a digit that the check refuses to send is still a parcel row: it fails closed, not served, even when another of its ids was sent and came back unmatched.

    The endpoint is a per-id oracle by nature: anyone may ask whether a given id is listed. It is rate-limited PER ID, not per call (1,000 ids a minute and 3,000 an hour per IP), and it FAILS CLOSED: when its limiter store is unreachable it answers 503 and the page withholds. On a 429 the page waits for the window the server names (retry-after, up to 65 s, so the per-minute burst) and retries once. A longer wait (the hourly budget) is remembered for the whole tab (refusedUntilMs): until it passes, no page and no later lookup asks again; the rows that need the check are withheld with the time left shown, and rows that do not need it are untouched. The converter's cost, only on a layer in a listed county, a statewide or shared layer in a listed county's state, or a layer outside the atlas: one id per distinct id-column value on rows that need the check, so a 1,000-row page with one id column is 1,000 ids. The first page goes at once; the second, third and fourth each wait out the minute's burst (about a minute each), and the fourth is then refused for the hour (3,000 ids). From there on pages make no call and wait for nothing, their rows withheld with the hour's remaining wait shown. Measured with the real handler and limiter: a 20-page conversion keeps 3,000 rows in about 3.5 minutes (11 minutes before the refusal was remembered), a 50-page one keeps 3,000 rows in about 4.3 minutes (26 before), and a layer from an unlisted county makes no call at all. On a layer whose county is unknown, a row is in scope only when it has a non-blank owner or mailing value AND a parcel-id candidate, a mailing column or a situs column; a bare NAME counts as the owner only beside a parcel id AND a filled situs or mailing column (Kentucky's main PVA schema: NAME, LOCATION, ADDRESS1 on McCracken, Shelby, Franklin and others). A park, a school, a building, a permit or a subdivision with a NAME and a parcel column but no situs or mailing column makes no call: a road centerline with OWNER "STATE" has no location for a person and is never withheld, while owner + id, owner + situs and owner + mailing rows still fail closed. The protection is about where a person lives. Residual risk, Leo's to accept: someone holding the county's own data can compare it with a withheld row on our surfaces and learn that the parcel is listed; withholding cannot hide the fact that it withheld. The list ships empty, and geometry is withheld only where a surface returns it (the free GeoJSON converter and Parcel Lookup); no paid surface returns geometry.

    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. Whether an outcome is chargeable is answered in one place for every product and every tier: billableOutcome in src/lib/billing-decision.ts, a pure function with no tier argument. The tier only picks the mechanism that carries the answer out: a plan refund or a reservation settle, or the pay-as-you-go meter. REST bulk, the MCP enrich_address tool, lookup, radius and county all call it, and every exit on each of them settles through one step that reads the same decision for both tiers. On enrichment a plan key is refunded exactly when a pay-as-you-go key is not metered. Lookup is the deliberate exception: its plan and free draw is a per-call rate limit, so it stays spent on every exit while the pay-as-you-go meter follows the decision. Before it existed the same question had four inline answers, and they disagreed: pay-as-you-go metered a parcel-number-only row that plan refunded, and metered a source-updating row because its predicate never read the error.

    The policy lives in one table at the top of that file, one constant per pricing call: caller negatives (no geocode match, no parcel, an ambiguous match) are free; a parcel-number-only row from a county that cannot publish owner names is free; an unindexed county is free; a stale cached geocode is served and not metered for pay-as-you-go, while a plan lookup keeps its per-call draw; county metadata is free; and a mailing-address-only row is billed. That last one is load bearing: Pima AZ and Thurston WA return mailing addresses without owner names, and refunding those gave the product away free. Radius bills one unit per returned parcel that carries an owner name or a mailing address, but it runs only where a layer has an owner-name field: a county that publishes mailing addresses and no owner names (Pima AZ, Riverside CA, Thurston WA) is not radius-covered, because radius returns one mailing line per parcel and a notice list built from line 1 alone may be unmailable. Covering those three is a separate constant that stays off until Leo decides it. Flipping a constant moves every product and every tier together; a test flips each constant through the real handlers, and a wiring test fails if a call site stops asking.

    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 in the registry at the time.

    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, 3,132 tests, lint at baseline, validate-atlas passing 303 counties / 322 endpoints.

    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 52 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 303 counties and 1335 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 writes one not-billed, zero-unit row per answer: it is a metadata read of our own index, never charged on any tier, so a keyed call draws no allowance unit and is bounded by a separate per-key daily ceiling instead, the key's own daily limit or the pay-as-you-go safety ceiling, whichever is lower. A plan lookup's row says what happened to its unit: the per-call draw is kept on every exit, a stale geocode included, so the row is billed. 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.