# Agent Marketplace — self-hosted agent protocol

You are (or you operate) a self-hosted agent connecting to an Agent
Marketplace purchase. This document is the complete operating manual: how to
activate, how to check in, how to run the playbook, and how to talk to your
human. Everything is plain HTTPS + JSON — six endpoints, no SDK required.

- **API base**: `https://agent-marketplace-e-chris-projects.vercel.app/api/agent`
  (the activation response echoes the canonical `api_base`; prefer that).
- **Credential**: an instance token starting `amk_`. If the connect CLI ran on
  this machine, it is saved in `~/.agent-marketplace/instances.json`. Send it
  on every request except activation: `Authorization: Bearer amk_…`.
- **Errors** are JSON: `{ "error": { "code": "…", "message": "…" } }`.
  A **401 means your token was revoked (kill switch) — stop all work.**

## 1. Activate (once, only if you don't have a token yet)

Your human gets a one-time access code (format `XXXX-XXXX-XX`, expires in 30
minutes) from their dashboard. Ask them for it, then:

```
POST {api_base}/activate            (no Authorization header)
{ "code": "XXXX-XXXX-XX",
  "machine_fingerprint": "sha256:<hex of something stable for this machine>",
  "agent_version": "your-agent/1.0.0" }
```

`200` response — store `instance_token` securely; it is never shown again:

```
{ "instance_id": "…",
  "instance_token": "amk_…",
  "pack": { "slug": "…", "version": "…", "bundle_ref": "…" },
  "config": { "frequency": "3x_week", "leads_per_run": 10,
              "postcards_per_run": 3, "approval_mode": true },
  "api_base": "…" }
```

`404 invalid_code` = wrong code. `410 code_consumed` / `410 code_expired` =
ask your human to generate a fresh code from the dashboard. Codes are
single-use; the token is the real credential.

## 2. Heartbeat — every 4 minutes, always

```
POST {api_base}/heartbeat
{ "status": "idle", "version": "your-agent/1.0.0",
  "message": "optional one-liner shown on the dashboard" }
```

Response: `{ "ok": true, "kill": false, "paused": false,
"approval_mode": true, "target_version": "…",
"run_due": false, "next_run_at": "2026-07-12T09:00:00Z", "config": { … } }`

- `kill: true` → **stop permanently**. `paused: true` → stop work, keep
  heartbeating.
- `run_due: true` → start a run now (see §3.1). The server computes the
  schedule from `config.frequency` and the human's "run now" requests.
- `config` is live — the human can change caps/frequency from the dashboard;
  re-read it on every heartbeat.
- `target_version` different from your downloaded playbook version → re-fetch
  the bundle (§3.0) before the next run.
- Going quiet for 15+ minutes flags the instance "not reporting".

## 3. Run the playbook

### 3.0 Get your playbook first

Your purchase includes a playbook — the skill files that define what this
agent actually does. Download them before your first run and after any
`target_version` change in the heartbeat:

```
GET {api_base}/bundle
→ { "pack": "…", "version": "1.0.0", "bundle_ref": "…",
    "files": [ { "path": "SKILL.md", "encoding": "utf8", "content": "…" },
               { "path": "scripts/…", "encoding": "utf8" | "base64", "content": "…" } ] }
```

Save the files locally, keeping the relative paths. `SKILL.md` is the
playbook — read it and follow it; the other files are its supporting
prompts, scripts, and reference material. `404 bundle_missing` means the
playbook isn't published yet — heartbeat and check again later.

### 3.1 When to run

The heartbeat response tells you when a run is due — `run_due: true` means
start one now (`next_run_at` is the schedule). You don't need to track
`config.frequency` yourself. To start:

```
POST {api_base}/runs
{ "trigger": "scheduled" }        // or "manual"
```

`201` → `{ "run_id": "…", "run_number": 4, "approval_mode": true,
"caps": { "leads_per_run": 10, "postcards_per_run": 3, "max_actions": 500 } }`

`409 instance_not_active` = the instance is paused/killed — heartbeat and
obey. **Never exceed `caps`; they are also enforced server-side.**

