This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Setup (for operators)
The hosted App at github.com/apps/agenticmail
runs on Netlify Functions. The same code can be re-deployed under any
other GitHub App by setting the env vars below — the function itself is
infrastructure-agnostic (any platform that delivers Request/Response
and supports context.waitUntil works).
Architecture in one diagram
GitHub webhook delivery
│
▼
POST /api/github/webhook
├─ HMAC-SHA256 verify (constant-time)
├─ dedup on X-GitHub-Delivery UUID (5-min TTL)
├─ 202 Accepted in <100ms
│
├─ context.waitUntil(processWebhook(...))
│ ├─ branch by event
│ ├─ rate-limit check (paid verbs)
│ ├─ plan check (paid verbs)
│ ├─ fetchPRFiles (if PR + relevant verb)
│ ├─ generateReply via Anthropic
│ │ └─ writeUsage(account, tokens, cost)
│ ├─ post comment OR state-changing action
│ └─ writeAudit(status, latency, error)
│
└─ writeAudit(accepted)
The function never blocks on agent work. GitHub gets its 202 in under
100ms; the LLM call runs in context.waitUntil() so Netlify keeps the
function alive for up to 15s of background work.
GitHub App settings
When you register the App at Settings → Developer settings → GitHub Apps:
- Webhook URL:
https://<your-host>/api/github/webhook - Webhook secret: strong random string (set on the App and in env as
GITHUB_WEBHOOK_SECRET) - Subscribe to events:
issue_comment,pull_request_review_comment,issues,pull_request,installation,marketplace_purchase
Permissions
| Scope | Access | Why |
|---|---|---|
| Issues | Read + write | Read thread context, post comments, react, close |
| Pull requests | Read + write | Summarize, post review comments, post formal reviews |
| Contents | Read + write | Required for @agenticmail merge (modifies default branch) |
| Metadata | Read | Required by GitHub for any App |
Drop Contents: write if you don't need the merge verb — it's the
only feature that touches branch state.
Environment variables
Required
| Var | Purpose |
|---|---|
GITHUB_APP_ID |
Numeric App ID from settings |
GITHUB_APP_PRIVATE_KEY |
PEM-encoded RSA private key (escaped \n ok) |
GITHUB_WEBHOOK_SECRET |
HMAC secret matching the App's webhook config |
ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY |
At least one — see below |
The function reads ANTHROPIC_AUTH_TOKEN first (Claude OAuth surface,
sk-ant-oat01-…); if absent it falls back to ANTHROPIC_API_KEY
(classic sk-ant-api03-…). OAuth tokens require model
claude-haiku-4-5 or higher — earlier-generation aliases like
claude-3-5-haiku-latest are not visible on the OAuth surface and
return 404.
Optional but recommended
| Var | Purpose |
|---|---|
ADMIN_AUDIT_TOKEN |
Enables /api/github/audit, /usage, /billing |
SENDGRID_API_KEY |
Outbound email (welcome + ops notifications) |
SENDGRID_FROM_EMAIL |
Verified sender address on SendGrid |
AGENTICMAIL_OPS_EMAIL |
Where install/uninstall notifications land |
AGENTICMAIL_SEND_URL |
Custom email endpoint (fallback if no SendGrid) |
AGENTICMAIL_API_KEY |
API key for the custom endpoint |
If no email provider is configured, the function continues working — ops notifications just don't fire.
Netlify Blob stores
The function uses six independent Blob stores:
| Store | Purpose | Key | TTL |
|---|---|---|---|
github-webhook-dedup |
GitHub delivery UUID → seen-at-ms | UUID | 5 min |
github-installations |
Installation metadata | <installation-id> |
install lifetime |
github-webhook-audit |
One entry per delivery (accepted, processed, errored, etc) | <YYYY-MM-DD>/<UUID> |
none (forever) |
github-rate-limit |
Per-installation token bucket | <installation-id> |
1 hour window |
github-billing |
Plan record per Marketplace customer account | <account-login-lowercased> |
until cancelled |
github-usage |
Per-call token + cost telemetry | <YYYY-MM-DD>/<account>/<UUID> |
none (forever) |
Delete a store from the Netlify dashboard (Project → Blobs) only if
you understand what it does. Deleting github-webhook-dedup is safe
(might cause one duplicate reply). Deleting github-billing flips all
paid accounts back to Free until the next marketplace_purchase
webhook re-populates them.
Admin endpoints
All three require x-admin-token: <ADMIN_AUDIT_TOKEN> header.
GET /api/github/audit
GET /api/github/audit?day=YYYY-MM-DD&limit=100&installs=1
Returns the day's delivery entries + (with installs=1) the full
installations list. Use it to answer "did GitHub's reviewer event
arrive?" or "why didn't the bot reply to X?".
GET /api/github/usage
GET /api/github/usage?account=<login>&range=30
Rolls up Anthropic token usage by account. Returns per-verb breakdown
- USD cost estimate at the current claude-haiku-4-5 rate. Use it to sanity-check unit economics before/after pricing changes.
GET | POST | DELETE /api/github/billing
# Inspect plan for one account
GET /api/github/billing?account=<login>
# Comp an account onto a paid plan without a real subscription
POST /api/github/billing body: {"account":"<login>","planName":"Pro"}
# Clear a stale plan record
DELETE /api/github/billing?account=<login>
The marketplace_purchase webhook handler is the primary writer; this
endpoint is an escape hatch for comps and corrections.
Note on propagation: Netlify Blob writes can take up to ~60s to be visible to function instances in other regions. After a DELETE, the gate may briefly still see "paid" for in-flight requests.
Deploy
The frontend repo (agenticmail/frontend) is wired to Netlify with
auto-deploy on push to main. Workflow:
- Make changes locally, commit, push to
main. - Netlify picks up the push, runs
next build, deploys functions. - Health endpoint comes up at
https://<your-host>/api/github/healthin ~3–5 minutes.
For env var changes (no source change), trigger a redeploy from the Netlify dashboard — env var updates do not auto-trigger a deploy.