Restaurant Growth Grader
Operationalhigh@atrium/graderLead-gen tool: scans a restaurant and returns a scored growth report.
checked 3m ago· owner Atrium
Checks
Grader page
The lead-gen funnel is closed.
100%
24h
100%
30d
GET https://atrium-grader.vercel.appexpects 200last 200 · 244ms · 3m ago
Search endpoint
Visitors cannot find their restaurant, so no scan can start.
100%
24h
100%
30d
GET https://atrium-grader.vercel.app/api/grader/searchexpects 405 or 400last 405 · 1959ms · 3m ago
What it is
A single page and six route handlers. A visitor searches for their restaurant, picks it from Google Autocomplete, and the scan fans out across four providers in parallel. The output is a scored report with an LLM-written narrative on top of it.
This is the only app in the estate that spends money per request — every scan costs Google Places calls plus one LLM completion — which makes its budget guard as important as its uptime.
What one scan actually does
POST /api/grader orchestrates:
- Business data — Google Places v1: details, opening hours, categories.
- Website — PageSpeed Insights (Lighthouse) plus a direct fetch of the restaurant's HTML.
- Benchmark — Google
searchNearbyfor comparable venues in the area. - Reputation — the Places reputation summary (rating, review count).
- Social — ScrapeCreators, only when a profile is confidently detected. Skipped otherwise, and the report says so.
- Narrative — one LLM completion that turns the numbers into prose.
Every step reports its own confidence and is recorded in the scan evidence, so a partial scan is still a usable report rather than a silent hole.
Outbound safety
The website scanner fetches URLs a stranger typed, so lib/safe-fetch.ts sits in front of it: http(s) only, private / loopback / link-local addresses rejected, response body size capped, hard timeout. This is the one place in the estate where SSRF is a live risk, and it is guarded deliberately.
Cost control — and why it is currently off
lib/providers/business-provider.ts counts Google Places calls against a daily and a monthly ceiling. Both ceilings come from env vars. Neither is set, so recordGooglePlacesUsage returns before it counts anything and the guard is a no-op.
Even once set, the counters live in process memory: they reset on every cold start and do not hold across serverless instances. Treat them as a speed bump, not a budget.
Rate limiting
middleware.ts limits per IP with an in-memory counter, with the same caveat — one instance, one counter. The file itself flags the fix: an Upstash-backed limiter before this runs multi-region. The status app already has an Upstash connection it could share.
Choosing the narrative model
GRADER_AI_PROVIDER selects the provider at request time: openrouter (default), google, or anthropic. Each needs its own key. Selecting anthropic today throws at model init because ANTHROPIC_API_KEY is not set anywhere — the narrative then returns null and the report renders without prose instead of failing loudly.
When it breaks
Symptom
Scans return but every report is missing its narrative
Check
GRADER_AI_PROVIDER and the matching key in the deployment environment.
Fix
Set the provider's key, or switch GRADER_AI_PROVIDER back to openrouter.
Symptom
Scans fail at the business-data step
Check
The Google Places monitor, then the Google Cloud console for quota or billing.
Fix
Restore quota. There is no fallback provider — despite the name, lib/open-data-places.ts is Google only.
Symptom
Unexpected Google bill
Check
GRADER_GOOGLE_DAILY_LIMIT and GRADER_GOOGLE_MONTHLY_LIMIT — if unset, nothing was ever capped.
Fix
Set both, and move the counters to Upstash so they survive restarts.
Symptom
Social section always empty
Check
SCRAPECREATORS_API_KEY; the social route 503s without it.
Fix
Set the key. The report is designed to degrade here, so this fails quietly by design.
Needs
- Google Places API v1Every piece of business data in a grader scan — and the only paid call per scan.
- PageSpeed InsightsLighthouse scores for the website half of a grader scan.
- ScrapeCreatorsPublic social profile data for the optional social step of a grader scan.
- LLM providersOpenRouter, Anthropic or Google — whichever writes the grader's narrative.
Configuration
| variable | status | purpose |
|---|---|---|
GOOGLE_PLACES_API_KEYapps/atrium.grader/.env | set | Auth for every Google Places call. |
PAGESPEED_API_KEYapps/atrium.grader/.env | set | Gates and authenticates the PageSpeed audit. |
SCRAPECREATORS_API_KEYapps/atrium.grader/.env | set | Auth for social scraping. |
OPENROUTER_API_KEYapps/atrium.grader/.env | set | Default narrative provider. |
GOOGLE_GENERATIVE_AI_API_KEYapps/atrium.grader/.env | set | Narrative provider when GRADER_AI_PROVIDER=google. |
ANTHROPIC_API_KEYread at lib/report-agent.ts:156 | missing | Narrative provider when GRADER_AI_PROVIDER=anthropic.In neither .env nor .env.example. Selecting anthropic throws at model init and the narrative silently returns null. |
GRADER_GOOGLE_DAILY_LIMITapps/atrium.grader/.env.example | missing | Per-day cap on Google Places calls.Unset, so the spend guard never counts. Both limits must be set for it to engage. |
GRADER_GOOGLE_MONTHLY_LIMITapps/atrium.grader/.env.example | missing | Per-month cap on Google Places calls. |
GRADER_BUSINESS_PROVIDERapps/atrium.grader/.env | dead | Intended osm|google|auto switch.No code reads it. Setting it changes nothing. |
WEBSITE_AUDIT_PROVIDERapps/atrium.grader/.env | dead | Intended basic|pagespeed switch.No code reads it. |
Where to start reading
Scan orchestration
apps/atrium.grader/app/api/grader/route.tsGoogle Places client
apps/atrium.grader/lib/google-places-client.tsNarrative model selection
apps/atrium.grader/lib/report-agent.tsSSRF guard
apps/atrium.grader/lib/safe-fetch.tsSpend guard
apps/atrium.grader/lib/providers/business-provider.ts
In the repo
path
apps/atrium.graderversion
files
lines
routes
depends on
Incident history
No incidents recorded for this system.