# AffiliateOS — Heartbeat

> A periodic monitoring routine for an agent that watches a publisher's affiliate
> business. Run it on a schedule (hourly to daily). Requires the tools described in
> [SKILL.md](./SKILL.md). Designed to be cheap: one `check_events` call decides whether
> anything else runs.

## State to persist between runs

Keep a small state file (JSON) wherever your agent persists things:

```json
{
  "high_water_mark": "<event id from the last run, or null on first run>",
  "last_summary": { "pending": 0, "approved": 0, "reversed": 0, "paid": 0 },
  "last_run_at": "<ISO timestamp>"
}
```

## The routine

### 1. Pull new events (always)

```
check_events { after: <high_water_mark>, limit: 100 }
```

Save the returned `high_water_mark`. If `events` is empty, report "no change" and stop
— do not run the rest.

### 2. Triage by type

| Event | Action |
|---|---|
| `commission.approved`, `commission.paid` | Good news. Accumulate for the report. |
| `commission.created` | Pending signal — note which attribution tag is converting. |
| `commission.reversed` | Investigate: `get_transactions` filtered to the tag/program. One reversal is noise; a cluster on one program is a pattern worth flagging. |
| `sync.auth_error` | **Alert the human immediately** — a network connection is broken and data is silently going stale. Include the reconnect URL from `connect_network_instructions`. |
| `program.terminated` | Alert: existing links for that program may stop paying. List affected links if asked. |
| `program.approved` | Opportunity: a pending application went through; suggest minting links. |

### 3. Compare earnings (when commission events arrived)

```
get_earnings_summary { group_by: "tag" }
```

Diff against `last_summary`. Interesting deltas:

- A tag whose approved total moved > 20% since last run.
- A tag with rising `reversed` — quality problem at the merchant or the traffic.
- A tag with many `pending` transactions but nothing approving over weeks — check the
  program's approval lag before assuming the worst.

### 4. Spot-check sync health (daily, not every run)

```
get_sync_status {}
```

Any run with `status: "error"` repeating across checks → alert with the error text.

### 5. Report

One short message to the human, only when there is something to say:

```
♥ affiliateos heartbeat — 3 new approvals ($112.40), 1 reversal
  • tiktok-v3 approved +$98.20 (now $511.06) — best performer this week
  • blog-roundup reversed −$14.80 (RunFast Gear; 2nd reversal this month)
  • all syncs healthy · next check in 6h
```

Silence is a valid report: if step 1 returned nothing, say nothing (or log "no change"
if your runner expects output).

## Cadence guidance

- **Hourly** fits active campaign launches (transactions sync hourly upstream, so
  faster polling sees nothing new).
- **Daily** fits steady-state publishing.
- For push instead of polling, `subscribe_webhook` with
  `["commission.approved", "commission.reversed", "sync.auth_error"]` and skip step 1.
