Spendict — the performance-marketing brain for AI agents
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_centsso margin is observable from day one.
Local development (zero accounts needed)
pnpm install
cp .env.example .env.local
# in .env.local set:
# SPENDICT_MOCK_MODEL=1 → deterministic heuristic verdicts, no inference cost
# SPENDICT_DEV_API_KEY=test_key → this string works as an API key locally
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 # unit tests (gating matrix, parsing, mock engine)
pnpm typecheck
pnpm calibrate # run the calibration set through the engine (see below)
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/killcall 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 saysrunwhere you'd saykill). - Re-run on every prompt edit — it's the regression test for the IP.
pnpm calibrate -- --strictexits 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; regeneratesconvex/_generated), thennpx convex deployfor prod. SetNEXT_PUBLIC_CONVEX_URL. Generate a secret (openssl rand -hex 32) and set it asSPENDICT_INTERNAL_KEYin 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;/dashboardis the only gated area. - OpenRouter — set
OPENROUTER_API_KEY, pickSPENDICT_MODEL(+SPENDICT_VISION_MODELforcreative_urlcalls) from calibration. EnsureSPENDICT_MOCK_MODELis unset. - Polar — create Starter ($19) and Pro ($49) subscription products; set
POLAR_PRODUCT_ID_*, checkout links asPOLAR_CHECKOUT_URL_*, and a webhook tohttps://<domain>/api/webhooks/polarwithPOLAR_WEBHOOK_SECRET(events:subscription.active,subscription.updated,subscription.canceled,subscription.revoked). Checkout email must match the Clerk signup email (or passcustomer_external_id= Clerk user id). - Vercel — deploy, set all env vars, set
NEXT_PUBLIC_APP_URLto the real domain. - Do not set
SPENDICT_DEV_API_KEYin production. - Distribution — list on the MCP registry, mcp.so, MCP Market, Smithery; the tool description in
src/app/api/[transport]/route.tsis 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 gatesrc/lib/prompt.ts— the crown jewel: versioned framework system promptsrc/lib/engine.ts— OpenRouter call, retry, parse, mock modesrc/lib/assess-service.ts— shared pipeline (validate → meter → infer → log)src/lib/store.ts— Convex-backed key/quota/logging facade + dev fallbackconvex/— schema + functions (gateway, dashboard, billing)scripts/calibrate.ts+calibration/— the quality barsrc/app/— landing, docs, dashboard, API routes
© Space Cadet d.o.o.