ADR-013 Accepted 2026-06-02

Recurring Advertiser Subscriptions

Advertisers may establish a recurring monthly or yearly Stripe subscription that automatically credits ad spend each billing cycle, maintaining catalog rank without manual rebidding.

Context

The base madcatalog model is pay-per-bid: an autonomous agent must actively call POST /ads/bid (or the MCP place_ad tool) for each spend increment. Rank then decays relative to competitors who bid more frequently. Two problems emerge at scale:

  1. Operational burden — running a polling loop that checks rank via GET /ads/{ad_id}/rank-status ($1.00 per check) and rebids on every overtake is non-trivial to operate reliably.
  2. Revenue smoothing — per-bid revenue is lumpy. A subscription model gives madcatalog predictable recurring revenue and gives advertisers predictable cost.

The previously considered alternative — gating machine catalog reads (ADR-010 planned behavior) — was rejected in favour of subscriptions because subscriptions align revenue with advertiser commitment rather than punishing information-seeking behaviour.

Decision

Add a subscription revenue rail alongside the existing per-bid and per-poll rails. Implementation:

  • REST API — POST /subscriptions, GET /subscriptions/{id}, DELETE /subscriptions/{id}. Authenticated with X-Advertiser-Api-Key.
  • MCP tools — create_subscription, get_subscription, cancel_subscription.
  • Intervals — monthly ($10/mo) or yearly ($99/yr). Prices are env-var-configurable (SUBSCRIPTION_MONTHLY_PRICE_USD_CENTS, SUBSCRIPTION_YEARLY_PRICE_USD_CENTS).
  • Spend credit — each successful Stripe invoice credits the billed amount to the advertiser’s total_spend_usd_cents and, if a target_ad_id is specified, to that ad’s cumulative_spend_usd_cents — boosting its catalog rank automatically.
  • Payment rail — Stripe subscriptions only. Tempo crypto payments are not supported for recurring billing at this time.
  • Cancellation — DELETE /subscriptions/{id} sets cancel_at_period_end=true; the subscription remains active until current_period_end and one final invoice credit fires.

Consequences

Positive

  • Predictable MRR / ARR alongside lumpy per-bid revenue.
  • Advertisers can set a subscription with a target_ad_id and stop operating a polling loop entirely — rank climbs passively.
  • Telemetry captures all three revenue rails independently in Grafana (subscription.payment.amount_cents, subscription.created, subscription.canceled).
  • Fiat vs crypto breakdown is now first-class in the overview dashboard for all revenue sources, including subscriptions (always Stripe/fiat).

Neutral / Trade-offs

  • Subscriptions require a madcatalog-subscriptions DynamoDB table with two GSIs (advertiser_slug-index, stripe_subscription_id-index), adding operational surface.
  • The Stripe webhook handler must now process five event types instead of three, increasing complexity; each new event type is guarded by independent try/catch to avoid cascading failures.
  • Spend credited from subscriptions is indistinguishable from bid-based spend in the ad record itself. A future audit field (subscription_spend_usd_cents) could be added without breaking existing rank logic.