A self-hosted service that monitors your Gmail inbox, matches emails to deals in Attio CRM, and uses Claude AI to automatically update deal stages based on email context.
Gmail → match contacts → find deals → Claude analysis → update Attio stage + add note
Every 5 minutes (configurable), the service:
- Fetches new emails from Gmail since the last run
- Extracts all email addresses (from, to, cc) and filters out internal ones
- For each external address, looks up the contact in Attio
- For each matching deal, asks Claude to analyze the email and suggest a stage update
- If Claude is confident enough, updates the deal stage and adds an audit note
All stage changes are recorded as notes on the deal with Claude's reasoning.
- Node.js 18+
- An Attio workspace with an API key
- An Anthropic API key
- A Google Cloud project with the Gmail API enabled
- Go to Google Cloud Console and create a project
- Enable the Gmail API (APIs & Services → Library → Gmail API)
- Create OAuth2 credentials (APIs & Services → Credentials → Create Credentials → OAuth client ID)
- Application type: Desktop app (simplest for self-hosting)
- Add
http://localhost:3099/oauth2callbackas an authorized redirect URI - If your OAuth consent screen is in "Testing" mode, add your email as a test user
cd email-deal-screener
npm installnpm run auth-setupThis opens a browser authorization flow and prints your GMAIL_REFRESH_TOKEN. Run this once — the token doesn't expire unless you revoke access.
Copy .env.example to .env and fill in all values:
cp .env.example .env| Variable | Required | Description |
|---|---|---|
ATTIO_API_KEY |
Yes | Attio workspace API key |
ANTHROPIC_API_KEY |
Yes | Anthropic API key |
GMAIL_CLIENT_ID |
Yes | Google OAuth2 client ID |
GMAIL_CLIENT_SECRET |
Yes | Google OAuth2 client secret |
GMAIL_REFRESH_TOKEN |
Yes | From npm run auth-setup |
GMAIL_USER_EMAIL |
Yes | The Gmail address to monitor |
INTERNAL_DOMAINS |
No | Comma-separated domains to skip (e.g. yourco.com) |
ATTIO_DEAL_OBJECT |
No | Attio object slug for deals (default: deals) |
ATTIO_STAGE_ATTRIBUTE |
No | Attio attribute slug for stage (default: stage) |
PORT |
No | HTTP server port (default: 3001) |
CRON_SCHEDULE |
No | Cron expression for polling (default: */5 * * * *) |
MIN_CONFIDENCE |
No | Minimum confidence to apply updates: high, medium, low (default: medium) |
# Development (auto-restarts on file changes)
npm run dev
# Production
npm run build
npm startTrigger an immediate run without waiting for the next cron tick:
curl -X POST http://localhost:3001/run-nowRun once and exit (useful for testing):
npm run screen-nowcurl http://localhost:3001/healthThe service outputs structured JSON logs:
{"ts":"...","event":"run_start","since":"..."}
{"ts":"...","event":"fetch_complete","count":12}
{"ts":"...","event":"recommendation","deal":"Acme Corp","currentStage":"Discovery","suggestedStage":"Proposal","confidence":"high","reasoning":"..."}
{"ts":"...","event":"stage_updated","deal":"Acme Corp","from":"Discovery","to":"Proposal","confidence":"high"}
{"ts":"...","event":"run_complete","emails":12,"dealsEvaluated":3,"updatesMade":1}The service is a plain Node.js process — deploy it anywhere you can run Node:
- VPS / server: run with
pm2orsystemd - Docker: the service has no persistent state beyond
.sync-state.json(mount a volume for that) - Fly.io / Railway: set env vars via their dashboard, run
npm start
MIT