For agents
MRR Ledger is built to be read by machines. Free, CC BY 4.0, no key needed for the static endpoints.
Endpoints
GET /api/v1/businesses.json | All businesses with sourced metrics |
GET /api/v1/businesses/{slug}.json | One business |
GET /api/v1/leaderboard.json | Ranked by annualized revenue |
GET /api/v1/leaderboard.csv | Same ranking as CSV (one row per ranked business: headline label, raw value_usd and value_text, basis, as_of, source_url, profile_url). Opens in Sheets/Excel/pandas. |
GET /api/v1/categories.json | Category insights: id/slug, label, count, ranked_count, median_annualized_usd (among ranked), median_annualized_by_tier, members[{slug,name,rank,annualized_revenue_usd,url_profile}], method. Leaderboard deep-link /leaderboard/?category=<slug>. MCP list_categories / get_category. |
GET /api/v1/tags.json | Tag insights: slug, label, count, ranked_count, median_annualized_usd (among ranked), median_annualized_by_tier, members[{slug,name,rank,annualized_revenue_usd,url_profile}], method. Sorted by count desc then label. HTML /tags/ shows count≥2 only. Leaderboard deep-link /leaderboard/?tag=<slug>. MCP list_tags / get_tag. |
GET /api/v1/feature.json | Today's featured business |
GET /api/v1/features.json | 14-day feature rotation (entries, card_url, is_today) |
GET /api/v1/stats.json | Hub totals: businesses, chartable, lessons, tiers/categories, stale counts, median annualized by tier and by category, revenue_disclosed_count / revenue_undisclosed_count, agent_native_count / agent_added_count |
GET /badge/{slug}.svg | Embeddable 20px SVG badge: rank + raw headline metric (marks reported / last synced). /badge/{slug}-plain.svg = "listed", no number. URLs also on each record as badge_url / badge_plain_url. |
GET /api/v1/newsletter.json | This week's issue: feature, new businesses, number moves, indie corner, lesson, upcoming, and paste-ready markdown |
GET /api/v1/growth.json | Businesses with multi-point growth series |
GET /api/v1/growth.csv | Every growth_series point as CSV (slug, as_of, label, value_usd, annualized_usd, basis, confidence, source_url, profile_url, growth_json_url). Free under CC BY 4.0. Full all-metrics history CSV stays Pro. |
GET /api/v1/growth/{slug}.json | Dated annualized revenue points for one business |
GET /api/v1/lessons.json | Every lesson, attributed, with keyword-tagged themes (rules included) |
GET /api/v1/insights.json | Pricing-model buckets with medians, and time to $100M (with sources and method) |
GET /api/v1/sources.json | Every cited source, which businesses use it, and its last automated link check |
GET /api/v1/changes.json | Dated change log: businesses added, new sourced points, revisions (newest first) |
GET /api/v1/pricing.json | Plans, prices and Stripe checkout links (paid plans fulfilled by email) |
GET /api/v1/compare.json?a=&b= | Compare two businesses by slug (briefs + diff). Needs the Vercel compare function; also MCP compare_businesses |
GET /api/v1/search.json · ?q=&category=&tier=&verified_only=&limit= | Substring search over name, slug, one_liner, tags, lessons. verified_only=true keeps only businesses whose headline figure is payment-processor verified (headline_verification not null). Static file = full index; query params need the Vercel search function. MCP search_businesses covers the same job for agents. |
/openapi.json · /openapi-3.0.json | OpenAPI 3.1 spec, plus an OpenAPI 3.0.3 copy for tools that only import 3.0 |
/llms.txt, /llms-full.txt | LLM-friendly summaries |
/playbook/ | Human page: lessons library, pricing models, time to $100M |
/compare/ | Human page: side-by-side compare (?a=&b= deep links) |
/features/ | Human page: 14-day featured rotation archive |
/mcp | MCP server (Streamable HTTP, no key). Canonical endpoint: https://mrrledger.com/mcp (POST JSON-RPC, answered directly; /mcp/ also works). Tools (all read-only, annotated readOnlyHint): search_businesses, get_business, leaderboard, todays_feature, get_growth, search_lessons, whats_new, compare_businesses, hub_stats, list_categories, list_tags, get_tag, get_category. Resources: mrrledger://llms.txt, mrrledger://leaderboard, mrrledger://stats, mrrledger://changes |
/.well-known/mcp-server.json | MCP Registry manifest (server.json format) for registries and clients |
Field notes
- tier (venture, indie, experiment): Tier is about funding, not size: Venture-backed = raised outside venture money (even at under $1M annualized); Indie = bootstrapped or founder-funded; Experiment = agent-run or research project.
- confidence:
"reported"= press-reported, not company-confirmed (amber reported pill);"self-reported"= stated by the company itself (release, founder post), not independently verified (dashed self-reported pill). - headline_verification (records, leaderboard, search and MCP briefs): how the ranked figure itself is verified,
{method: "payment-processor", processor, via}when it comes from a payment-connected source (e.g. TrustMRR via Stripe), else null. Useverified_only=trueon/api/v1/search.jsonor MCPsearch_businesses/leaderboardto keep only those. - verified_corroboration: when the headline is self-reported or press-reported and a payment-verified point (e.g. a TrustMRR Stripe or Paddle snapshot) also exists, this object carries it (
label, value_usd, annualized_usd, as_of, source_url, source_name, source_sync_stale_since, verification). Shown as a muted "Stripe-verified …" / "Paddle-verified …" line under the headline on the leaderboard and profile. Null otherwise. - source_sync_stale_since (YYYY-MM-DD, on metrics and on each record's headline): the source page says its live data connection stopped (e.g. an expired Stripe key on TrustMRR). The value is the last synced figure; treat it as dated that day. Also listed in sources.json
summary.source_sync_stale. - ownership / independent:
ownershipappears only when a business was acquired ({status: "acquired", acquirer, as_of, source_url});independentis then false. Tier still describes pre-sale funding, so a bootstrapped company sold for cash stays indie. Acquired rows show a purple acquired pill, are skipped in the newsletter indie corner and the home indie grid, and are left out ofstats.jsonmedian_annualized_indie_independent. - last_ownership_check (YYYY-MM-DD or null): date of the latest active ownership search recorded in
data/ownership_checks.json(verdict independent / acquired / watch). Present only when we have searched; unchecked rows stay null — never invented. Profiles with a ledger entry show a muted "Ownership checked YYYY-MM-DD" line. - ownership_watch: present only when the ownership ledger verdict is
watch({verdict, checked, note, source_url?}) — sale process or for-sale signal, no closed deal. Amber watch pill on profile + leaderboard; never invents acquisitions. - agentic_origin (optional string):
agent-native= product is agents / an agent platform from the start;agent-added= established product that later added agents or MCP (amber agent-added pill on profile + leaderboard);unclearor omitted when not confident. Leaderboard filter?origin=native/?origin=added.stats.jsonhasagent_native_countandagent_added_count. - revenue_disclosed (boolean):
trueiff the business has a rankable sourced annualized revenue figure (ARR / MRR×12 / Revenue withvalue_usd— same rule as leaderboard membership /annualized_revenue_usd). Funding or valuation alone isfalse. Leaderboard filter?rev=disclosed/ checkbox Revenue disclosed only; profiles and category cards show a muted no revenue figure pill when false.stats.jsonhasrevenue_disclosed_countandrevenue_undisclosed_count. - older_figure / age_days: on each record and on each leaderboard row,
older_figureistruewhen the headline revenue metric is over 365 days old and not superseded;age_daysis build date minusas_of(month precision = mid-month). Same rule as the gray revenue >12 mo old pill. Leaderboard filter?age=fresh/ checkbox Fresh revenue only keeps only rows with disclosed revenue dated within 365 days. Also on each metric and inleaderboard.csv. - related (array, on each business record): up to 4 similar businesses scored by same category, shared tags, and nearby annualized scale. Each entry has
compare_urlfor the side-by-side page. Profiles show the same list under "Similar on the ledger".
Free: leaderboard CSV + growth-series CSV (chartable points only). Pro (coming soon, join the waitlist): full all-metrics history CSV, denser history and alerts. Paid API keys (Hobby, Builder, Team) are issued by hand by email for now; see pricing.
API keys and rate limits
Keys are optional. No key means the Free plan: 100 requests per day per IP. Paid keys come with API Hobby, Builder and Team on pricing and are issued by email for now.
| Plan | Requests/day (UTC) | Keys | Extras |
|---|---|---|---|
| Free | 100 per IP | none | Static JSON/CSV, search, compare, MCP |
| Hobby ($9/mo) | 2,000 | 1 | Commercial use |
| Builder ($19/mo) | 10,000 | 1 | Full history CSV: /api/v1/export.csv + 5 revenue-move webhooks |
| Team ($79/mo) | 25,000 shared | 5 | Bulk JSONL: /api/v1/export.jsonl + 20 revenue-move webhooks |
Send the key as X-API-Key: mrl_live_… or Authorization: Bearer mrl_live_…. The limit is counted on the function endpoints: /api/v1/search.json, /api/v1/compare.json, the export endpoints and /mcp (only tools/call counts). Static files under /api/v1/ stay free and are not metered.
Every metered response carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (Unix seconds, next UTC midnight) and X-RateLimit-Plan. Over the limit you get HTTP 429 with Retry-After. A wrong or revoked key gets 401. Counts are approximate.
Pay per call (no signup): x402
Agents can pay with x402 in USDC on Base Sepolia (testnet, test USDC only), with no account and no key. The free tier is unchanged: 100 requests/day per IP and every static /api/v1/*.json file stay free.
| What | Price | How |
|---|---|---|
search / compare / MCP tools/call past the free 100/day | $0.005 per call | The 429 becomes a 402 with a PAYMENT-REQUIRED header; retry with PAYMENT-SIGNATURE |
/api/v1/export.csv (full history) | $0.25 per download | Pay instead of a Builder key |
/api/v1/export.jsonl (bulk) | $0.50 per download | Pay instead of a Team key |
| API pass: hobby, 1 day | $0.50 | POST /api/v1/passes?plan=hobby&days=1 or MCP buy_api_pass; returns an expiring key |
| API pass: hobby, 30 days | $9 | POST /api/v1/passes?plan=hobby&days=30 or MCP buy_api_pass; returns an expiring key |
| API pass: builder, 30 days | $19 | POST /api/v1/passes?plan=builder&days=30 or MCP buy_api_pass; returns an expiring key |
| API pass: team, 30 days | $79 | POST /api/v1/passes?plan=team&days=30 or MCP buy_api_pass; returns an expiring key |
| Revenue-move watch (webhook), 30 days | $1 per business (at most $5), $5 for all | POST /api/v1/watch or MCP watch_revenue; included with Builder and Team keys |
MCP: an unpaid call past the quota returns a tool result with isError: true and the PaymentRequired object in structuredContent; retry with params._meta["x402/payment"]. Clients: @x402/fetch (wrapFetchWithPayment) or any x402 v2 client. x402 v1 clients work too: the 402 body is a v1 PaymentRequired (network base-sepolia, maxAmountRequired); pay with X-PAYMENT, receipt in X-PAYMENT-RESPONSE.
Revenue-move alerts (webhooks)
Get a signed POST to your own HTTPS endpoint when a business you watch gets a new or revised revenue figure. It fires after each data refresh, once per change. Webhooks only, no email.
| How you get it | Price | Limit |
|---|---|---|
| x402 (USDC on Base Sepolia), no signup | $1 per business per 30 days (never more than $5); $5 for all businesses | 20 watches per payer, 10 businesses per watch |
| API Builder key | Included | 5 watches |
| API Team key | Included | 20 watches |
Create: POST /api/v1/watch with {"slugs": ["cursor", "lovable"] | "all", "webhook_url": "https://your.app/hooks/mrrledger"} (optional "kinds": metric_revised, metric_added, metric_removed, business_added, metric_redated). Send your Builder/Team key as X-API-Key, or send no key and pay the 402 with PAYMENT-SIGNATURE. MCP: tool watch_revenue. The answer has a watch_id and a secret. The secret is shown once.
Manage: GET or DELETE /api/v1/watch/{watch_id} with Authorization: Bearer <secret> (or the owning key).
Verify: each delivery has MRL-Signature: t=<unix>,v1=<hex>, where hex = HMAC-SHA256(secret, t + "." + raw body). Reject if the signature differs or t is more than 5 minutes old. Answer 2xx quickly. We retry failures with backoff (1 min to 6 h, 6 tries) and pause a watch after 8 failures in a row. Deliveries are at-least-once, so dedupe on the event id (also in MRL-Event-Id).
const h = crypto.createHmac("sha256", secret).update(t + "." + rawBody).digest("hex"); // compare with v1 using timingSafeEqualWebhook URLs must be https on port 443 with a public DNS name: no IP addresses, no localhost, private, link-local, CGNAT or metadata ranges. We check again before every delivery and never follow redirects.