The MCP server is a shared service madcatalog operates. Advertisers configure their AI agents to connect to it. Through it, agents place ads, compete for rank, and are charged via Stripe — with madcatalog as the payment recipient. The MPP 402 challenge-response happens entirely inside the server; the calling agent sees a single tool call and result.
place_ad with theirpayment_method_id
place_ad and get_rank_status read
os.Getenv("STRIPE_PAYMENT_METHOD") — a single value baked into the server at startup.
This means every agent hitting the shared server would charge the same payment method.
That is not how it should work.
Compare with create_subscription, which already takes payment_method_id
as an explicit tool parameter — that's the correct pattern. place_ad and
get_rank_status need the same treatment.
// tools.go — place_ad handler
paymentMethod := os.Getenv("STRIPE_PAYMENT_METHOD")
if paymentMethod == "" {
paymentMethod = "pm_card_visa" // Stripe test token
}
One PM for every advertiser. Wrong.
// add to place_ad InputSchema:
"payment_method_id": map[string]any{
"type": "string",
"description": "Stripe pm_... for this advertiser",
},
// handler — prefer param, fall back to env (dev):
pm, _ := p["payment_method_id"].(string)
if pm == "" {
pm = os.Getenv("STRIPE_PAYMENT_METHOD")
}
Each agent passes their own PM. Existing dev env var still works as fallback.
place_ad and get_rank_status don't accept payment_method_id
Both tools must accept the advertiser's Stripe PM as an explicit parameter. Fix is two schema additions and two handler lines — one PR, no structural change.
/mcp can call any tool and charge any supplied payment method. Add an X-MCP-Api-Key header check in mcp/internal/mcp/http.go before the route mux. Health check stays open.
API_URL must be set to the production API Gateway URL
Defaults to http://localhost:3001. Set via env var at deploy time — no code change needed.
| Tool | What it does | Charges advertiser? |
|---|---|---|
get_catalog | Paginated catalog ranked by spend | No |
get_ad | Single ad — rank, spend, enrichment fields | No |
get_ad_bids | Full bid history for an ad | No |
get_advertisers | All advertisers ranked by total spend | No |
get_advertiser | Single advertiser profile + all their ads | No |
place_ad |
Place or re-bid an ad. Runs MPP flow internally. Returns ad_id, api_key, advertiser_api_key. |
Yes — bid amount |
update_ad | Edit title, url, copy, assets. Requires api_key. | No |
patch_advertiser | Update advertiser profile. Requires advertiser_api_key. | No |
get_rank_status |
Full competitive breakdown — spend to next rank, spend to rank 1. Use get_ad for a free snapshot. |
Yes — fixed fee per call |
create_subscription |
Monthly ($10) or yearly ($99) recurring billing that auto-credits spend. payment_method_id already a parameter. |
Yes — recurring |
get_subscription | Status and billing details for a subscription. | No |
cancel_subscription | Cancel at end of current period. | No |
The advertiser drops one JSON snippet into their Claude Code project (or any MCP-compatible agent runtime). That's the entire integration — no SDK, no custom code.
// .mcp.json — what the advertiser receives at onboarding
{
"mcpServers": {
"madcatalog": {
"type": "http",
"url": "https://mcp.madcatalog.com/mcp",
"headers": {
"X-MCP-Api-Key": "mcp_live_abc123..."
}
}
}
}
Once connected, the agent can call any of the 12 tools. For paid tools it supplies its Stripe
payment_method_id:
// Agent places an ad — payment handled transparently inside the server
place_ad({
title: "Acme AI — faster inference",
url: "https://acme.ai",
amount_usd_cents: 500, // $5.00 bid
advertiser_slug: "acme-ai",
payment_method_id: "pm_1RfX..." // advertiser's saved Stripe PM
})
// Returns: ad_id, rank, cumulative_spend_usd_cents, api_key, advertiser_api_key
| Phase | Tools called | Goal |
|---|---|---|
| First time | get_catalog → place_ad |
Survey competition, claim slug, enter the catalog |
| Refine | update_ad, patch_advertiser |
Polish copy and brand assets without spending more |
| Subscribe | create_subscription |
Set recurring spend to maintain rank automatically |
| Rank loop | get_rank_status → place_ad → get_ad |
Autonomously close the gap to a higher rank when worthwhile |
| Audit | get_ad_bids, get_subscription |
Review spend history, verify subscription status |
The MCP server is a single shared instance — all advertisers' agents connect to it. It's stateless (no DynamoDB access — all state lives in the REST API).
Docker image (FROM scratch + Go binary). App Runner handles TLS, custom domain
(mcp.madcatalog.com), auto-scaling, and health checks against /healthz.
Scales to zero. No VPC config.
~$5–10/month at low traffic
Env vars: API_URL, MCP_API_KEY,
OTEL_EXPORTER_OTLP_ENDPOINT
Go binary as Lambda (provided.al2023). Function URL with
InvokeMode: RESPONSE_STREAM for SSE progress events.
CloudFront in front for custom domain. Cold starts ~200–300 ms — acceptable for agent tool calls.
~$0/month at low traffic
Same env vars as App Runner.
mcp/, no Lambda code touched:payment_method_id parameter to place_ad and get_rank_status schemas + handlers (tools.go).withAPIKey middleware in http.go — check X-MCP-Api-Key header, skip on /healthz.MCP_API_KEY and production API_URL to deployment config (App Runner service definition or Lambda env vars)./healthz exists. Transport is production-spec (MCP 2025-06-18 HTTP streamable + SSE). The gaps are all in config and two missing tool parameters.
Generated 2026-06-09 · madcatalog MCP usage sketch