Case study · Civic AI

The weekend I rebuilta city’s website.

Brownsville, Texas: a bilingual, task-first city site with an AI assistant that cites the municipal code for every answer — and the honest math on what it costs a city to run.

By Prithvi Datla, Founder · July 2026 · 15 min read

Can a city give residents an AI assistant they can actually trust?

Yes — if every answer is grounded in the city’s own municipal code, carries a citation, and the system refuses when it doesn’t know. Over the July 4th weekend I rebuilt the City of Brownsville’s website to prove it: one engineer, one weekend, and an assistant that runs anywhere from a public API (about $90 a month at pilot volume) to hardware the city owns outright, where nothing a resident asks ever leaves the building.

Over the July 4th weekend, I did the most American thing I could think of: I worked.

I rebuilt the City of Brownsville, Texas’s website. Nobody asked, it felt like a cool project to test local LLMs on a common use case. I wired in an AI assistant that answers residents’ questions from the city’s own municipal code, with a citation for every answer. One engineer, one weekend, start to finish.

Video at the end.

This is the long retro: what I built, why decisions went the way they did, what broke, and — because every city exploring AI eventually asks the same question — what it actually costs to run, in three flavors: fully self-hosted on the city’s own hardware, private cloud, and public API (Anthropic, xAI, OpenAI, etc.).

The 60-second tour

The build in one minute: the bilingual service catalog, honest handoffs, and Ask Brownsville answering from the city’s own code. A demonstration project — not the official City of Brownsville website.

Why Brownsville

Brownsville is having a moment. It’s the city SpaceX launches Starship from, it sits on the Gulf coast, and roughly 186,000 people live there. Its digital front door looked like it needed a refresh.

An important thing to consider is that until less than a decade ago, city and government websites were largely informational. With advancements in technology all around, they are now increasingly transactional. For example, 15 years ago, city residents would mostly go to the city website to check addresses, phone numbers, office hours, etc. More recently, they’ve become transactional — residents can pay bills, upload documents, apply for permits, and review their own transaction history.

The current Brownsville site is more informational, a big opportunity to rethink. Navigation mirrors the city’s internal org chart instead of residents’ tasks. Spanish support is missing in a city where the Census American Community Survey puts Spanish-spoken-at-home above 80%. Finding an answer buried in the Unified Development Code means reading the Unified Development Code. There’s also a deadline attached: the Department of Justice’s new ADA Title II web accessibility rule requires cities of Brownsville’s size to meet WCAG 2.1 AA by April 2026, which has already passed.

I did not start from a blank canvas. I researched government websites, how they were being used, who launched new features or services recently, etc. The benchmark I built against was co.colorado.gov — I found it to be the best task-based government website in the country.

The product thinking: tasks, not departments

The single biggest decision was the information architecture. Residents don’t think “Public Works Department”; they think pay my water bill, report a pothole, find a park. So the site’s spine is a bilingual, verb-first service catalog. Every service became a task.

Three product rules followed from field research and from studying how real residents actually fail on city sites:

  1. Bilingual parity, not a translate button. Every route, every string, every error message exists in English and Spanish — written Spanish in the formal usted register, authored, not machine-translated. The language toggle swaps languages on the same page.
  2. Honest handoffs. Many “city” services aren’t run by the city; the water utility is BPUB, permits run through Accela. I added clarity for residents. Every external service carries an operator label: who runs it, why you’re leaving, and what you’ll need when you get there.
  3. Accessibility as a floor, not a feature. WCAG 2.1 AA contrast on every surface, keyboard-navigable everything, screen-reader-correct language tags.
Technical depth · the design system

For design, I leaned into the city’s current burgundy shade. The stack is Next.js with locale-prefixed routes (/en/…, /es/…), a single services.ts catalog driving the IA, and a single i18n.ts file holding every user-facing string in both languages.

The assistant: why “cites its sources” is the whole product

The marquee feature is Ask Brownsville: type a question — “How tall can a fence be in a residential district?” — and get an answer drawn from the city’s actual municipal code, with numbered citations linking to the exact sections, in English or Spanish.

The biggest blocker to cities adopting AI is trust, not capability. A city cannot deploy a system that occasionally invents an ordinance. So the assistant runs on one non-negotiable rule: every answer is grounded in retrieved sections of the city’s own code, every answer carries citations, and if the code doesn’t cover the question, the assistant says so and hands the resident to a human channel instead of guessing. The refusal behavior isn’t nice-to-have. It’s tested, and covered below.

