ATS
All work · 01

From thousands of raw government tenders to a personal bid desk

Client:
TenderPulse, an ATS product
Industry:
Procurement intelligence
Status:
Live product
Stack:
Python · PostgreSQL · Node.js · Express · React · Gemini · Google Cloud Run · Cloud SQL · GCS

Indian government tenders are published across portals that were never designed to be read by machines: CAPTCHAs, session walls, inconsistent document formats, and layouts that change without notice. A contractor who wants complete coverage has to check dozens of portals every day and read hundreds of notices to find the three that matter.

TenderPulse is our answer, built and operated as an ATS product. Purpose-built scraping infrastructure ingests more than 35 portals on a daily schedule, a governed Postgres data layer normalizes everything, and an intelligence API turns each tender into something a bid manager can act on: an explainable relevance score, a decision-ready briefing, and a deadline alert that arrives in time to matter. The bid desk is now a team workspace, with seat-capped invites and named bid profiles that let one firm run several strategies against the same feed.

The system

GOVERNED DATA LAYERpostgres · gcs documentsBID DESKpersonalized web appEMAIL ALERTSclosing soon · daily digest
Hover or focus a node for the engineering note.
fig. 01 · tenderpulse, system architecture

Three decisions that mattered

01

Scraping is infrastructure, not a script

Most tender tools treat ingestion as a cron job that breaks quietly. We built it as a service in its own right, with process isolation per source, time budgets, checkpoint and resume, and a run ledger. The trade-off is more moving parts up front; the return is a pipeline that survives portal changes without losing a day of coverage.

02

One briefing per tender, generated once

Every tender gets exactly one LLM-generated briefing, cached permanently in Postgres. Costs stay flat as the corpus grows and a briefing never silently changes under a user. The trade-off: briefings do not improve retroactively when we upgrade prompts, and we accepted that deliberately.

03

The relevance engine refuses to fake scores

Scoring only runs over criteria a user actually configured, and returns nothing rather than a made-up number when it lacks signal. Every match decomposes into per-criterion evidence in the UI. We fixed a score-inflation bug early and wrote the rationale into the code so the honesty rule outlives us.

The AI layer, honestly

Gemini generates each tender briefing in structured JSON: summary, why it matters, estimated scale, key deadlines, and risk flags. That is the extent of the AI. Relevance scoring is deterministic and explainable, alerts are rule-based, and nothing the model writes can change a deadline or a score. One model call per tender, cached forever, is the entire inference bill.

What broke, and what we changed

The first relevance engine inflated scores when users configured few criteria; it now scores only what is configured and returns null otherwise. Tender attachments were originally served from a public bucket; an internal review flagged it and they now ship as short-lived signed URLs behind auth. A client-side polling storm against the batch-status endpoint was found in production and rate-limit hardening followed. Each fix is documented in the codebase, which we consider part of the product.

Results, and how they were measured

35+

government portals monitored daily

38 sources on the daily schedule, counted in the source registry on 2026-08-01.

80k+

tenders in the governed data layer

81,818 ingested tenders counted live in production on 2026-07-31.

1

LLM call per tender, ever

Briefings are generated once and cached permanently in Postgres.