# Stitch3 brand reports: agent guide

Generate outreach images and suggested X DMs for any X handle, based on public X activity
that Stitch3 collects. Everything is plain HTTP and JSON; no browser is needed. This page is
complete on its own: you need nothing else except an access token.

- Base URL: `https://outreach.stitch3.ai`
- Auth: send `Authorization: Bearer <token>` on every `/api/*` call. Stitch3 provides your token; keep it secret.

## The whole workflow

The examples use `curl` and `jq`.

```bash
BASE=https://outreach.stitch3.ai
TOKEN=...   # brand-report access token
H="Authorization: Bearer $TOKEN"

# 1. Check for an existing report. Reuse it if generated_at is today (UTC) and status is not "error".
curl -s $BASE/api/report/kalshi -H "$H" | jq '{status, generated_at}'
#    HTTP 404 means no report exists yet: go to step 2.

# 2. Otherwise start one (one report at a time; each uncached handle may take several minutes)
JOB=$(curl -s -X POST $BASE/api/generate -H "$H" \
  -H 'Content-Type: application/json' -d '{"handle":"kalshi"}' | jq -r .job_id)

#    Poll every 5 seconds until status is "done" or "error". Give up after 10 minutes.
curl -s $BASE/api/job/$JOB -H "$H" | jq -r .status    # queued -> running -> done | error

# 3. Read the result: images, blocking reasons and suggested messages in one response
curl -s $BASE/api/report/kalshi -H "$H" > kalshi.json

# 4. Download a ready image under its intended filename
jq -r '.variants[] | "\(.png_url) \(.download_name)"' kalshi.json |
  while read url name; do curl -s "$BASE$url" -o "$name"; done
```

A job ending in `error` publishes no images; read `/api/report/<handle>` for the reasons.
If polling returns 404 (`unknown job`), the server restarted; call `GET /api/report/<handle>`, as the report may already be saved.

## Codes

Use these codes to refer to images and messages. They never change meaning.

| Code | Name | What it is |
|---|---|---|
| **A1** | Portrait image (1200×1500) | Attention trend, sentiment, most-engaged posts and main themes |
| **A2** | Square image (1500×1500) | The A1 content plus a sidebar of the most-engaged accounts |
| **A3** | Community image (1500×1500) | Map of the most-engaged accounts, sized by engagement and ringed by stance |
| **M1** | Finding message | Opens with the most positively discussed theme; offers the attention report |
| **M2** | Moment message | Opens with a highly engaged post (never a hostile one); offers the attention report |
| **M3** | Question message | Asks whether a theme is something they want to build awareness around |
| **M4** | Voices message | Opens with how the most-engaged accounts lean; offers the community map |

## The report response (`GET /api/report/<handle>`)

| Field | Meaning |
|---|---|
| `status` | `ready` (all images), `partial` (some), `unavailable` (none), `running`, `error` |
| `generated_at` | When this report was built (UTC). It reflects X activity available then |
| `variants[]` | The **only** images you may use. Each has `code`, `label`, `size`, `png_url`, `download_name` |
| `zip_url` | One ZIP of every ready image, or `null` |
| `assets.<id>` | Every image with `status` (`ready` or `blocked`) and `reasons[]` (`code`, `message`, `actual`, `required`) |
| `outreach` | Suggested X DMs. `review_required` is always `true`; `suggestions[]` is described below. When empty, `unavailable_reason` says why |
| `data` | The underlying numbers (mentions, themes, key posts, sentiment), for reference only. Use the top-level `variants`, not `data.variants` |

Image URLs are relative: fetch `$BASE` + `png_url`.

## Suggested messages (`outreach.suggestions[]`)

Each suggestion has:

- `code` and `label`: e.g. `M2`, `M2 Moment`
- `recommended`: `true` for exactly one, the best-supported angle
- `opener`: the first DM. Contains no link and no image
- `follow_up`: send this **after the prospect replies**, together with the image in `asset_code`
- `asset_code`: which image to attach with the follow-up (always one of the ready images)
- `evidence[]`: the report facts behind each claim, including links to cited posts

On X you cannot send an image until the prospect replies. So:

1. Send `opener` on its own.
2. Wait for a reply.
3. Send `follow_up` with the image whose `code` equals `asset_code`.

## Mention coverage

Reports count whole-handle matches in tweet text, read from complete CSV records
(including quoted multiline posts). Similar handles, such as `trylimitlessfin`
when requesting `trylimitless`, do not match. A post is not counted solely because
the target account authored it. Bare display-name aliases are not inferred, since
common names can refer to unrelated subjects.

Mention caches are isolated by snapshot. Reports generated before the complete-record
parser fix on 2026-10-26 must be regenerated to recover omitted multiline posts;
their saved images and numbers do not change automatically. Topic candidates are
checked for author diversity before selecting up to four talking points.

## Rules

- Only use images listed in `variants`. A blocked image is never available, even if you guess its URL.
- The messages are drafts (`outreach.review_required: true`). Every claim comes from the report, and `evidence[]` lists the facts behind it. Before sending, check that the opener still matches `evidence[]` and that any cited post link opens.
- If a report is blocked, read `assets.<id>.reasons`. Reasons mentioning the image provider may succeed on a later retry. Reasons about too few mentions, themes or talking points will not change by retrying the same day.
- Contact each brand once: send one opener per brand, then only the matching follow-up if they reply. Keep your own record of who you have contacted.
- Generate only what you need. Every report costs real money to build, and all callers share one queue.

## Errors

| HTTP | Meaning |
|---|---|
| 400 | Handle must be 1-20 letters, digits or underscores (no `@`) |
| 401 | Missing or wrong token |
| 404 | No report for that handle yet (generate one), or the image is not ready |