Technical depth · the retrieval engine

The assistant is powered by CivicLLM, a retrieval-augmented generation (RAG) engine I built to be tenant-driven: a new institution is a YAML profile plus an ingestion source, no code changes. For Brownsville, ingestion walks Municode’s table of contents, parses the HTML (including linearizing the code’s many tables so they survive as text), chunks the sections, and embeds them. The Brownsville corpus is 222 sections — the Unified Development Code Articles 1 through 6, Flood Damage Prevention (Ch. 308), Impact Fees (Ch. 314), and Signs (Ch. 328), at Supplement No. 25, current through Ordinance 2025-1785 — producing 1,021 embedded chunks in Postgres with pgvector. Retrieval is hybrid: vector similarity and full-text search run in parallel and are fused with reciprocal rank fusion, then an LLM reranks the shortlist. Ingestion is content-hash-diffed, so when the city publishes a new supplement, only changed sections re-process.

Scope check · a slice of the code, not all of it

This is a slice of the municipal code, not all of it. Brownsville’s actual Code of Ordinances runs chapters 1 through 348 (Municode Library, City of Brownsville, TX, retrieved 2026-07-09). The 222 sections ingested here cover six of those: the Unified Development Code (Articles 1-6) plus Flood Damage Prevention, Impact Fees, and Signs — a zoning and development slice. Ask this assistant about a business license, a utility bill, a court date, or a records request and it correctly refuses, because none of that was ingested. A production deployment meant to actually absorb resident contact volume, not just zoning questions, would need most or all of the remaining 342 chapters ingested. That’s a real cost the estimates below don’t carry: more Municode pages to parse, chunk, and embed, and a corpus large enough that holding answer quality likely needs a wider retrieval window than the 8 sections this build pulls per query — which pushes input tokens, and cost, up from what a 222-section corpus needs.

Technical depth · the guardrails

Four layers stand between answer and the query: a retrieval-score floor (if nothing scores high enough, refuse), a refusal sentinel the model must emit when the corpus doesn’t cover the question, a citation-required check (an answer without citations is rejected), and a jurisdiction guard (ask Brownsville’s assistant about Austin’s code and it declines rather than freelancing). Correctness is enforced by a golden-set evaluation: known-answer questions checked for the correct figure, the correct cited section, and correct refusals on out-of-corpus traps, including a Spanish-language case and a deliberate cross-tenant trap. Brownsville scores 10/10 on a 7-billion-parameter local model.

Picking the model and infrastructure

Two stories have to be true at once:

  • The private story. For a city deployment that’s really private, nothing about a resident’s question should have to leave the building. So the engine runs on Ollama with open-weight models — qwen2.5:7b-instruct for answers, nomic-embed-text for embeddings — on hardware the city owns. The 10/10 golden-set score above is on exactly this configuration.

What went wrong

A lot. Highlights from the adversarial red-team pass I ran on my own product — a live hostile-browser session plus a code-level review that produced 41 findings, all fixed before launch:

  • The 42-second wall of silence. On the local 7B model on my Macbook Air M4 w 24GB memory, a full answer took a measured 42 seconds, and the UI’s wait copy stalled at “Reading the sections…” with no elapsed-time signal and no cancel button. A hung stream locked the whole app. Fixed: cancel mid-stream, staged progress copy, and streaming that shows tokens as they arrive.
  • Error dead-ends. The error copy said “use the official channels below” and rendered zero channels. Raw internals (“Could not resolve authentication method...”) surfaced verbatim, in English, inside the Spanish interface. Fixed: localized error cards with human handoff channels and a retry button on every failure path.
  • The trust badge failed accessibility! The green “Grounded in cited sources” badge — the product’s core trust signal — tested at 4.01:1 contrast, just under the 4.5:1 AA threshold. The one element that most needed to be legible wasn’t.
  • Hallucinated refusal. The system watches for a refusal sentinel when the corpus doesn’t cover a question. During verification, the model invented its own sentinel — answering a Spanish question with “NO_SE_INFORMA_SOBRE_ESTE_TEMA” followed by eight fabricated citations. It evaded the guards because it wasn’t the sentinel we were watching for. The fix: a detector for ALL-CAPS snake-case pseudo-sentinels, plus regression tests. The lesson: guardrails need guardrails, and the failure modes you enumerate are never the complete set.

