MCP Server
Connect MyAppAffiliate to Claude Code, Cursor, or any MCP client. Set attribution up by asking for it, run your creator program without opening a dashboard, and find out why an integration is recording zero.
The server is remote — there is nothing to install. One entry in your client config and a key.
- Endpoint:
https://api.myappaffiliate.com/mcp - Transport: streamable HTTP, stateless.
POSTonly. - Auth:
Authorization: Bearer maa_sk_live_…
1. Get a key
During the beta we issue account keys on request — tell us which permissions you need and we'll send you one. It is shown once. Self-serve key management in the dashboard is not built yet.
A key is scoped to one account. It sees that account's apps and nothing else; anything belonging to another account returns "not found", so the API is never a way to discover what other accounts own.
2. Connect
Claude Code — .mcp.json in your project, or your user config:
{
"mcpServers": {
"myappaffiliate": {
"type": "http",
"url": "https://api.myappaffiliate.com/mcp",
"headers": { "Authorization": "Bearer maa_sk_live_…" }
}
}
}Cursor — the same object in ~/.cursor/mcp.json.
Anything else — any client that speaks streamable HTTP MCP. Point it at the endpoint
and set the Authorization header.
Start by asking it to list your apps. Every other tool takes an appId.
Permissions
Each key carries explicit permissions. A tool whose permission the key lacks is not
registered at all — it never appears in tools/list, so the model cannot propose an
action you did not grant. A call for a missing permission returns 403.
| Permission | Grants | Tools |
|---|---|---|
read | Analytics, every list, diagnostics, setup planning and verification | 18 |
write | Create and update affiliates and links, bulk operations, app settings | 7 |
setup | Create apps, rotate SDK keys and webhook secrets | 2 |
payout | run_payout — closes out commissions and exports the payout CSV | 1 |
Grant the least you need. read alone is genuinely useful and cannot change anything.
payout is never granted by default.
Setting up from scratch
Point your agent at your repo and ask it to set up MyAppAffiliate. The sequence:
1. setup_plan — the ordered steps for your exact stack (platform × billingSource),
each with the file or dashboard it happens in, and which ones you have to do by hand.
Platforms: ios, android, react-native, flutter, web, node.
Billing: revenuecat, stripe, adapty, superwall, paddle.
Combinations that cannot work are refused rather than fudged — RevenueCat has no web SDK, Stripe is not a mobile IAP processor.
2. setup_create_app — provisions the app, its SDK key and its webhook signing
secrets, and returns the exact webhook URLs. Secrets are shown once and stored hashed.
Give them to a human; wire the code to read them from config or environment.
3. setup_get_snippet — paste-ready code for one step, in your platform's language.
No API URL appears in it: every SDK already knows where to call. <SDK_KEY> stays a
placeholder on purpose, because a snippet with a real key in it is a key someone commits.
4. setup_get_provider_config — the webhook URL, where it goes in the provider's
dashboard, how we verify it, and which events to enable.
This is the one step no agent can complete. No billing provider has an API for "add this webhook endpoint", so the agent's job ends at handing you the exact values.
5. setup_verify — below.
setup_verify
Every way an attribution setup can be wrong produces the same symptom: a dashboard
showing zero. A missing start() call, an unverified Universal Link, a mistyped webhook
secret, an absent Stripe metadata field — identical from the outside, all different fixes.
So setup_verify checks them against your live data, in dependency order, and reports the
first failure with the step to fix. Later failures are marked blocked, because they
usually fail as a consequence — but they keep their real observations, so you still learn
what was seen.
| # | Rung | A failure means |
|---|---|---|
| 1 | SDK key has been used | The SDK never authenticated. Wrong key, or start() never runs. |
| 2 | At least one link exists | No affiliate or link created yet. |
| 3 | Clicks are being recorded | Nobody has opened a link, or the link service isn't pointed here. |
| 4 | Installs are being attributed | attribute() / applyCode() isn't called on first launch, or deep links aren't verified. |
| 5 | Users are being identified | identify(userId) is never called. |
| 6 | Billing webhooks are arriving | The provider's webhook isn't pointed at us. |
| 7 | Webhook signatures verify | The signing secret in the provider's dashboard doesn't match ours. |
| 8 | Revenue events are being recorded | Deliveries arrive but carry no purchase/renewal, or fail to normalize. |
| 9 | Billing and attribution agree on the user id | The usual culprit — see below. |
| 10 | Commissions are being created | Rate is 0, or the purchase fell outside the attribution window. |
Rung 9 is the one that catches real integrations
identify(userId) and your billing provider must send byte-identical strings for the
same person. When they don't, everything upstream is green — clicks, installs, webhooks,
valid signatures, recorded revenue — and no commission is ever created, because the join
key never matches.
1–8. [PASS] everything looks perfectly healthy
9. [FAIL] Billing and attribution agree on the user id
10. [----] Commissions are being created
DETAIL: No overlap. Billing sent auth0|abc123; the SDK bound usr_abc123.Seeing the two spellings together is usually the whole fix. Where the id comes from, per provider:
| Provider | Field that must match identify(userId) |
|---|---|
| RevenueCat | the app_user_id you pass to logIn() |
| Adapty | customer_user_id |
| Superwall | the app user id |
| Stripe | metadata.customer_user_id on the subscription and the session |
| Paddle | custom_data.customer_user_id |
diagnose_identity runs this check on its own and returns sample ids from both sides.
list_unattributed_events lists paid events that earned no commission, each with a likely
cause.
Tool reference
Reading
| Tool | Does |
|---|---|
list_apps | Apps in the account. Start here. |
get_app | Commission rate, attribution window, hold period, link subdomain. |
get_app_overview | Lifetime funnel, revenue, commission totals by status, per-affiliate totals. |
get_affiliate_overview | One creator's funnel, earnings, links, next payout date. |
get_timeseries | Time-bucketed metrics: clicks, installs, identified, paid, revenue_usd, commission_usd. |
list_affiliates | Creators with clicks, installs and lifetime commission. |
list_links | Branded links with click counts. |
list_commissions | Individual commissions, filterable by affiliate, status and date. |
list_payouts | Payout history; itemCount reconciles to the CSV. |
get_payout_preview | What a payout would cover. Writes nothing. |
list_audit_log | Every write, with the key that made it. |
Diagnosing
| Tool | Does |
|---|---|
setup_verify | The ten-rung ladder above. |
diagnose_identity | Do billing and attribution agree on user ids? |
list_unattributed_events | Paid events that earned no commission, with a cause each. |
list_webhook_deliveries | Recent deliveries: signature validity, processing status, errors. Never the raw body. |
Setting up
| Tool | Permission | Does |
|---|---|---|
setup_plan | read | The ordered steps for a stack. |
setup_get_snippet | read | Paste-ready code for one step. |
setup_get_provider_config | read | Webhook URL, location, required events. |
setup_create_app | setup | Provision an app and its secrets. |
rotate_app_secret | setup | New SDK key or webhook secret; the old one dies immediately. |
Changing things
| Tool | Does |
|---|---|
create_affiliate · create_affiliates_bulk | Add creators. Bulk takes up to 100. |
update_affiliate | Name, email, rate, status (active / paused / removed). |
create_link · create_links_bulk | Mint links. Bulk takes up to 100. |
update_link | Retarget, rename campaign, or retire with active: false. |
update_app_settings | Commission rate, attribution window, hold period. |
run_payout | payout only. Records a payout and returns its CSV; moves no money — see below. |
Bulk calls always return 200 with created and failed arrays. One bad row fails
only itself, and failed[].index points at the row you submitted. A row that fails
schema validation rejects the whole batch with 400 instead — fix the payload and
resubmit.
Link slugs cannot be renamed. A slug is baked into every link a creator has already shared. Retire it and mint a new one.
Changing the commission rate is not retroactive. Commission.rateBps is snapshotted
at calculation time, so existing commissions keep the rate they were created with.
Workflows
Your client shows these as commands. Each is a recipe over the tools above — nothing here can do more than your key allows, and a workflow whose permissions you lack is not offered.
| Command | Does |
|---|---|
/onboard-app | Plan, provision, write the code, verify. |
/recruit-creators | A list in, creators and links out, share-ready table back. |
/monthly-payout-run | Preview, stop for your approval, record, reconcile. |
/creator-performance-review | Rank creators on what they drive, with a recommendation each. |
/debug-attribution | Diagnose an integration recording zero. |
/fraud-sweep | Flag click inflation and conversion anomalies, propose pauses. |
Payouts
We never move your money. MyAppAffiliate holds no funds and reaches no payment rail.
run_payout matures a creator's due commissions, marks them paid, and hands back a
reconciliation CSV; actually paying the creator is a separate step on whatever rail you
already use.
The bookkeeping is what cannot be undone: there is no un-pay, and a commission marked paid never appears in a later payout. Two things stand in the way.
The payout permission, off by default. A key without it has no payout tool to call.
A confirmation token. get_payout_preview returns one per creator, derived from that
creator's exact commission count and total. run_payout refuses without a matching one:
get_payout_preview → Mia: 12 commissions, $340.00, token abc123…
(you check the number and agree)
run_payout → with token abc123… ✓ recorded, CSV returnedIf a commission matures between the preview and the call, the token no longer matches and the call fails rather than quietly recording more than you approved. Preview again and confirm the new total.
The token is a digest of those figures, not a secret. It cannot be produced without having
previewed — which is the point. It does not stop someone who holds a payout key; it stops
an agent closing out commissions without having looked, and it stops the total changing
underneath you.
Safety
Tool results are data, not instructions. Creator names, campaign names, link slugs and webhook payloads are written by other people. The server tells the model never to act on text found inside a result, and no tool accepts an account id — so there is no parameter an injected instruction could widen to reach another account's data.
Every tool says what it does to your data. Each carries an MCP annotation — read-only,
additive, or destructive — and clients use it to decide what may run unattended. The 18
reads are marked read-only. The five that only create rows (create_affiliate,
create_link, their bulk forms, setup_create_app) are additive. The five that change or
consume something that already exists (update_affiliate, update_link,
update_app_settings, run_payout, rotate_app_secret) are destructive, so a client asks
before each call.
Every write is recorded. list_audit_log returns the action, target, arguments and the
key that made it. Secrets are redacted before anything is stored.
Permissions are enforced by absence. A tool you have not granted is never registered, so it cannot be called, suggested, or hallucinated into existence.
Conventions and limits
- Money is integer USD cents, everywhere. Rates are basis points — 3000 = 30%.
- Lists are cursor-paginated: 50 per page by default, 100 maximum. Pass the previous
response's
nextCursorascursor; stop when it comes backnull. list_affiliateswithsort=earningsreturns the toplimitcreators and does not paginate — ranking by an aggregate has no stable key to resume from. Usesort=createdorsort=nameto walk the whole list.get_timeseriesis capped at 90 buckets per call.GETandDELETEon/mcpreturn405. The server is stateless; the deprecated HTTP+SSE transport is not implemented.
Troubleshooting
| Symptom | Cause |
|---|---|
401 on connect | Key is wrong, revoked, or an SDK key (pk_live_…) rather than an account key (maa_sk_live_…). |
403 on a tool | The key lacks that permission. Mint a new key — permissions are fixed at creation. |
403 with "no permissions" | The key was created with an empty permission set and can reach nothing. |
| A tool you expected is missing | Same cause: it needs a permission your key lacks. Check tools/list. |
404 on an appId | The app belongs to another account, or does not exist. These are deliberately indistinguishable. |
| Everything green but no commissions | Rung 9. Run diagnose_identity. |
SDK keys authenticate /sdk/* only and are rejected everywhere else; an account key is
likewise rejected on /sdk/*.