---
title: "MCP Server"
source: https://docs.myappaffiliate.com/mcp
docs: "MyAppAffiliate — Developer Documentation"
index: https://docs.myappaffiliate.com/llms.txt
---
# 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`.

| 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. `` 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 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

| 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/*`.