What held up under attack is worth noting too: script injection rendered inert as literal text, and questions wrapped in garbage still retrieved and answered correctly (“35 feet, cited”).

What would it actually cost a city?

City manager stuff: All numbers below are estimates with stated assumptions, deliberately widened toward the expensive end rather than the cheap one; check my arithmetic, and read the scope caveat above first, since these tiers still price the narrow 222-section corpus, not a full-code deployment.

Usage assumptions. Brownsville has roughly 186,000 residents. Two real cities give an anchor for 311-style contact volume: Chicago logged about 654,500 service requests in the twelve months to May 2026 against a population of roughly 2.7 million — about 0.24 requests per resident per year; NYC311 logged about 2.7 million requests in 2018 against roughly 8.3 million residents — about 0.33 per resident per year. Applied to Brownsville’s population, that’s roughly 45,000 to 61,000 contacts a year, 3,750 to 5,000 a month, across all channels combined.

That anchor almost certainly understates the real number: 311 datasets only count logged service tickets, not the much larger volume of informational questions residents ask a front desk or a phone line that never becomes a ticket at all. “How tall can my fence be” doesn’t get filed — it gets answered, or the resident gives up. Comparable deployments back that up: Williamsburg, VA’s city chatbot resolved 79% of resident questions on first contact; Phoenix’s myPHX311 assistant fields service requests, utility on/off included, without a human. Neither city publishes a hard per-capita AI-assistant query count I can cite, but the direction is consistent: once a low-friction, always-on, bilingual channel exists, usage runs toward the high end of what 311 data alone implies, not the low end. I’ve modeled three tiers against that wider, sourced range, not a top-of-head adoption percentage:

