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. POST only.
  • 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:

JSON
{
  "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.

PermissionGrantsTools
readAnalytics, every list, diagnostics, setup planning and verification18
writeCreate and update affiliates and links, bulk operations, app settings7
setupCreate apps, rotate SDK keys and webhook secrets2
payoutrun_payout — closes out commissions and exports the payout CSV1

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.

#RungA failure means
1SDK key has been usedThe SDK never authenticated. Wrong key, or start() never runs.
2At least one link existsNo affiliate or link created yet.
3Clicks are being recordedNobody has opened a link, or the link service isn't pointed here.
4Installs are being attributedattribute() / applyCode() isn't called on first launch, or deep links aren't verified.
5Users are being identifiedidentify(userId) is never called.
6Billing webhooks are arrivingThe provider's webhook isn't pointed at us.
7Webhook signatures verifyThe signing secret in the provider's dashboard doesn't match ours.
8Revenue events are being recordedDeliveries arrive but carry no purchase/renewal, or fail to normalize.
9Billing and attribution agree on the user idThe usual culprit — see below.
10Commissions are being createdRate 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.

Code
 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:

ProviderField that must match identify(userId)
RevenueCatthe app_user_id you pass to logIn()
Adaptycustomer_user_id
Superwallthe app user id
Stripemetadata.customer_user_id on the subscription and the session
Paddlecustom_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

ToolDoes
list_appsApps in the account. Start here.
get_appCommission rate, attribution window, hold period, link subdomain.
get_app_overviewLifetime funnel, revenue, commission totals by status, per-affiliate totals.
get_affiliate_overviewOne creator's funnel, earnings, links, next payout date.
get_timeseriesTime-bucketed metrics: clicks, installs, identified, paid, revenue_usd, commission_usd.
list_affiliatesCreators with clicks, installs and lifetime commission.
list_linksBranded links with click counts.
list_commissionsIndividual commissions, filterable by affiliate, status and date.
list_payoutsPayout history; itemCount reconciles to the CSV.
get_payout_previewWhat a payout would cover. Writes nothing.
list_audit_logEvery write, with the key that made it.

Diagnosing

ToolDoes
setup_verifyThe ten-rung ladder above.
diagnose_identityDo billing and attribution agree on user ids?
list_unattributed_eventsPaid events that earned no commission, with a cause each.
list_webhook_deliveriesRecent deliveries: signature validity, processing status, errors. Never the raw body.

Setting up

ToolPermissionDoes
setup_planreadThe ordered steps for a stack.
setup_get_snippetreadPaste-ready code for one step.
setup_get_provider_configreadWebhook URL, location, required events.
setup_create_appsetupProvision an app and its secrets.
rotate_app_secretsetupNew SDK key or webhook secret; the old one dies immediately.

Changing things

ToolDoes
create_affiliate · create_affiliates_bulkAdd creators. Bulk takes up to 100.
update_affiliateName, email, rate, status (active / paused / removed).
create_link · create_links_bulkMint links. Bulk takes up to 100.
update_linkRetarget, rename campaign, or retire with active: false.
update_app_settingsCommission rate, attribution window, hold period.
run_payoutpayout 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.

CommandDoes
/onboard-appPlan, provision, write the code, verify.
/recruit-creatorsA list in, creators and links out, share-ready table back.
/monthly-payout-runPreview, stop for your approval, record, reconcile.
/creator-performance-reviewRank creators on what they drive, with a recommendation each.
/debug-attributionDiagnose an integration recording zero.
/fraud-sweepFlag 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:

Code
get_payout_preview  → Mia: 12 commissions, $340.00, token abc123…
                      (you check the number and agree)
run_payout          → with token abc123…   ✓ recorded, CSV returned

If 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 nextCursor as cursor; stop when it comes back null.
  • list_affiliates with sort=earnings returns the top limit creators and does not paginate — ranking by an aggregate has no stable key to resume from. Use sort=created or sort=name to walk the whole list.
  • get_timeseries is capped at 90 buckets per call.
  • GET and DELETE on /mcp return 405. The server is stateless; the deprecated HTTP+SSE transport is not implemented.

Troubleshooting

SymptomCause
401 on connectKey is wrong, revoked, or an SDK key (pk_live_…) rather than an account key (maa_sk_live_…).
403 on a toolThe 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 missingSame cause: it needs a permission your key lacks. Check tools/list.
404 on an appIdThe app belongs to another account, or does not exist. These are deliberately indistinguishable.
Everything green but no commissionsRung 9. Run diagnose_identity.

SDK keys authenticate /sdk/* only and are rejected everywhere else; an account key is likewise rejected on /sdk/*.