# AffiliateOS — Agent Skill

> Give this file to any AI agent (Claude Code, a custom MCP client, anything that reads
> markdown) and it will know how to drive AffiliateOS end to end: discover affiliate
> programs, mint tracked links, and read earnings back per campaign.

**What AffiliateOS is:** unified rails over the affiliate network accounts a publisher
already owns — Awin, Impact, CJ, Rakuten, and Amazon. It is BYOC (bring your own
credentials): commissions are paid by the networks directly to the user. AffiliateOS
never touches the money; it normalizes discovery, link minting, and earnings into one
surface an agent can operate.

**Companion file:** [HEARTBEAT.md](./HEARTBEAT.md) — a periodic monitoring routine
(new commissions, reversals, sync health) to run on a schedule.

---

## Connecting

Preferred: the MCP server (streamable HTTP, 16 tools).

```
claude mcp add --transport http affiliateos https://api.affiliateos.dev/mcp
```

Self-hosted deployments substitute their own API origin (`API_URL` env; local dev is
`http://localhost:8787`). Everything below uses `{API_URL}` for that origin.

### Authentication

Two Bearer credentials work everywhere (MCP and REST):

| Method | How |
|---|---|
| OAuth 2.1 (default for MCP) | An unauthenticated MCP request returns 401 with protected-resource metadata; clients like Claude complete an authorization-code + PKCE flow automatically. Tokens last 1 hour and refresh. |
| API key | `Authorization: Bearer aff_live_...` — issued by the human in the dashboard under Developers. Use for clients without an OAuth flow. |

Agents can never submit network credentials (Awin API tokens etc.). If a network is
not connected, call `connect_network_instructions` and hand the returned dashboard URL
to the human.

Rate limit: 120 requests/minute per key or user. On 429, honor `Retry-After`.

---

## The core loop

Every profitable session follows the same shape:

1. **Orient** — `list_connected_networks` to see which accounts exist and are healthy.
2. **Discover** — `search_programs` (topic → programs) or `resolve_url` (URL → programs).
3. **Mint** — `create_link` with an `attribution_tag` naming the campaign/channel/variant.
4. **Publish** — the human (or your own pipeline) places the `tracking_url`.
5. **Close the loop** — `get_earnings_summary { group_by: "tag" }` to see what each tag
   earned; reallocate effort toward what converts.

Attribution tags are the unit of learning. Use one tag per experiment
(`tiktok-v3`, `blog-roundup-2026-07`), not one tag for everything. Tags are slugs:
lowercase letters, digits, `-`, `_`. `create_link` auto-creates unknown tags.

---

## MCP tools (16)

### Discovery (7)

| Tool | Use when | Key input → output |
|---|---|---|
| `list_connected_networks` | First call of any session | `{}` → accounts with status + last sync |
| `search_programs` | You have a topic/niche | `{query, joined_only?, network?, vertical?, min_commission_pct?, limit?}` → programs with `program_id`, commission summary |
| `get_program` | Need cookie window, deep-link support, full detail | `{program_id, fields?}` → full program record |
| `resolve_url` | You have a product/merchant URL | `{url}` → programs whose domains cover it, ranked by commission |
| `search_products` | Need specific products with prices (Amazon in v1) | `{query, network?, max_price?, limit?}` → products with link-ready URLs |
| `get_sync_status` | Data looks stale | `{}` → recent sync runs per account with errors |
| `connect_network_instructions` | A network isn't connected | `{network}` → credential field spec + dashboard connect URL for the human |

Notes:
- `search_programs` defaults to `joined_only: true` (programs the user is approved
  for). Empty results? Retry with `joined_only: false` to find programs worth applying to.
- `resolve_url` answers "can this URL be monetized?" — always prefer it over
  `search_programs` when you already hold a URL.

### Attribution & links (4)

| Tool | Use when | Key input → output |
|---|---|---|
| `create_link` | THE tool: turn a URL into a paid link | `{destination_url, attribution_tag, program_id?, account_id?}` → `tracking_url`, `subid` |
| `bulk_create_links` | Up to 50 URLs, one tag (roundups, storefronts) | `{urls[], attribution_tag}` → per-URL success/error |
| `create_attribution_tag` | Pre-create a tag with metadata (usually unnecessary) | `{tag, description?, metadata?}` → tag (idempotent) |
| `list_attribution_tags` | Find existing campaigns | `{query?}` → tags |

Notes:
- Omit `program_id` and `create_link` resolves the right account/program by domain.
- On `MONETIZATION_NOT_FOUND`, fall back to `resolve_url`, then
  `search_programs { joined_only: false }` to find a program the human can join.

### Earnings & events (5)

| Tool | Use when | Key input → output |
|---|---|---|
| `get_earnings_summary` | The loop-closer: aggregates with currency conversion | `{group_by: "tag"\|"network"\|"program"\|"day", from?, to?, currency?}` → pending/approved/reversed/paid per group |
| `get_transactions` | Inspect individual conversions | `{attribution_tag?, status?, network?, from?, to?, cursor?}` → transactions + `next_cursor` |
| `get_payments` | Reconcile actual payouts | `{from?, to?}` → payout records |
| `check_events` | Pull-based trigger feed (poll in-session) | `{after?, types?, limit?}` → events + `high_water_mark` |
| `subscribe_webhook` | Push delivery to your own endpoint | `{url, event_types[]}` → endpoint + signing secret (shown once) |

Event types: `commission.created`, `commission.approved`, `commission.reversed`,
`commission.paid`, `program.approved`, `program.terminated`, `sync.auth_error`.

Commission lifecycle: **pending → approved → paid**, with **reversed** possible
(returns, fraud). Only treat `approved`/`paid` as real money; `pending` is a signal,
not income.

---

## Response conventions

- **Compact by default** — responses stay under ~2,000 tokens; list tools paginate via
  `next_cursor`; detail tools accept `fields` to slim payloads.
- **Structured errors** — failures return `{error, message, hint}`; the hint names the
  next tool to try. Follow it.
- **Plan limits** — write operations can fail with `PLAN_LIMIT` (HTTP 402 on REST) when
  the user's plan cap is reached (network accounts, links/month, webhook endpoints).
  Surface it to the human with the dashboard billing page (`/billing`); do not retry.

---

## REST API (fallback / non-MCP clients)

Same auth, base `{API_URL}`. Full reference: https://affiliateos.dev/docs/api

```
GET  /v1/accounts                 connected network accounts
GET  /v1/programs                 program search (query params mirror search_programs)
POST /v1/links                    { destination_url, attribution_tag } → tracking_url
POST /v1/links/bulk               { urls[], attribution_tag }
GET  /v1/transactions             conversions with filters
GET  /v1/earnings/summary         ?group_by=tag&currency=USD
GET  /v1/payments                 payouts
GET  /v1/events                   pull feed (same semantics as check_events)
POST /v1/webhooks                 { url, event_types[] } → signed deliveries
GET  /v1/billing                  plan, limits, and current usage (check before bulk minting)
```

Webhook deliveries are signed with HMAC-SHA256 in `X-AffiliateOS-Signature`;
verify against the secret returned at subscribe time.

---

## Network quirks worth knowing

- **Amazon**: product search and links work; there is **no earnings API**. Do not
  expect Amazon rows in `get_earnings_summary`.
- **Sync cadence**: programs every 12h, transactions hourly, payments daily. If numbers
  look stale, check `get_sync_status` before assuming a bug.
- **Compliance**: affiliate links require disclosure wherever they are published. When
  generating content around a tracking URL, include a plain-language affiliate
  disclosure. See https://affiliateos.dev/docs/disclosure