Usage tiers
TierAssumptionQueries / month
PilotNarrow, zoning-only corpus (this build's actual scope), early rollout~2,000
AdoptedFull 311-equivalent contact volume for a city Brownsville's size (Chicago/NYC per-capita anchor)~4,000–5,000
Heavy, full-codeCorpus expanded to the full 348-chapter code, mature adoption on the Williamsburg/Phoenix pattern~20,000–30,000

The Heavy tier is the one to plan around if this succeeds, not the Pilot tier: it assumes the scope-check caveat above gets addressed (full code ingested) and residents actually trust and use the channel, which is the stated goal, not an edge case.

Per-query token math. Each answer sends the model a system prompt plus about 8 retrieved code sections — roughly 5,000 input tokens — and generates a roughly 400-token answer. That figure is calibrated to the 222-section corpus in this build. A full 348-chapter corpus spanning many more topics would likely need a wider retrieval window, more candidate chunks, possibly a category-routing step before retrieval, to hold answer quality across that many more subjects, which pushes input tokens per query up from this baseline. I don’t have a measured number for that larger corpus, so the cost tables below are a floor for Option 1, not a ceiling.

Option 1: Public API

At Claude Sonnet 5’s list price ($3 per million input tokens, $15 per million output), a query costs about 2.1 cents (5,000 tokens times $3/M, plus 400 tokens times $15/M). Anthropic’s current introductory pricing ($2/$10 through August 2026) brings that to roughly 1.4 cents. Prompt caching the fixed system prompt shaves roughly another 20 percent.

Option 1 — public API, monthly cost
TierModel cost / month (list)Plus hosting (~$50)
Pilot (2k)~$42~$90/mo
Adopted (4,000–5,000)~$84–105~$130–155/mo
Heavy, full-code (20,000–30,000)~$420–630~$470–680/mo

These numbers use the 5,000-input-token baseline from the narrow corpus; at full-code retrieval width, expect the Heavy tier to land above $680/mo, by how much depends on the retrieval tuning a bigger corpus needs, which hasn't been built or measured yet. Pros: best answer quality, close to zero ops, scales instantly, pay only for what residents use. Cons: resident questions transit a third-party API (with a commercial no-training agreement, but still off-premises), and cost scales linearly with adoption — success gets more expensive.

Option 2: Private cloud (a GPU VM in the city’s own cloud account)

One GPU instance runs the whole stack — open-weight model, embeddings, database — inside the city’s VPC. Current AWS on-demand pricing: a g6.xlarge (NVIDIA L4, comfortable for a 7 to 14B model) runs about $0.80 an hour, roughly $588 a month running 24/7; the older g5.xlarge is about $1.01 an hour, roughly $734 a month. A one-year commitment typically cuts that by roughly a third, into the $350 to $450 range. Add roughly $40 to $60 for storage, snapshots, and egress.

Option 2 — private cloud, monthly cost
TierOn-demand1-yr commit (est.)
Any volume~$630–790~$400–510

Pros: resident questions never leave the city's cloud tenancy; cost is flat no matter how popular the assistant gets; standard cloud procurement. Cons: you pay the full rate at 3 a.m. when nobody's asking; someone owns patching and monitoring; a 7 to 14B open model is measurably good (10/10 on our golden set) but not frontier-model good on hard edge cases.

Option 3: Fully self-hosted (the city’s own hardware)

The maximum-privacy option: a single GPU workstation in the city’s server room. Assumptions: roughly $3,500 one-time for an RTX 4090-class machine, amortized over 3 years (roughly $97 a month); roughly 300W average draw times 730 hours times $0.13/kWh Texas commercial power, roughly $28 a month.

Option 3 — self-hosted, monthly cost
TierCost / month (amortized)
Any volume~$125–170 plus IT staff time

Pros: cheapest at any real volume; nothing — not the question, not the answer, not the logs — ever leaves the building; no per-query anxiety. Cons: capital procurement instead of a subscription; the city's IT department owns uptime, backups, and the eventual hardware refresh; no elastic burst capacity.

My read

At pilot volume, the public API is the cheapest way to find out whether residents want this at all: roughly $90 a month with the best model quality available. That’s the narrow-scope, low-adoption case, not the target state. If the corpus grows to the full code and adoption follows the Williamsburg pattern, the realistic operating point is the Heavy tier, and the curves cross well inside it: the private-cloud flat rate (~$400–790/mo) and the API’s Heavy-tier cost (~$470–680/mo, likely higher once full-corpus retrieval is measured) land in the same range, while self-hosted (~$125–170/mo amortized, plus IT staff time) stays cheaper at any volume above pilot.

For a city whose residents ask about immigration status, code violations on their own property, or utility shut-offs, “nothing leaves the building” is not a technical preference — it’s a trust posture, and it’s also the cheaper option once usage is real. That’s why the system was built to make the choice reversible: same code, same corpus, same evaluation suite, one environment variable. The number a city manager should hold onto isn’t the $90/mo pilot figure — it’s that the full-code, real-adoption case likely runs several times that, and self-hosting is the way to keep it from scaling linearly with success.

Plot Twist: What if residents could just talk to it?

The obvious next question: strap a speech-to-speech model onto this — Amazon Nova 2 Sonic, OpenAI’s Realtime API, or Google’s Gemini Live — and let residents talk instead of type. The pricing is real and checkable. The harder part is that voice isn’t a cost line one can add to the existing model; it’s an architecture decision that touches the guardrail stack that’s the actual product here.

The architecture choice. All three vendors are audio-native reasoning models, not text-to-speech bolted onto a fixed pipeline. That gives two real options:

  1. Let the voice model be the LLM. Fastest, and the pricing below applies directly. But then citation grounding, the refusal sentinel, the retrieval-score floor — the whole guardrail stack covered above — has to be rebuilt inside a live audio session. All three vendors support mid-session function/tool calling, so in principle the existing retrieval engine can be wired in as a callable tool. In practice, context window is an issue. Besides, none of the three vendors’ documentation shows a production example of a similarly-scoped assistant reliably citing sources and refusing out-of-corpus questions by voice.
  2. Build voice around the existing pipeline. Speech-to-text converts the resident’s question, the existing grounded RAG call answers it exactly as it does today (same 10/10 golden set, same refusal sentinel, same citation check), and text-to-speech reads the answer back. Nothing about the trust architecture changes. The cost is additive: audio I/O on top of the per-query model cost already priced above. Latency goes up by one extra hop, and native mid-sentence barge-in is harder to support well in a cascade — but the guardrails that are the actual point of this build stay intact and already-tested.

Given that the whole pitch of this assistant is “it doesn’t make things up and it says when it doesn’t know,” the cascaded architecture is the responsible default. Native reasoning is worth revisiting once one of these vendors publishes a grounding/refusal pattern that’s actually been red-teamed for this.

What each vendor actually costs and supports, at 2026 published rates:

Voice vendors at 2026 published rates
VendorRateSpanishMid-session tool calling
Amazon Nova 2 Sonic$3/M input, $12/M output speech tokens · ~$0.015/min blendedYes (Lupe/Carlos voices, polyglot code-switching)Yes
Google Gemini Live (3.1 Flash Live)$3/M input, $12/M output audio tokens · ~$0.023/min blendedYes (70+ languages)Yes
OpenAI Realtime$32/M input, $64/M output audio tokens · measured sessions $0.05–0.10/min cached, $0.18–0.46/min withoutYes (57+ languages)Yes

Bilingual parity was a hard product rule for the rest of this build (see above); it survives the voice layer — all three vendors support Spanish natively — so that's not a differentiator here. Price is: OpenAI Realtime runs 3 to 4x Nova Sonic or Gemini Live for this workload even in its best case, and real measured sessions (HackerNoon's 4,000-session dataset) show that gap widening without careful prompt caching.