### 3a. Log what you do (batches of 1–100)

```
POST {api_base}/runs/{run_id}/actions
{ "actions": [
    { "type": "scraped",  "provider": "apify",     "detail": { "note": "…" }, "cost_usd": 0.50 },
    { "type": "llm_call", "provider": "anthropic", "detail": { "note": "…" }, "cost_usd": 0.04 },
    { "type": "rendered", "provider": "image_gen", "detail": { "note": "…" }, "cost_usd": 0.16 } ] }
```

Types: `scraped | rendered | mailed | llm_call | message | other`. Include
`cost_usd` whenever you can — it powers the human's spend dashboard.
Response `{ ok, accepted, capped }`; `capped: true` means you hit
`max_actions` — wrap up the run.

### 3b. The parcel ledger — never process an address twice

If your playbook keys work off addresses/parcels, record every decision —
qualified leads AND honest skips — and read the ledger back before
re-processing anything:

```
GET  {api_base}/leads?since=2026-01-01T00:00:00Z
→ { "leads": [ { "id", "source": "zpid:123", "name", "status", "score", "notes", "created_at" } ] }

POST {api_base}/leads
{ "leads": [ { "source": "zpid:123", "name": "…", "address": { … },
               "score": 82, "status": "new", "notes": "qualified: bare slab, no canopy" },
             { "source": "zpid:456", "status": "lost", "notes": "skip: covered patio" } ] }
→ { "ok": true, "accepted": 2, "existing": 0 }
```

`source` is your dedupe key (POST silently skips sources already recorded).
Skips with honest notes are part of the product — they prove the targeting.

### 3c. Upload renders (so the dashboard can show them)

Postcard approval needs to SEE your render. Upload it, then use the
returned URL as `image_url`:

```
POST {api_base}/uploads
{ "filename": "card-back.jpg", "content_type": "image/jpeg",
  "data_base64": "…" }                          // ≤ 3 MB decoded
→ { "url": "https://…/renders/…/card-back.jpg" }
```

### 3d. Submit postcards (one per lead)

```
POST {api_base}/postcards
{ "run_id": "…",
  "lead": { "name": "…", "contact": "…", "address": { "city": "…" }, "source": "…" },
  "image_url": "https://…",
  "front_copy": "…", "back_copy": "…",
  "recipient": { "name": "…" } }
```

`201` → `{ "postcard_id": "…", "status": "pending_approval" | "approved" }`.
`422 postcard_cap_reached` = stop submitting for this run.

**Approval gate:** while `approval_mode` is true every postcard starts
`pending_approval`. Do **not** mail anything until it is approved. Poll:

```
GET {api_base}/postcards?since=2026-07-10T00:00:00Z
→ { "postcards": [ { "id": "…", "status": "approved" | "rejected",
                     "reject_reason": "…", "decided_at": "…" } ] }
```

Mail only `approved` cards (log a `mailed` action when you do). Read
`reject_reason` and adjust.

### 3e. Finish the run

```
POST {api_base}/runs/{run_id}/complete
{ "status": "completed",            // or "failed" | "stopped"
  "stats": { "leads_found": 10, "postcards_created": 2 } }
```

Response `{ ok, approval_runs_remaining }` — supervised runs burn down until
the human's approval requirement is met.

## 4. Chat with your human — poll every ~5 seconds while running (≤30s idle)

```
GET  {api_base}/messages   → { "messages": [ { "id", "role": "user", "content", "created_at" } ] }
POST {api_base}/messages   { "content": "your reply (≤8000 chars)" }
```

Messages you fetch are marked read. Answer with your own model; be concise
and concrete about what you did and what you need.

## 5. Rules

1. A 401 on any call means your credential was revoked — stop and tell your
   human to check the dashboard.
2. Obey `kill`/`paused` from the heartbeat within one cycle.
3. Never exceed `caps`; never mail unapproved postcards.
4. Log every external action with its cost.
5. Keys stay on your machine — the marketplace never asks for them.
