Give a coding agent a product brief and build an AICD API app
Give a coding agent a focused product brief, run a local evidence-card app, and test its source mapping before connecting a live AICD API response.

Give a coding agent a screen to build, not an instruction to “make a civic-data platform.” A small, useful starting point is a set of evidence cards: a record title, a short description, a source reference, and a filter that helps the reader find the relevant item.
That is the application built for this walkthrough. It runs locally, includes a small historical dataset, and has a separate adapter for the documented AICD API public sample. You can inspect the code and run the local tests before connecting a live source.
The local application was implemented and tested during article preparation. Its optional live API path was not successfully exercised, so the demonstration keeps those two results separate rather than presenting fixture data as a live integration.
Give the agent a bounded product brief
Here is a reusable assignment:
Build a local evidence-card application for a researcher. Show a record title, source context, and an original-source link. Add a text filter over the loaded cards. Start with the supplied historical source extract, clearly labeled as such. Put live retrieval behind a separate adapter using the current AICD API reference. Reject an unexpected response shape, escape displayed text, and do not silently fall back to sample data when live retrieval fails. Include tests and report exactly which paths were executed. Do not deploy the application or send messages.
That brief defines a useful result and makes the verification boundary part of the implementation. It does not prescribe a large framework, an authentication system, or a database service the first screen does not need.
Keep the documentation with the assignment. The AICD API reference and its Markdown version are the contract to inspect before enabling live calls. A coding agent should identify an unavailable contract as a specific dependency, not fill it in with plausible endpoint names.
Start with a source extract you can inspect
The included authorizations.json contains three manually transcribed Fort Worth authorization records from June 24, 2025: M&C 25-0563, M&C 25-0573, and M&C 25-0574.
Each row retains its official source URL. This extract is not an API response, a complete meeting export, or payment data. Its small size makes the initial display and filter behavior easy to inspect.
The fixture and live adapter both produce an internal card with title, summary, evidence, and source_url. Those are application fields. Keeping that interface small lets the display work independently of how the source was retrieved.
Run the application
Save the files from the example code section below in one directory. The Python application uses the standard library; the tested environment was Python 3.13.5.
python aicd_examples.py test
python aicd_examples.py serve
Open the loopback address printed by the server. The default view shows the three historical records. Enter a distinctive term such as HVAC to filter the loaded cards, or a term absent from the sample to inspect the no-match state.
The filter is deliberately local. It does not call a citywide search endpoint, and the interface says that an empty result is not a coverage finding. This makes the first application honest about the job it performs.
The server binds to 127.0.0.1 and is a learning example. Python's HTTP server documentation explicitly cautions against using this server as a production service. A public deployment would need a different serving and security setup.
Keep retrieval behind a small adapter
The optional mode is:
python aicd_examples.py serve --live
It attempts the public sample route described in the saved reference-derived project material:
GET https://aicdapi.com/api/v1/public/agenda-hits?limit=3
The adapter expects an object with a data array and maps title, summary, evidenceQuote, and sourceUrl to the card interface. It applies a response-size limit and validates the expected types before rendering. It also checks the final response host.
The live reference could not be retrieved in this environment, and no successful live response was obtained. Confirm that route and response against the current reference before using the optional mode. The default historical mode remains independently runnable.
A live failure stops the program rather than disguising the fixture as current data. That behavior makes debugging much easier: the developer can tell whether the problem is retrieval, adaptation, or display.
Test the boundaries that affect the reader
The shared package has thirteen passing Python unit tests. They cover the fixture, calculations and relationship query used by companion tutorials, plus rendering, filtering, URL checks, and the expected sample envelope. Three additional loopback HTTP checks passed for the local application.
Those tests establish local behavior. They are not a browser-accessibility audit, a production security review, or proof that a live API account is correctly configured.
The rendering test includes script-like text and checks that it is escaped rather than inserted as executable markup. Another test rejects a javascript: source URL. The no-match test checks that the screen explains the small sample's scope.
These are useful instructions to give a coding agent because they describe observable behavior. “Make the app robust” leaves the agent to choose what robustness means; a fixture and an expected result make the requirement concrete.
Review the implementation instead of accepting a demonstration alone
Inspect the source-to-card mapping first. A source identifier should not become a made-up project ID, and a historical date should not be rewritten as the date the application ran.
Then inspect failures. An unexpected response shape should produce a clear error. An unavailable source should not look like a successful search with no matches. A data field should remain text unless the application has deliberately validated a more specific meaning.
Finally, check the result from the reader's perspective. Can they tell which records are loaded? Can they open the original source? Does the filter do what its label says? A successful local request is useful evidence, but the screen still needs to explain its scope.
Use the next agent task to add one capability
After checking the live contract, the next implementation task could be a verified public-sample integration. A later task could add authenticated retrieval, a saved watchlist, or persistent history. Each should have its own source scope and acceptance tests.
Do not treat the three-hit public sample as an ongoing complete monitor. A recurring product needs the appropriate documented search or synchronization path, a clear coverage model, and state for handling changed records.
The initial deliverable is intentionally smaller: a working local application, an explicit data adapter, and tests that a developer can rerun. That gives the next coding-agent assignment a concrete base instead of another abstract specification.
Example code
Save each code block below using its heading as the filename, in one directory. The dataset is a manually transcribed extract of three official Fort Worth authorization records, not an AICD API response. Thirteen Python unit tests and three loopback HTTP checks passed using Python 3.13.5 and SQLite 3.46.1. The serve command binds only to 127.0.0.1:8765 by default and is not for production. serve --live is an optional reference-based adapter that still needs verification against the current API reference.
authorizations.json
[
{
"record_id": "fw-mc-25-0563",
"agency": "City of Fort Worth",
"action_date": "2025-06-24",
"category": "Telecommunications",
"annual_ceiling_cents": 155000000,
"vendors": ["AT&T Corporation"],
"source_url": "https://fortworthgov.legistar.com/LegislationDetail.aspx?G=5D78EE16-987A-40AB-8FB5-726D3DBA5713&GUID=98F18D76-29EC-47B8-98A4-C0E497FFE570&ID=7440706&Options=&Search=",
"source_file": "M&C 25-0563",
"measure": "stated annual authorization amount",
"source_status": "Passed"
},
{
"record_id": "fw-mc-25-0573",
"agency": "City of Fort Worth",
"action_date": "2025-06-24",
"category": "HVAC equipment and installation",
"annual_ceiling_cents": 12000000,
"vendors": ["Baker Distributing Company LLC", "EPMA Corporation DBA Johnstone Supply"],
"source_url": "https://fortworthgov.legistar.com/LegislationDetail.aspx?G=5D78EE16-987A-40AB-8FB5-726D3DBA5713&GUID=41CC6E8D-6E48-4246-A420-D2C696EE9F55&ID=7440754&Options=&Search=",
"source_file": "M&C 25-0573",
"measure": "combined annual authorization ceiling",
"source_status": "Passed"
},
{
"record_id": "fw-mc-25-0574",
"agency": "City of Fort Worth",
"action_date": "2025-06-24",
"category": "HVAC services",
"annual_ceiling_cents": 146500000,
"vendors": ["Texas AirSystems, LLC"],
"source_url": "https://fortworthgov.legistar.com/LegislationDetail.aspx?G=5D78EE16-987A-40AB-8FB5-726D3DBA5713&GUID=14801F53-DE07-48C6-8022-A30072C5EEE4&ID=7440760&Options=&Search=",
"source_file": "M&C 25-0574",
"measure": "combined annual authorization ceiling",
"source_status": "Passed"
}
]
aicd_examples.py
"""Local article examples. Standard library only; not a production web server."""
from __future__ import annotations
import argparse
import csv
import hashlib
import html
import json
import sqlite3
import sys
import unittest
from datetime import date, timedelta
from decimal import Decimal
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlparse
from urllib.request import Request, urlopen
ROOT = Path(__file__).resolve().parent
PUBLIC_SAMPLE = 'https://aicdapi.com/api/v1/public/agenda-hits?limit=3'
MAX_RESPONSE = 1_048_576
def https_url(value: object) -> str:
if not isinstance(value, str):
raise ValueError('Source URL must be a string')
parsed = urlparse(value)
if parsed.scheme != 'https' or not parsed.netloc or parsed.username or parsed.password:
raise ValueError('Only credential-free HTTPS source URLs are accepted')
return value
def read_records() -> list[dict]:
values = json.loads((ROOT/'authorizations.json').read_text())
seen = set()
for row in values:
if row['record_id'] in seen:
raise ValueError('Duplicate source-extract record ID')
seen.add(row['record_id'])
https_url(row['source_url'])
date.fromisoformat(row['action_date'])
if type(row['annual_ceiling_cents']) is not int or row['annual_ceiling_cents'] < 0:
raise ValueError('Amount must be nonnegative integer cents')
return values
def sample_cards(payload: object) -> list[dict]:
"""Reference-based adapter. Live API schema has NOT been reverified."""
if not isinstance(payload, dict) or not isinstance(payload.get('data'), list):
raise ValueError('Expected a data array; inspect the current API reference')
if len(payload['data']) > 3:
raise ValueError('Unexpected public-sample size; inspect the response')
cards = []
for row in payload['data']:
if not isinstance(row, dict) or not isinstance(row.get('title'), str):
raise ValueError('Each hit needs a title')
source = https_url(row.get('sourceUrl'))
for key in ('summary','evidenceQuote'):
if key in row and row[key] is not None and not isinstance(row[key], str):
raise ValueError(f'{key} must be text when present')
cards.append({'title':row['title'], 'summary':row.get('summary') or '',
'evidence':row.get('evidenceQuote') or '', 'source_url':source})
return cards
def fetch_public_cards() -> list[dict]:
request = Request(PUBLIC_SAMPLE, headers={'Accept':'application/json'})
with urlopen(request, timeout=10) as response:
if urlparse(response.geturl()).hostname != 'aicdapi.com':
raise ValueError('Unexpected redirect host')
raw = response.read(MAX_RESPONSE+1)
if len(raw) > MAX_RESPONSE:
raise ValueError('Sample response exceeded the local size limit')
return sample_cards(json.loads(raw))
def curated_cards() -> list[dict]:
return [{'title':r['source_file']+' | '+r['category'],
'summary':r['agency']+'; source action date '+r['action_date']+
'; selected authorization record, not payment data.',
'evidence':'Named in the source: '+ '; '.join(r['vendors']),
'source_url':r['source_url']} for r in read_records()]
def render(cards: list[dict], query: str = '', label: str = 'Curated historical source extract') -> bytes:
escape = html.escape
q = query.casefold()
selected = [c for c in cards if q in (c['title']+' '+c['summary']+' '+c['evidence']).casefold()]
blocks = ''.join('<article><h2>'+escape(c['title'])+'</h2><p>'+escape(c['summary'])+
'</p><p>'+escape(c['evidence'])+'</p><a rel="noopener noreferrer" href="'+
escape(https_url(c['source_url']), quote=True)+'">Open official source</a></article>'
for c in selected)
if not selected:
blocks = '<p>No matches in this small loaded sample. This is not a coverage finding.</p>'
doc = '''<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Civic evidence cards</title><style>body{font:17px/1.55 system-ui;max-width:850px;margin:3rem auto;padding:0 1rem}article{border:1px solid #bbb;border-radius:8px;margin:1rem 0;padding:1rem}input,button{font:inherit;padding:.4rem}label{display:block}</style><h1>Civic evidence cards</h1>'''
doc += '<p>'+escape(label)+'. Source-linked learning example, not a complete city dataset.</p>'
doc += '<form><label for="q">Filter the loaded cards</label><input id="q" name="q" value="'+escape(query,quote=True)+'"><button>Filter</button></form>'
doc += '<p>'+str(len(selected))+' card(s) shown.</p>'+blocks+'</html>'
return doc.encode()
def database() -> sqlite3.Connection:
db = sqlite3.connect(':memory:')
db.execute('PRAGMA foreign_keys=ON')
db.executescript('''CREATE TABLE actions(id TEXT PRIMARY KEY, agency TEXT NOT NULL,
action_date TEXT NOT NULL, category TEXT NOT NULL, amount_cents INTEGER NOT NULL,
source_url TEXT NOT NULL); CREATE TABLE action_vendors(action_id TEXT NOT NULL,
vendor TEXT NOT NULL, PRIMARY KEY(action_id,vendor),
FOREIGN KEY(action_id) REFERENCES actions(id));''')
for r in read_records():
db.execute('INSERT INTO actions VALUES (?,?,?,?,?,?)',
(r['record_id'],r['agency'],r['action_date'],r['category'],r['annual_ceiling_cents'],r['source_url']))
db.executemany('INSERT INTO action_vendors VALUES (?,?)',[(r['record_id'],v) for v in r['vendors']])
db.commit()
return db
def analyze() -> dict:
with database() as db:
count, total = db.execute('SELECT COUNT(*),SUM(amount_cents) FROM actions').fetchone()
vendors = db.execute('SELECT COUNT(DISTINCT vendor) FROM action_vendors').fetchone()[0]
naive = db.execute('SELECT SUM(a.amount_cents) FROM actions a JOIN action_vendors v ON a.id=v.action_id').fetchone()[0]
return {'scope':'three deliberately selected Fort Worth authorizations dated 2025-06-24; not a census',
'selected_actions':count,'named_vendor_strings':vendors,
'selected_annual_authorization_amount_cents':total,
'incorrect_vendor_join_total_cents':naive,
'claim_type':'sum of stated annual amounts for selected actions, NOT payments or revenue',
'source_records':[{'id':r['record_id'],'source_url':r['source_url']} for r in read_records()]}
def graph_rows(vendor: str) -> list[dict]:
"""A typed two-hop graph expressed as relational joins, preserving edge evidence."""
with database() as db:
rows = db.execute('''SELECT a.agency,a.id,v.vendor,a.source_url
FROM actions a JOIN action_vendors v ON a.id=v.action_id
WHERE v.vendor=? ORDER BY a.id''',(vendor,)).fetchall()
return [{'agency':a,'action_id':i,'relation':'names_vendor','vendor':v,'evidence_url':u}
for a,i,v,u in rows]
def write_outputs() -> None:
report = analyze()
(ROOT/'analysis_results.json').write_text(json.dumps(report, indent=2)+'\n')
(ROOT/'graph_results.json').write_text(json.dumps(graph_rows('EPMA Corporation DBA Johnstone Supply'),indent=2)+'\n')
with (ROOT/'authorization_report.csv').open('w',newline='') as f:
writer = csv.DictWriter(f, fieldnames=['record_id','action_date','category','annual_ceiling_cents','source_url'])
writer.writeheader()
for r in read_records(): writer.writerow({k:r[k] for k in writer.fieldnames})
digest = hashlib.sha256((ROOT/'authorizations.json').read_bytes()).hexdigest()
(ROOT/'data_manifest.json').write_text(json.dumps({'fixture_sha256':digest,
'origin':'Manual transcription of three official Legistar authorization records',
'api_response':False,'research_date':'2026-10-03','record_count':len(read_records())},indent=2)+'\n')
def serve(port: int, live: bool) -> None:
cards = fetch_public_cards() if live else curated_cards()
label = 'AICD public sample fetched at startup' if live else 'Curated historical source extract'
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
parsed = urlparse(self.path)
if parsed.path != '/':
self.send_error(404); return
body = render(cards, parse_qs(parsed.query).get('q',[''])[0], label)
self.send_response(200)
self.send_header('Content-Type','text/html; charset=utf-8')
self.send_header('Content-Length',str(len(body)))
self.send_header('X-Content-Type-Options','nosniff')
self.send_header('Content-Security-Policy',"default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'")
self.end_headers(); self.wfile.write(body)
print(f'Local-only example: http://127.0.0.1:{port}',flush=True)
ThreadingHTTPServer(('127.0.0.1',port),Handler).serve_forever()
class Tests(unittest.TestCase):
def test_fixture(self): self.assertEqual(len(read_records()),3)
def test_integer_totals(self): self.assertEqual(analyze()['selected_annual_authorization_amount_cents'],313500000)
def test_join_inflation(self): self.assertEqual(analyze()['incorrect_vendor_join_total_cents'],325500000)
def test_graph(self): self.assertEqual(graph_rows('EPMA Corporation DBA Johnstone Supply')[0]['action_id'],'fw-mc-25-0573')
def test_unknown_vendor(self): self.assertEqual(graph_rows("' OR 1=1 --"),[])
def test_escaping(self):
page = render([{'title':'<script>alert(1)</script>','summary':'','evidence':'','source_url':'https://example.com'}]).decode()
self.assertNotIn('<script>',page); self.assertIn('<script>',page)
def test_unsafe_url(self):
with self.assertRaises(ValueError): https_url('javascript:alert(1)')
def test_wrong_envelope(self):
with self.assertRaises(ValueError): sample_cards({'results':[]})
def test_empty_sample(self): self.assertEqual(sample_cards({'data':[]}),[])
def test_filter(self): self.assertIn(b'No matches',render(curated_cards(),'zzzzzz'))
def test_adapter(self): self.assertEqual(sample_cards({'data':[{'title':'Synthetic test','sourceUrl':'https://example.com','summary':'x'}]})[0]['summary'],'x')
def test_review_date(self): self.assertEqual(date(2026,4,12)-timedelta(days=90),date(2026,1,12))
def test_payment_fixture(self): self.assertEqual(sum(map(Decimal,['8000.00','1200.00','-200.00']),Decimal('0')),Decimal('9000.00'))
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('command',choices=['test','analyze','graph','serve'])
parser.add_argument('--port',type=int,default=8765)
parser.add_argument('--live',action='store_true')
args = parser.parse_args()
if args.command == 'test': unittest.main(argv=[sys.argv[0]],verbosity=2)
elif args.command == 'analyze': write_outputs(); print(json.dumps(analyze(),indent=2))
elif args.command == 'graph': print(json.dumps(graph_rows('EPMA Corporation DBA Johnstone Supply'),indent=2))
else: serve(args.port,args.live)