What it costs layered on the existing tiers, assuming a ~50 to 60 second round trip per voice query (a ~12-second spoken question, a ~35 to 40-second spoken answer — the citation-laden answers this assistant gives take longer to say than to read) and Gemini Live as the audio layer around the existing text pipeline:

Voice layered on Option 1, model cost only
TierQueries / monthGemini Live audioExisting text-model costCombined / month
Pilot~2,000~$26~$42~$68
Adopted~4,000–5,000~$52–65~$84–105~$136–170
Heavy, full-code~20,000–30,000~$260–390~$420–630~$680–1,020

Model cost only, before the ~$50/mo hosting baseline, and before the same full-corpus retrieval-width caveat that applies to every Heavy-tier number in this piece. Swap in Nova 2 Sonic and the numbers move within about 15 percent of Gemini Live's; swap in OpenAI Realtime and the audio line alone can exceed the existing text-model cost, pushing Heavy-tier combined cost toward $1,500 to $2,500/mo at the cached end and considerably higher uncached.

One gap worth naming rather than papering over: every number above is a metered cloud API. Unlike the text half of this build, there’s no mature open-weight speech-to-speech model I can price for the self-hosted or private-cloud columns — the open alternatives exist but aren’t at a point I’d cite a production cost for.

Voice, as of today, doesn’t get cheaper by owning the hardware the way text generation does. That tips the calculus back toward “pace the rollout and watch the meter” for voice specifically, even for a city that’s otherwise committed to self-hosting the text assistant.

What’s next

Any city, a university’s student policies, or a hospital’s administrative code, is a YAML profile, an ingestion source, and a ten-question golden set. The recipe is mostly above.

If you are working on something similar, or you’re a city wondering what AI could do for your residents without losing their trust, I’d love to connect and show the actual build.

Sources & receipts

Spanish-spoken-at-home: Census ACS 2024 5-year estimates, table C16001, Brownsville city, TX (145,469 of 175,842 residents age 5+ — 82.7%), retrieved 2026-07-10. Brownsville’s full Code of Ordinances chapter count (1–348) per Municode Library, retrieved 2026-07-09. AWS GPU pricing per Vantage (g6.xlarge) and CloudPrice (g5.xlarge), retrieved 2026-07-09. Claude Sonnet 5 pricing per Anthropic’s published rates as of June 2026. 311 per-capita volume anchors from Chicago’s 2026 311 dashboard (transparent.city) and NYC311’s 2018 figures (Data Collaborative for Justice), retrieved 2026-07-09. Williamsburg, VA and Phoenix myPHX311 chatbot outcomes per Govlaunch and The Atlas, retrieved 2026-07-09. Amazon Nova 2 Sonic, Gemini Live API (3.1 Flash Live), and OpenAI Realtime pricing and language support per each vendor’s published documentation (plus HackerNoon’s 4,000-session Realtime dataset), retrieved 2026-07-09. Current-site failure analysis, red-team findings, golden-set evaluation, and ingestion pipeline are documented in the project’s repository. This is a demonstration project, not the official City of Brownsville website.

Keep reading

Your city, campus, or
code could be next.

A new institution is a profile, an ingestion source, and a golden set. If you’re wondering what a grounded, private AI assistant could do for the people you serve — let’s talk.

Book a free discovery call