Work · Independent · Apr 2026–present
A GTM data stack from scratch
I built a revenue and GTM data stack from scratch and ran it in production: five ATS APIs re-checked at the source, technology detection from first-party signals, enrichment that quarantines unverified rows, and a thirteen-prompt LLM pipeline that drops any claim it cannot trace to the source page.
Problem
Vendor lists go stale. Cached indexes keep filled roles listed as open. A no-code canvas stops at the endpoint the vendor did not expose. Language models invent a CRM that was never on the page. I had already watched an extractor of mine read a truncated job description and almost report the wrong CRM into a live campaign.
Constraints
Production, not a demo. Every row needs a source. Unverified contact data does not ship. The operator, not the model, accepts the send. Built with Claude Code and Cursor as daily tools — custom skills, subagents, MCP integrations — with the rule that what an agent may settle without a human is decided up front.
What shipped
- Hiring-signal crawler over Greenhouse, Lever, Ashby, Workable, Teamtailor. Cached indexes go stale, so every posting is re-checked at the source before use. Each row stamped with source API and timestamp. 351 companies verified live at the source. 456 contacts resolved across 219 of them.
- Technology detection from first-party signals: DNS records, HTTP response headers, cookie names, resource hosts. Extracts per-customer IDs (HubSpot portal ID, GA4 measurement ID) as proof. A vendor merely named in page text is not an install and is rejected with a reason.
- Sequencing and deliverability: about 1,500 lines of Python against a sending-platform API — campaign push, rate caps per inbox and per domain, send scheduling in each recipient’s timezone, suppression registry. Where docs were silent, the API was probed.
- LLM pipeline: 13 chained prompts turning a rendered screenshot of a prospect site into a personalized outbound sequence, across 5,000+ accounts sourced from live ad activity. Any claim not traceable to the source page is dropped. Extractors abstain when confidence is low.
- Enrichment via Clay CLI, waterfall plus email verification, push into Instantly. Unverified rows quarantined with a reason.
- Operator UI in Next.js, React, TypeScript: a human accepts, rejects, or corrects before anything ships.
Numbers
351 companies live at the ATS. 456 contacts. 219 companies. 5,000+ accounts through the LLM pipeline. 13 prompts. Five ATS APIs. About 1,500 lines against the sending API.
Stack
Python, TypeScript, Next.js, React, Playwright, Clay CLI, Instantly, DNS and response-header fingerprinting, Anthropic Claude API, MCP.
Guardrail
Extractors abstain when confidence is low. Every row carries its evidence. A vendor name in copy is not an install. A truncated page is not a CRM. The human signs off where the agent would guess.