Skip to Content
Hey Town Hall For Businessdocs

Recipes

Open tenders and what came before them

For a company that sells to governments: a town's open tenders, the council decisions that led to each, and what the town spends in that area.

View as Markdown

After this recipe you can hand your own language model a town's open tenders, the agenda items and motions behind each one and the town's spending lines, each with its link, and ask which tenders to bid on.

For: a vendor that sells to local governments.

Uses: GET /v1/bids, GET /v1/search and GET /v1/financial-lines. One records request for the tenders, one search for each tender, and one records request for the spending lines.

Steps

  1. List the town's open tenders. Each has its closing time, closes_at.
  2. For each tender, search the town's agenda items and motions for its subject: the words after the tender's number. A road or a water main usually appears in a council report or a contract award before it is tendered.
  3. Read the expense lines of the town's audited statement of operations, as printed.
  4. Number each record with its link and hand the list to your language model.

The example is Port Ellery, a sandbox town.

The code

Python 3.11 or later
import os

import requests

BASE = "https://data.heytownhall.com"
HEADERS = {"Authorization": f"Bearer {os.environ['HTD_KEY']}"}
TOWN = "port-ellery"
QUESTION = "Which of these tenders fit a civil engineering firm, and what did council decide before each one?"


def get(path, params):
    res = requests.get(f"{BASE}{path}", params=params, headers=HEADERS, timeout=30)
    body = res.json()
    if not res.ok:
        raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
    return body["results"]


records = []
for bid in get("/v1/bids", {"jurisdiction": TOWN}):
    records.append((f"Tender: {bid['title']}, closes {bid['closes_at']}", bid["source_url"]))
    subject = bid["title"].split(": ", 1)[-1]
    for hit in get("/v1/search", {"q": f'"{subject}"', "jurisdiction": TOWN, "in": "agenda_items,motions", "limit": 5}):
        records.append((f"  {hit['date']}, {hit['family'].replace('_', ' ')}: {hit['title']}. {hit['snippet']}", hit["source_url"]))

for line in get("/v1/financial-lines", {"jurisdiction": TOWN, "kind": "statement_of_operations", "limit": 100}):
    if line["role"] == "expense":
        records.append((f"Spent in {line['fiscal_year']}, {line['label']} ({line['label_plain']}): ${line['printed']}", line["source_url"]))

prompt = "\n".join([
    QUESTION,
    "Answer only from these records. Cite each claim by its number and keep its link. If the records do not answer the question, say so.",
    "",
    *(f"[{i}] {text} ({url})" for i, (text, url) in enumerate(records, 1)),
])
print(prompt)  # Send this to your own language model.
JavaScript (Node 18 or later)
const BASE = 'https://data.heytownhall.com';
const headers = { Authorization: `Bearer ${process.env.HTD_KEY}` };
const TOWN = 'port-ellery';
const QUESTION = 'Which of these tenders fit a civil engineering firm, and what did council decide before each one?';

async function get(path, params) {
  const url = new URL(path, BASE);
  for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
  const res = await fetch(url, { headers });
  const body = await res.json();
  if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
  return body.results;
}

const records = [];
for (const bid of await get('/v1/bids', { jurisdiction: TOWN })) {
  records.push([`Tender: ${bid.title}, closes ${bid.closes_at}`, bid.source_url]);
  const subject = bid.title.split(': ').slice(1).join(': ') || bid.title;
  for (const hit of await get('/v1/search', { q: `"${subject}"`, jurisdiction: TOWN, in: 'agenda_items,motions', limit: 5 })) {
    records.push([`  ${hit.date}, ${hit.family.replaceAll('_', ' ')}: ${hit.title}. ${hit.snippet}`, hit.source_url]);
  }
}

for (const line of await get('/v1/financial-lines', { jurisdiction: TOWN, kind: 'statement_of_operations', limit: 100 })) {
  if (line.role === 'expense') records.push([`Spent in ${line.fiscal_year}, ${line.label} (${line.label_plain}): $${line.printed}`, line.source_url]);
}

const prompt = [
  QUESTION,
  'Answer only from these records. Cite each claim by its number and keep its link. If the records do not answer the question, say so.',
  '',
  ...records.map(([text, url], i) => `[${i + 1}] ${text} (${url})`),
].join('\n');
console.log(prompt); // Send this to your own language model.

What the sandbox prints

The prompt from the sandbox
Which of these tenders fit a civil engineering firm, and what did council decide before each one?
Answer only from these records. Cite each claim by its number and keep its link. If the records do not answer the question, say so.

[1] Tender: ITT 2026-06: Snow clearing services 2026-2028, closes 2026-11-21T22:00:00.000Z (https://bids.example/port-ellery/100009)
[2] Tender: RFP 2026-00: Eastside water main replacement, closes 2026-11-20T22:00:00.000Z (https://bids.example/port-ellery/100006)
[3]   2026-09-21, agenda item: Award of contract for the Eastside water main replacement. THAT City of Port Ellery receive the report "Award of contract for the Eastside water main replacement" for information and direct staff to report back. (https://port-ellery.civicweb.example/meetings/900006)
[4]   2026-08-14, agenda item: Award of contract for the Eastside water main replacement. THAT City of Port Ellery receive the report "Award of contract for the Eastside water main replacement" for information and direct staff to report back. (https://port-ellery.civicweb.example/meetings/900005)
[5]   2026-08-14, motion: R814-1. THAT City of Port Ellery approve the recommendation on "Award of contract for the Eastside water main replacement". (https://port-ellery.civicweb.example/meetings/900005/minutes.pdf)
[6]   2026-07-24, agenda item: Award of contract for the Eastside water main replacement. THAT City of Port Ellery receive the report "Award of contract for the Eastside water main replacement" for information and direct staff to report back. (https://port-ellery.civicweb.example/meetings/900003)
[7]   2026-05-10, agenda item: Award of contract for the Eastside water main replacement. THAT City of Port Ellery receive the report "Award of contract for the Eastside water main replacement" for information and direct staff to report back. (https://port-ellery.civicweb.example/meetings/900001)
[8] Spent in 2025, Protective services (Police, fire and bylaw enforcement): $3,757,103 (https://portellery.example/finance/2025-statements.pdf)
[9] Spent in 2025, Transportation services (Roads, sidewalks and transit): $3,416,411 (https://portellery.example/finance/2025-statements.pdf)
[10] Spent in 2025, Water utility (Water supply and distribution): $3,296,483 (https://portellery.example/finance/2025-statements.pdf)

Going further

  • Every town you sell to: set region=BC on the tenders instead of jurisdiction, then search each tender's own town.
  • Budget documents a town publishes before it tenders are in GET /v1/budgets, with their links.
  • Tenders as they appear: read post events in the change feed and keep the ids you have seen.

On this page