An MCP server (and REST API) that gives AI agents a performance marketer's judgment on demand. One tool, done exceptionally: assess_ad_creative tells an agent whether an ad creative will actually work, why not, and how to fix it — and returns a deterministic run / fix_first / kill verdict the agent can use to gate its own ad spend, before a cent is spent.
Built per the PRD. v1 sells to the humans building agents (developers, growth engineers, agencies); autonomous agents paying is the narrative upside, not the dependency.
Architecture
MCP client (Claude/Cursor/n8n/…) ──► /api/mcp (Streamable HTTP, mcp-handler)
REST caller ──► /api/v1/assess (same pipeline)
│
validate input → quota consume (BEFORE inference)
│
OpenRouter (framework system prompt, low temp)
│
server recomputes score + recommendation (tamper-proof gate)
│
log usage_event + assessment (the compounding dataset)
- The IP is the system prompt in src/lib/prompt.ts (versioned, server-side only, never exposed) plus the deterministic gating in src/lib/contract.ts. The model provides judgment; the server derives the authoritative verdict, and model-vs-server disagreement is logged as a calibration signal.
- Stack: Next.js (Vercel) · Convex (keys, metering, assessments dataset) · Clerk (dashboard auth) · Polar (billing, MoR) · OpenRouter (inference).
- Metering discipline: quota check happens before inference; failed assessments are refunded; every call logs
cost_cents so margin is observable from day one.
Local development (zero accounts needed)
pnpm install
cp .env.example .env.local
pnpm dev
Smoke it:
curl -s -X POST http://localhost:3000/api/v1/assess \
-H "Authorization: Bearer test_key" -H "Content-Type: application/json" \
-d '{"ad_copy":{"primary_text":"Still paying $200/mo for a gym you visit twice?"},"platform":"meta","product_context":"home fitness app"}' | jq
Real inference locally: set OPENROUTER_API_KEY, remove SPENDICT_MOCK_MODEL.
pnpm test
pnpm typecheck
pnpm calibrate
Calibration — the launch bar, not optional
The product is the judgment. Before launch (PRD §13):
- Replace calibration/calibration-set.json (10 starter examples) with 30–50 real ads you can personally judge, each with your own
run/fix_first/kill call written before running the engine.
OPENROUTER_API_KEY=… pnpm calibrate
- Where the server diverges from you, the prompt is wrong — tighten src/lib/prompt.ts (bump
PROMPT_VERSION) until agreement ≥80% and there are zero false greenlights (server says run where you'd say kill).
- Re-run on every prompt edit — it's the regression test for the IP.
pnpm calibrate -- --strict exits non-zero below the bar (CI-able).
Model choice: bench 2–3 OpenRouter models on the set via SPENDICT_MODEL=… pnpm calibrate; pick on judgment-quality-per-cost.
Going live (checklist)
- Convex —
npx convex dev (creates deployment; regenerates convex/_generated), then npx convex deploy for prod. Set NEXT_PUBLIC_CONVEX_URL. Generate a secret (openssl rand -hex 32) and set it as SPENDICT_INTERNAL_KEY in both the Convex dashboard env and the Next.js env.
- Clerk — create app, set
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY + CLERK_SECRET_KEY. Hosted sign-in works out of the box; /dashboard is the only gated area.
- OpenRouter — set
OPENROUTER_API_KEY, pick SPENDICT_MODEL (+ SPENDICT_VISION_MODEL for creative_url calls) from calibration. Ensure SPENDICT_MOCK_MODEL is unset.
- Polar — create Starter ($19) and Pro ($49) subscription products; set
POLAR_PRODUCT_ID_*, checkout links as POLAR_CHECKOUT_URL_*, and a webhook to https://<domain>/api/webhooks/polar with POLAR_WEBHOOK_SECRET (events: subscription.active, subscription.updated, subscription.canceled, subscription.revoked). Checkout email must match the Clerk signup email (or pass customer_external_id = Clerk user id).
- Vercel — deploy, set all env vars, set
NEXT_PUBLIC_APP_URL to the real domain.
- Do not set
SPENDICT_DEV_API_KEY in production.
- Distribution — list on the MCP registry, mcp.so, MCP Market, Smithery; the tool description in
src/app/api/[transport]/route.ts is the listing copy.
Endpoints
| Endpoint | What |
|---|
POST /api/mcp | MCP Streamable HTTP — tool assess_ad_creative; auth Authorization: Bearer spd_live_… |
POST /api/v1/assess | REST front door, same pipeline |
GET /api/health | liveness + prompt version |
POST /api/webhooks/polar | billing webhook |
/dashboard | signup, API keys, usage (the only human UI) |
/docs | agent-builder quickstart |
Quota errors come back structured ({"error":"quota_exceeded","upgrade_url":…}), never as crashes, so agents can handle them.
Repo map
src/lib/contract.ts — input/output schemas, deterministic scoring + spend gate
src/lib/prompt.ts — the crown jewel: versioned framework system prompt
src/lib/engine.ts — OpenRouter call, retry, parse, mock mode
src/lib/assess-service.ts — shared pipeline (validate → meter → infer → log)
src/lib/store.ts — Convex-backed key/quota/logging facade + dev fallback
convex/ — schema + functions (gateway, dashboard, billing)
scripts/calibrate.ts + calibration/ — the quality bar
src/app/ — landing, docs, dashboard, API routes
© Space Cadet d.o.o.