discover-mpprouter
High-level discovery for the MPP Router service catalog. Lets agents find and call paid APIs on Stellar mainnet without hardcoding URLs.
What is MPP Router?
MPP Router is a Stellar-native 402 proxy. It takes USDC payments on stellar:pubnet and forwards your request to upstream merchant APIs — Parallel.ai, Exa, Firecrawl, OpenRouter, and more. You pay once in Stellar USDC and get back whatever the upstream merchant returns. No need to hold USDC on Base/Tempo.
Mainnet only. The router does not run on testnet.
When to trigger
- "Summarize https://stripe.com/docs using parallel.ai search via MPPRouter"
- "Search for X via exa through mpp"
- "Scrape this page with firecrawl — use stellar to pay"
- "List available MPP services"
- "What paid APIs can I call with stellar USDC?"
- User mentions
mpprouterormpp routerby name
Flow
- Discover —
GET https://apiserver.mpprouter.dev/v1/services/catalogreturns the live service list. - Match — pick the service whose
idorcategorybest fits the user's intent. Ask the user if ambiguous. - Read docs — check the matched service's
docsfield:- If
docs.llms_txtexists → fetch it and read the API schema to build the correct request body. The router forwards bodies as-is — it does not transform or validate them. - If only
docs.api_reference→ read that instead. - If only
docs.homepage→ browse it for the API format. - If no docs at all → ask the user for the request body format, or try a minimal probe.
- If
- Invoke —
{method} {base_url}{public_path}with the correct request body. Use themethodfield from the catalog entry — most services are POST, but a few are GET; do not assume. Expect402 Payment Requiredwith a Stellar challenge inWWW-Authenticate: Payment request=.... - Pay — hand off to
pay-per-callwith the 402 challenge; it produces a signed credential. - Retry — re-POST the same body with
Authorization: Payment <credential>. Receive the upstream response +Payment-Receiptheader.
Service catalog
The catalog is live and large (~500 services). Always call
/v1/services/catalog fresh — never hardcode a list, never cache for
more than a minute.
Payment-mode labels (IMPORTANT for the LLM)
The catalog exposes a verified_mode field per service. The discover
skill reads it and tags each record with a payment_mode string:
charge(✓ verified charge) — end-to-end verified to work with this skill's Stellar charge flow. Safe to call. Known-good as of 2026-08-10 (12):anthropic_chat_completions,coingecko_simple_price,deepgram_deepgram_list-models,deepseek_chat,exa_search,firecrawl_scrape,grok_grok_chat,groq_chat,mistral_mistral_chat,parallel_search,perplexity_perplexity_chat,storage_upload(plustavily_tavily_search, first paid-verified 2026-08-10). Every service on this list was verified with a real paid call through the production Router. For the live view, read the catalog'scharge_rozo_verified/charge_rozo_verified_atfields — newly verified services appear there in real time — and the tx-hash audit trail of verification payments is published inrozo-mpprouter/docs/verified-runs.json.session(⚠ session-only) — upstream merchant is session-mode only. The router now correctly advertisesstellar.intents: ["channel"](nocharge) for these services. To use them, the agent must have a registered Stellar channel contract — see thestellar-agent-walletroot SKILL.md "Out of scope" section. Affected:anthropic_messages,openai_chat,openrouter_chat,tempo_rpc. Charge-mode clients cannot call these services. (gemini_generateis currentlypayment_status: unavailable— the upstream merchant's Google API key is invalid; do not offer it.)unverified(· unverified) — the router hasn't labeledverified_mode(or labeled it as the literal string"false", which is a router-side catalog-generator bug). ~97% of the catalog is in this bucket. Most of these work but we can't prove it without a paid probe. Treat as cautiously optimistic: acceptable to call on user request, but warn the user that it's not formally verified.
Decision rule for the LLM
When the user asks to call a paid API via MPP Router:
- Fetch the catalog fresh.
- Find the best service match.
- Check its
payment_mode:charge→ proceed to pay-per-call. This is still a real-money mainnet payment: pay-per-call's own confirmation gate applies (it prompts before every mainnet payment unless the user has set a session--max-autoceiling). "Verified" describes the service record, not permission to spend without the user seeing it.session→ do not call it by default. Tell the user: "This service is currently in session-only mode on MPP Router. Paying via Stellar charge mode would succeed but the upstream would reject the receipt, so you'd lose the fee with no result. Waiting on a router-side fix. Alternative service?" The ONE exception: the user, after reading exactly that warning, explicitly says to proceed anyway and accepts the fee is likely lost — then proceed. There is no other path to calling asessionservice.unverified→ proceed (through the same confirmation gate) but surface a brief caveat in your response ("this service isn't formally verified for Stellar charge mode; proceeding anyway — let me know if the call fails after payment").
Service record shape
Discover enriches each record with a payment_mode field (one of
charge, session, unverified) derived from verified_mode. This
is the field the LLM should branch on — it normalizes the router's
raw verified_mode string (which can be missing or literally
"false") into a fixed vocabulary.
{
"id": "exa_search",
"name": "Exa – Search the web",
"category": "search",
"description": "Search the web",
"public_path": "/v1/services/exa/search",
"method": "POST",
"price": "$0.005/request",
"payment_method": "stellar",
"network": "stellar-mainnet",
"asset": "USDC",
"status": "active",
"methods": { "stellar": { "intents": ["charge", "channel"] } },
"docs": {
"homepage": "https://docs.exa.ai",
"llms_txt": "https://docs.exa.ai/llms.txt"
},
"verified_mode": "charge",
"payment_mode": "charge"
}
docs field — upstream API documentation
The router forwards request bodies as-is without transformation. Clients
must know each upstream API's request format. The docs field provides
the upstream's own documentation:
| Sub-field | What it is | How the agent should use it |
|---|---|---|
llms_txt |
LLM-readable docs (69/88 providers) | Read this first. Fetch it and parse the API schema to construct the correct request body. |
api_reference |
Full API reference (8/88 providers) | Detailed endpoint docs — use when llms_txt is missing or insufficient. |
homepage |
Docs homepage (82/88 providers) | Fallback — browse or scrape if neither of the above is available. |
Decision rule: if docs.llms_txt exists, fetch and read it before
calling the service. If not, check docs.api_reference. If neither
exists, warn the user that you may need them to supply the request body
format, or try docs.homepage.
19 providers have no llms_txt — these include Google Gemini, Google
Maps, Nansen, Parallel, Allium, AgentMail, and several Tempo-proxied
third-party APIs.
How to run
# List all services
./node_modules/.bin/tsx skills/discover/run.ts
# Filter by category
./node_modules/.bin/tsx skills/discover/run.ts --category search
# Match a free-text intent (uses simple keyword match)
./node_modules/.bin/tsx skills/discover/run.ts --query "web search"
# JSON output for piping
./node_modules/.bin/tsx skills/discover/run.ts --json
Full end-to-end example
# Discover which service to use — capture path, method, and the
# catalog-advertised payment expectations so pay-per-call can refuse
# a 402 that tries to redirect funds or inflate the price.
SERVICE_JSON=$(./node_modules/.bin/tsx skills/discover/run.ts --query "web search" --pick-one --json)
SERVICE=$(echo "$SERVICE_JSON" | jq -r '.public_path')
METHOD=$(echo "$SERVICE_JSON" | jq -r '.method')
EXPECT_AMT=$(echo "$SERVICE_JSON" | jq -r '.expect.amount_usdc // empty')
EXPECT_TO=$(echo "$SERVICE_JSON" | jq -r '.expect.pay_to // empty')
# Call it — pay-per-call handles the 402 → pay → retry loop.
# The --expect-* flags cross-check the 402 challenge against the
# catalog. Any drift aborts before signing.
./node_modules/.bin/tsx skills/pay-per-call/run.ts "https://apiserver.mpprouter.dev$SERVICE" \
--method "$METHOD" \
--body '{"query": "Summarize https://stripe.com/docs"}' \
${EXPECT_AMT:+--expect-amount "$EXPECT_AMT"} \
${EXPECT_TO:+--expect-pay-to "$EXPECT_TO"}
Anti-patterns
- ❌ Don't hardcode service paths — the catalog is the source of truth.
- ❌ Don't pay twice for one call — credentials are single-use and HMAC-bound to amount/currency/recipient.
- ❌ Don't use testnet — MPP Router is mainnet-only.
- ❌ Don't cache the catalog for more than a minute — prices and service availability can change.
