DEPLOYMENTS / AGENCY CRM
AI-DRIVEN CRMAgency CRM
The CRM that runs our own agency. A nightly scraper pulls ~100 Slovenian companies into Postgres, Loops fires the outreach email, Claude reads every reply, and the pipeline updates itself — all we do is pick up the phone when someone is interested.
CLIENT
G.S. Digital Anchor
YEAR
2026
SYSTEMS
AI · WEB
STATUS
DEPLOYED
100
leads scraped / day, unattended
3
Claude classifications: interested · rejected · unclear
0
closed-without-deal rows (DB-enforced)
[ THE BRIEF ]
The challenge we walked into.
A two-person agency spending an hour a day copying leads into spreadsheets, another two hours reading and classifying reply emails, and losing track of who to follow up with. We needed the whole outreach loop — find company, email company, understand the reply, update the pipeline — to run without a human, so the humans could stay on the calls that actually close.
[ TOPOLOGY ]
How the system is wired.
// SYSTEM TOPOLOGY
[ WHAT WE SHIPPED ]
Feature by feature.
~100 fresh Slovenian companies every night.
A Playwright job scrapes bizi.si, dedupes on the company slug (external_id, unique), and writes new contacts straight into Postgres via the service-role key. It also remembers every company it's ever looked at in scraped_companies, so no company gets processed twice — even the phone-less ones we skip today can be pulled for email outreach later.
- ▸Dedup on external_id (bizi slug)
- ▸scraped_companies memory: never re-scrape
- ▸Service-role key stays server-side
- ▸Auto-triggers outreach for phone-less contacts
[ APPROACH ]
How it came together.
01 — WHERE THE AI LIVES
One Edge Function, one Anthropic key, one job.
The ANTHROPIC_API_KEY is set once as a Supabase secret and lives only inside the email-inbound Edge Function. The client bundle never sees it. Claude is called with a strict output schema — classification must be one of three enum values, summary must be a 1-2 sentence Slovene string — and if the model refuses or the JSON parse fails, the function returns 'unclear' rather than blocking the pipeline.
- ▸Server-only secret (Supabase secrets)
- ▸Structured output via JSON schema
- ▸Refusal → 'unclear' fallback
- ▸Prompt: strict rules for a Slovenian outreach context
02 — TRUSTING THE PIPELINE
Every AI decision is auditable in Postgres.
Every inbound reply lands in the inbound_emails table (deduped on Message-ID), and every status change writes a status_events row via trigger. Six months from now, we can prove exactly what Claude said about which company on which day, and which of those decisions turned into deals. The AI's contribution is a foreign key away from revenue.
- ▸inbound_emails table — every reply archived
- ▸Message-ID dedup: never process twice
- ▸status_events trigger: full history log
- ▸Classification joined to revenue for post-hoc audit
03 — FAILING SAFE, EVERYWHERE
The loop must run even when a service is down.
Loops send fails → the scraper still writes the contact. Claude refuses → the reply is stored as 'unclear' and the rep sees it manually. Non-CRM senders → the mailbox is a firehose so unrelated mail is ignored without archiving. Every external dependency has a defined failure mode, and none of them can stall the human queue.
04 — DATA MODEL
The invariants live in the database, not the app.
A deferred constraint trigger makes 'closed contact without a deal' impossible at the database level. status_events is written by a trigger so the funnel stays complete even if rows are edited outside the app. RLS is on with a simple 'authenticated users can do everything' policy — only the anon key ships to the browser, the scraper uses the service role.
- ▸Deferred FK: closed implies deal
- ▸Trigger-written status history
- ▸RLS on every table
- ▸Service-role key only in scraper env
[ DELIVERY ]
What the build looked like.
01 ▊
[ TECH STACK ]
What’s under the hood.
HOVER FOR ROLE
[ IMPACT ]
What changed.
The agency stopped losing leads and stopped reading email. The scraper finds companies, Loops sends the outreach, Claude reads and summarises every reply, and the pipeline updates itself — the humans only touch the ones that turn 'interested'. Because every AI decision is stored beside its outcome, we can prove which classifications actually convert. The two of us now run more outreach than a five-person team.