# Rush for agents

<!-- guide_version: 1.0-900412a3 -->

You are reading `https://therush.fun/agents.md`. It is the whole contract between Rush and a creator's
coding agent: everything you need to bring an app to Rush, make it remixable, and read how it
is doing. There is nothing else to read.

Base URL: `https://therush.fun`. MCP: `POST https://therush.fun/mcp`. REST: `POST https://therush.fun/api/agent/<tool>`.
Auth: `Authorization: Bearer rk_live_…`.


## 1. What Rush is, and what a Drop is

Rush is a feed of tiny mini-apps you try in one tap and share the result of. A **Drop** is one
of those apps: an input, one answer, a card worth screenshotting. A **native Drop** is a
`MiniAppSpec` — data, not code — that Rush's own renderer runs. A **hosted Drop** is an app its
creator already runs somewhere else, described to Rush by a small manifest, opened inside Rush
in a frame, and counted and ranked exactly like a native one.

Both live in one lineage graph. A hosted Drop cannot be remixed until its creator attaches a
**remix template**: a native spec whose parent is the hosted version. After that, remixes
descend from the template and the hosted app is the root of its own tree.


## 2. The manifest

Serve this at `https://<your domain>/.well-known/rush.json`, `Content-Type: application/json`.
Schema: `https://therush.fun/schema/rush-manifest.v1.json`.

```json
{
  "accent": "pink",
  "category": "shopping",
  "cover_url": "https://linkpls.app/cover.png",
  "creator": {
    "handle": "rush"
  },
  "embed": "iframe",
  "hook": "screenshot it. find it. buy it.",
  "icon_url": "https://linkpls.app/icon.png",
  "launch_url": "https://linkpls.app/mini",
  "name": "linkpls",
  "remixable": false,
  "spec_url": null,
  "verification": "rush_verify_3f9c0a1b2c3d4e5f",
  "version": 1
}
```

Rules Rush enforces, so check them before you publish the file:

- **https only**, from a public address, no cross-host redirects, 64 KB, 5 second budget.
- `launch_url` must be on the manifest's own domain or a subdomain of it.
- `embed: "iframe"` is a request, not a fact. Rush HEADs `launch_url` and honours it only if
  there is no `X-Frame-Options` and the CSP's `frame-ancestors` is absent or includes
  `https://therush.fun`. Otherwise the Drop is stored as `external` (it opens in a new tab) and the
  registration answer tells you the exact header to add.
- `verification` is the token Rush issued you (§3). It is stripped before hashing, so rotating
  it never changes the Drop's content hash.
- `cover_url` is optional; without one Rush draws a plain cover from `accent`. Add one.
- `surface` is optional: `window` (default), `dashboard` (wide), or `full` (wide and expanded).
  This chooses the display, not the app's backend or a permission grant.
- `privacy_url` may point to your HTTPS privacy policy. Your app retains its own data and sign-in.

Call `rush_plan_integration` with your manifest and `needs_identity`, `needs_offline`, or
`needs_device_access` to get compatibility blockers and the next steps. Rush does not yet
offer end-user SSO, hosted offline packs, or embedded device permissions. Never pass a
creator MCP token, Rush session or platform credential into a hosted app. A full web app
does not need to become a MiniAppSpec; remix templates are optional.

The result bridge asks the person to review and approve before it uploads a shareable result.
Only score, headline, one_liner, reasons and verdict cross that boundary. No app state is synced.

Validate it before you serve it: `rush_validate_manifest` needs no token.


## 3. Proving the creator owns the domain

Rush will not register an app until the domain under it is verified. Call
`rush_check_verification` with the domain; with no token on file it mints one and returns the
three ways to publish it. Any one of them is enough.

```
rush_verify_3f9c0a1b2c3d4e5f6a7b8c9d
```

1. **Manifest** — add it to `/.well-known/rush.json`:

   ```json
   "verification": "rush_verify_3f9c0a1b2c3d4e5f6a7b8c9d"
   ```

2. **Meta tag** — in the `<head>` of `https://<domain>/`:

   ```html
   <meta name="rush-verification" content="rush_verify_3f9c0a1b2c3d4e5f6a7b8c9d">
   ```

3. **DNS TXT** — a record at `_rush.<domain>`:

   ```
   _rush.yourapp.com.  TXT  "rush_verify_3f9c0a1b2c3d4e5f6a7b8c9d"
   ```

Publishing one of these is the one step you cannot do for the creator. Give them the snippet,
wait, then call `rush_check_verification` again. A proof is re-checked every 7 days,
so leave it in place.


## 4. Pairing and auth

The creator opens their Rush dashboard and copies a pairing code that looks like
`RUSH-7K3M-Q9WD`. It lasts 15 minutes and works once.

```bash
curl -sX POST https://therush.fun/api/agent/pair \
  -H 'content-type: application/json' \
  -d '{"code":"RUSH-7K3M-Q9WD"}'
```

```json
{"ok":true,"data":{"token":"rk_live_…","creator":{"handle":"ohad"},
  "scopes":["creator:read","drops:write","analytics:read"],"expires_at":null}}
```

Send it as `Authorization: Bearer rk_live_…` on every MCP and REST call. The token does not
expire; the creator can revoke it from their dashboard at any time, and a revoked token stops
working immediately. **Store it where you store secrets, never in the repository, never in the
manifest, never in a commit.**

Limits: 60 calls a minute per token (429 with `Retry-After`),
5 pairing attempts a minute per address. Every call is logged and
shown to the creator, with the tool name and the outcome.

The agent tools are a Creator Pro feature. A token belonging to an account that is not Pro gets
`403 forbidden` with the upgrade link.


## 5. The MCP server

`POST https://therush.fun/mcp`, JSON-RPC 2.0, Streamable HTTP, stateless. Single objects and batch arrays
both work. `Mcp-Session-Id` is accepted and ignored. `GET /mcp` is 405: there is no SSE stream
in v1, every answer comes back on the POST.

Add it to Claude Code:

```bash
claude mcp add --transport http rush https://therush.fun/mcp --header "Authorization: Bearer rk_live_…"
```

Cursor (`.cursor/mcp.json`) or any other client that takes JSON:

```json
{
  "mcpServers": {
    "rush": {
      "type": "http",
      "url": "https://therush.fun/mcp",
      "headers": { "Authorization": "Bearer rk_live_…" }
    }
  }
}
```

Methods: `initialize`, `notifications/initialized`, `ping`, `tools/list`, `tools/call`,
`resources/list`, `resources/read`, `prompts/list`, `prompts/get`.

`initialize`, `tools/list`, `resources/*` and `prompts/*` need no token, so you can read all of
this before the creator has given you a code. `tools/call` needs one, except for the four open
tools marked below.

A tool result is one `text` content block holding the envelope JSON, plus the same envelope in
`structuredContent`. A tool that failed sets `isError: true` and puts the error in the envelope;
a *protocol* error (bad method, bad params, no token) is a JSON-RPC `error` instead, with
`data.next_step` saying what to do.

```bash
curl -sX POST https://therush.fun/mcp \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer rk_live_…' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"rush_whoami","arguments":{}}}'
```

Resources: `rush://guide`, `rush://schema/manifest.v1`, `rush://schema/mini-app-spec.v1`,
`rush://examples/manifest`, and `rush://examples/spec/{am-i-cooked,should-i-send-it,weekend-picker}`.
Prompts: `bring_my_app(url, handle)`, `make_remixable(slug)`.


## 6. The tools

Every tool returns `{ "ok": true, "data": … }` or
`{ "ok": false, "error": { "code", "message", "next_step" } }`. Read `next_step`: it names the
tool to call or the change to make.

| Tool | Does | Needs |
|---|---|---|
| `rush_whoami` | The handle, plan, scopes and remaining daily allowances behind this token | a token with `creator:read` |
| `rush_get_guide` | Returns https://therush.fun/agents.md as markdown: the manifest format, ownership verification, every tool, the MiniAppSpec schema, the voice rules and the definition of done | nothing |
| `rush_plan_integration` | Checks a hosted manifest and its embedding policy, reports the launch surface, data boundaries, unsupported requirements and actionable next steps | nothing |
| `rush_validate_manifest` | Validates a hosted manifest and returns its canonical hash plus the embed check for its launch_url — whether the app actually allows being framed inside Rush, and the exact header to add if it does not | nothing |
| `rush_validate_spec` | Validates a MiniAppSpec v1 and returns its content hash and cost class | nothing |
| `rush_check_verification` | Looks for Rush's verification token on a domain in all three places: the manifest's `verification` field, a `<meta name="rush-verification">` tag on the front page, and a TXT record at _rush.<domain> | a token with `drops:write` |
| `rush_register_hosted_drop` | Reads /.well-known/rush.json at the URL you give, checks the domain is verified as this creator's, and creates the hosted Drop with its first immutable version | a token with `drops:write` |
| `rush_refresh_hosted_drop` | Fetches the manifest again | a token with `drops:write` |
| `rush_make_remixable` | Takes a MiniAppSpec (inline `spec`, an https `spec_url`, or a `draft_id` from rush_generate_rush_version) and stores it as a draft remix template for a hosted Drop | a token with `drops:write` |
| `rush_generate_rush_version` | Asks Rush's own model for a first MiniAppSpec from the manifest, the app's public page text and your notes, and stores it as a draft with a preview URL | a token with `drops:write` |
| `rush_publish_template` | The confirmation step | a token with `drops:write` |
| `rush_publish_native_drop` | Publishes a validated MiniAppSpec as a brand new native Drop | a token with `drops:write` |
| `rush_list_my_drops` | Slugs, kinds, status and the two numbers that matter (tries, remixes) for everything this creator has published | a token with `creator:read` |
| `rush_get_drop_analytics` | Tries (total, last hour, last day), shares, remixes, likes, cooking score, rank, badge, and the five biggest Drops descended from this one | a token with `analytics:read` |
| `rush_get_tree` | Every version descended from a Drop's lineage root, with the edges between them — what it came from and what came from it | nothing |
| `rush_run_test_try` | Runs a spec — a draft (`draft_id`) or one of this creator's published native Drops (`slug`) — against one input and returns the result payload a person would see | a token with `drops:write` |
| `rush_delete_draft` | Discards a draft nobody published | a token with `drops:write` |

### `rush_whoami`

The handle, plan, scopes and remaining daily allowances behind this token. Call it first to confirm pairing worked and that you are acting for the creator you think you are.

Needs: a token with `creator:read`. REST twin: `POST https://therush.fun/api/agent/whoami`.

```json
{
  "properties": {},
  "required": [],
  "type": "object"
}
```

### `rush_get_guide`

Returns https://therush.fun/agents.md as markdown: the manifest format, ownership verification, every tool, the MiniAppSpec schema, the voice rules and the definition of done. Call this before anything else if you have not read the guide. No token needed.

Needs: nothing. REST twin: `POST https://therush.fun/api/agent/get_guide`.

```json
{
  "properties": {},
  "required": [],
  "type": "object"
}
```

### `rush_plan_integration`

Checks a hosted manifest and its embedding policy, reports the launch surface, data boundaries, unsupported requirements and actionable next steps. Does not deploy, publish, run arbitrary code or grant user access. Use before registering a dashboard, full web app or small app window.

Needs: nothing. REST twin: `POST https://therush.fun/api/agent/plan_integration`.

```json
{
  "properties": {
    "manifest": {
      "type": "object"
    },
    "manifest_url": {
      "type": "string"
    },
    "needs_device_access": {
      "type": "boolean"
    },
    "needs_identity": {
      "type": "boolean"
    },
    "needs_offline": {
      "type": "boolean"
    }
  },
  "required": [],
  "type": "object"
}
```

### `rush_validate_manifest`

Validates a hosted manifest and returns its canonical hash plus the embed check for its launch_url — whether the app actually allows being framed inside Rush, and the exact header to add if it does not. Pass `manifest` (the object) or `manifest_url`. No token needed, so you can get the file right before pairing.

Needs: nothing. REST twin: `POST https://therush.fun/api/agent/validate_manifest`.

```json
{
  "properties": {
    "manifest": {
      "description": "the manifest object itself",
      "type": "object"
    },
    "manifest_url": {
      "description": "https url of a /.well-known/rush.json to fetch and check instead",
      "type": "string"
    }
  },
  "required": [],
  "type": "object"
}
```

### `rush_validate_spec`

Validates a MiniAppSpec v1 and returns its content hash and cost class. Errors come back as JSON pointers naming the exact field. Call this until it says valid — publishing runs the same check, so anything it refuses here it will refuse there. No token needed.

Needs: nothing. REST twin: `POST https://therush.fun/api/agent/validate_spec`.

```json
{
  "properties": {
    "spec": {
      "description": "a MiniAppSpec v1 object",
      "type": "object"
    }
  },
  "required": [
    "spec"
  ],
  "type": "object"
}
```

### `rush_check_verification`

Looks for Rush's verification token on a domain in all three places: the manifest's `verification` field, a `<meta name="rush-verification">` tag on the front page, and a TXT record at _rush.<domain>. With no token issued yet it mints one and returns the three snippets to publish — give those to the human, they are the only step Rush cannot do for them. Pass `rotate: true` for a fresh token.

Needs: a token with `drops:write`. REST twin: `POST https://therush.fun/api/agent/check_verification`.

```json
{
  "properties": {
    "domain": {
      "description": "the bare host, like linkpls.app",
      "type": "string"
    },
    "rotate": {
      "description": "mint a new verification token even if one exists",
      "type": "boolean"
    }
  },
  "required": [
    "domain"
  ],
  "type": "object"
}
```

### `rush_register_hosted_drop`

Reads /.well-known/rush.json at the URL you give, checks the domain is verified as this creator's, and creates the hosted Drop with its first immutable version. Registering the same manifest URL again refreshes it instead of making a second Drop, so this is safe to retry. Returns the slug, the public URL, whether the app framed, and what is still missing.

Needs: a token with `drops:write`. REST twin: `POST https://therush.fun/api/agent/register_hosted_drop`.

```json
{
  "properties": {
    "manifest_url": {
      "description": "https url of the manifest, usually https://<domain>/.well-known/rush.json",
      "type": "string"
    }
  },
  "required": [
    "manifest_url"
  ],
  "type": "object"
}
```

### `rush_refresh_hosted_drop`

Fetches the manifest again. A changed manifest becomes a new immutable version and the Drop's card follows it; an unchanged one writes nothing. The embed check runs every time, so this is also how you confirm a framing header you just added took effect.

Needs: a token with `drops:write`. REST twin: `POST https://therush.fun/api/agent/refresh_hosted_drop`.

```json
{
  "properties": {
    "slug": {
      "description": "the Drop's slug",
      "type": "string"
    }
  },
  "required": [
    "slug"
  ],
  "type": "object"
}
```

### `rush_make_remixable`

Takes a MiniAppSpec (inline `spec`, an https `spec_url`, or a `draft_id` from rush_generate_rush_version) and stores it as a draft remix template for a hosted Drop. It does NOT publish: you get a preview URL to show the creator. Nothing on Rush changes until rush_publish_template.

Needs: a token with `drops:write`. REST twin: `POST https://therush.fun/api/agent/make_remixable`.

```json
{
  "properties": {
    "draft_id": {
      "description": "an existing draft to attach",
      "type": "string"
    },
    "slug": {
      "description": "the hosted Drop's slug",
      "type": "string"
    },
    "spec": {
      "description": "a MiniAppSpec v1 object",
      "type": "object"
    },
    "spec_url": {
      "description": "https url of a MiniAppSpec v1",
      "type": "string"
    }
  },
  "required": [
    "slug"
  ],
  "type": "object"
}
```

### `rush_generate_rush_version`

Asks Rush's own model for a first MiniAppSpec from the manifest, the app's public page text and your notes, and stores it as a draft with a preview URL. Use it when the creator does not want to write a spec. It publishes nothing, counts against the creator's daily AI-draft allowance, and the draft is yours to edit with rush_make_remixable before publishing.

Needs: a token with `drops:write`. REST twin: `POST https://therush.fun/api/agent/generate_rush_version`.

```json
{
  "properties": {
    "notes": {
      "description": "the creator's steer, in their words — what the Rush version should ask for and answer",
      "type": "string"
    },
    "slug": {
      "description": "the hosted Drop's slug",
      "type": "string"
    }
  },
  "required": [
    "slug"
  ],
  "type": "object"
}
```

### `rush_publish_template`

The confirmation step. Publishes a draft as an immutable native version whose parent is the hosted version, lights up remix on the hosted Drop, and returns the URL. Ask the human first and show them the preview URL — the guide's rule is that nothing publishes without their yes. Pass `tryable: true` to also put the template itself in the feed as its own Drop.

Needs: a token with `drops:write`. REST twin: `POST https://therush.fun/api/agent/publish_template`.

```json
{
  "properties": {
    "draft_id": {
      "description": "from rush_make_remixable",
      "type": "string"
    },
    "tryable": {
      "description": "also publish the template as a native Drop people can try",
      "type": "boolean"
    }
  },
  "required": [
    "draft_id"
  ],
  "type": "object"
}
```

### `rush_publish_native_drop`

Publishes a validated MiniAppSpec as a brand new native Drop. `parent_slug` makes it a remix of an existing Drop — pass the hosted Drop's slug to descend from its remix template. Validate with rush_validate_spec first and get the creator's yes: this is live immediately.

Needs: a token with `drops:write`. REST twin: `POST https://therush.fun/api/agent/publish_native_drop`.

```json
{
  "properties": {
    "parent_slug": {
      "description": "publish it as a remix of this Drop",
      "type": "string"
    },
    "remix_policy": {
      "enum": [
        "public",
        "none"
      ]
    },
    "spec": {
      "description": "a MiniAppSpec v1 object",
      "type": "object"
    },
    "visibility": {
      "enum": [
        "public",
        "private"
      ]
    }
  },
  "required": [
    "spec"
  ],
  "type": "object"
}
```

### `rush_list_my_drops`

Slugs, kinds, status and the two numbers that matter (tries, remixes) for everything this creator has published. Use it to find a slug, or to check what a call actually did.

Needs: a token with `creator:read`. REST twin: `POST https://therush.fun/api/agent/list_my_drops`.

```json
{
  "properties": {},
  "required": [],
  "type": "object"
}
```

### `rush_get_drop_analytics`

Tries (total, last hour, last day), shares, remixes, likes, cooking score, rank, badge, and the five biggest Drops descended from this one. Rollups run every minute, so a Drop published seconds ago reads zero with a note saying so.

Needs: a token with `analytics:read`. REST twin: `POST https://therush.fun/api/agent/get_drop_analytics`.

```json
{
  "properties": {
    "slug": {
      "description": "the Drop's slug",
      "type": "string"
    }
  },
  "required": [
    "slug"
  ],
  "type": "object"
}
```

### `rush_get_tree`

Every version descended from a Drop's lineage root, with the edges between them — what it came from and what came from it. Public: no token needed, and it works for anybody's Drop.

Needs: nothing. REST twin: `POST https://therush.fun/api/agent/get_tree`.

```json
{
  "properties": {
    "slug": {
      "description": "any public Drop's slug",
      "type": "string"
    }
  },
  "required": [
    "slug"
  ],
  "type": "object"
}
```

### `rush_run_test_try`

Runs a spec — a draft (`draft_id`) or one of this creator's published native Drops (`slug`) — against one input and returns the result payload a person would see. No try is recorded and no counter moves: this is for checking your own work. The input is used and discarded.

Needs: a token with `drops:write`. REST twin: `POST https://therush.fun/api/agent/run_test_try`.

```json
{
  "properties": {
    "choice": {
      "description": "the input, for a choice Drop: the option ids",
      "items": {
        "type": "string"
      },
      "type": "array"
    },
    "draft_id": {
      "description": "a draft to run",
      "type": "string"
    },
    "image_url": {
      "description": "the input, for an image Drop: a public https image under 4mb",
      "type": "string"
    },
    "slug": {
      "description": "or one of your native Drops",
      "type": "string"
    },
    "text": {
      "description": "the input, for a text Drop",
      "type": "string"
    }
  },
  "required": [],
  "type": "object"
}
```

### `rush_delete_draft`

Discards a draft nobody published. Drafts expire on their own after seven days; this is for when the creator says no and you want the slate clean.

Needs: a token with `drops:write`. REST twin: `POST https://therush.fun/api/agent/delete_draft`.

```json
{
  "properties": {
    "draft_id": {
      "type": "string"
    }
  },
  "required": [
    "draft_id"
  ],
  "type": "object"
}
```



## 7. The REST twins

Every tool is also `POST https://therush.fun/api/agent/<name without the rush_ prefix>` with the same JSON
body as `arguments` and the same envelope back. If your environment has no MCP support, use
these and nothing is lost.

```bash
# the guide itself, no token
curl -s https://therush.fun/api/agent/guide

# check a manifest before you serve it, no token
curl -sX POST https://therush.fun/api/agent/validate_manifest \
  -H 'content-type: application/json' \
  -d '{"manifest_url":"https://yourapp.com/.well-known/rush.json"}'

# everything else takes the token
curl -sX POST https://therush.fun/api/agent/register_hosted_drop \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer rk_live_…' \
  -d '{"manifest_url":"https://yourapp.com/.well-known/rush.json"}'
```

The full order for "bring my app", start to finish:

```
pair → whoami → check_verification → (human publishes the token) → check_verification
  → register_hosted_drop → validate_spec → make_remixable → (human says yes)
  → publish_template → get_drop_analytics
```


## 8. The `MiniAppSpec`

Schema: `https://therush.fun/schema/mini-app-spec.v1.json`. A spec is **data**. It never contains code,
a prompt, a URL you invented, or a model name. `tool.prompt_id` names a reviewed prompt in
Rush's registry: `buy_v1`, `class_v1`, `cooked_v1`, `extract_ideas_v1`, `fit_v1`, `generate_spec_v1`, `generic_text_v1`, `generic_vision_v1`, `group_v1`, `launch_aura_v1`, `launch_battle_v1`, `launch_cooked_v1`, `launch_decision_v1`, `launch_group_v1`, `launch_rating_v1`, `launch_semester_v1`, `moderate_copy_v1`, `remix_delta_v1`, `room_v1`, `rush_version_v1`, `send_it_v1`, `study_v1`.

A remix may only change these fields, and only the ones the parent lists in `remix.mutable`:

`title`, `hook`, `instruction`, `cta`, `category`, `tool.audience`, `tool.tone`, `input.kind`, `input.placeholder`, `input.chips`, `input.label`, `input.sublabel`, `output.headline`, `output.labels`, `theme.accent`, `remix.chips`

`input.kind` may only move between `image` and `text`, and a change to it must supply the whole
`input` object. `lineage.parent_hash` is written by Rush; do not invent one.

### Example: `am-i-cooked` — image input, ai_verdict

```json
{
  "category": "school",
  "cta": "let's find out →",
  "hook": "upload your schedule. find out.",
  "input": {
    "accept": [
      "image/*"
    ],
    "example": "asset://ex/schedule.png",
    "kind": "image",
    "label": "upload your schedule",
    "max": 1,
    "sublabel": "screenshot or photo"
  },
  "instruction": "UPLOAD YOUR SCHEDULE AND GET A REALITY CHECK",
  "lineage": {
    "parent_hash": null
  },
  "min_client_version": 1,
  "output": {
    "advice_line": true,
    "headline": "YOU'RE {score}% COOKED",
    "kind": "verdict_card",
    "labels": [
      {
        "label": "you're fine",
        "max": 33
      },
      {
        "label": "not great",
        "max": 66
      },
      {
        "label": "cooked",
        "max": 100
      }
    ],
    "reasons": 3,
    "score": {
      "max": 100,
      "min": 0
    },
    "verdict_line": true
  },
  "remix": {
    "chips": [
      "make it for college",
      "make it about my friendships",
      "photos instead of text",
      "make it more brutal"
    ],
    "mutable": [
      "title",
      "hook",
      "instruction",
      "cta",
      "tool.audience",
      "tool.tone",
      "input.kind",
      "input.label",
      "input.sublabel",
      "output.headline",
      "output.labels",
      "theme.accent",
      "remix.chips"
    ]
  },
  "safety": {
    "faces": "allow_no_rating",
    "min_age_hint": 13
  },
  "spec_version": 1,
  "theme": {
    "accent": "pink",
    "cover": "asset://covers/am-i-cooked.png",
    "paper": "torn"
  },
  "title": "Am I Cooked?",
  "tool": {
    "audience": "students",
    "kind": "ai_verdict",
    "modality": "vision",
    "model_policy": "cheap_vision",
    "prompt_id": "cooked_v1",
    "tone": "brutal"
  }
}
```


### Example: `should-i-send-it` — text input, ai_verdict

```json
{
  "category": "social",
  "cta": "read it back to me →",
  "hook": "paste the message. we read it back to you.",
  "input": {
    "chips": [
      {
        "id": "to_ex",
        "label": "to an ex"
      },
      {
        "id": "group_chat",
        "label": "to the group chat"
      },
      {
        "id": "boss",
        "label": "to my boss"
      },
      {
        "id": "crush",
        "label": "to a crush"
      }
    ],
    "kind": "text",
    "label": "the message",
    "max_chars": 1200,
    "placeholder": "paste the message",
    "sublabel": "we never keep it"
  },
  "instruction": "PASTE IT BEFORE YOU REGRET IT",
  "lineage": {
    "parent_hash": null
  },
  "min_client_version": 1,
  "output": {
    "advice_line": true,
    "headline": "{score}% SEND IT",
    "kind": "verdict_card",
    "labels": [
      {
        "label": "do not send",
        "max": 33
      },
      {
        "label": "maybe don't",
        "max": 66
      },
      {
        "label": "send it",
        "max": 100
      }
    ],
    "reasons": 1,
    "score": {
      "max": 100,
      "min": 0
    },
    "verdict_line": true
  },
  "remix": {
    "chips": [
      "for a group chat",
      "for an ex",
      "for my boss",
      "make it kinder"
    ],
    "mutable": [
      "title",
      "hook",
      "instruction",
      "cta",
      "tool.audience",
      "tool.tone",
      "input.kind",
      "input.placeholder",
      "input.chips",
      "output.headline",
      "output.labels",
      "theme.accent",
      "remix.chips"
    ]
  },
  "safety": {
    "faces": "allow_no_rating",
    "min_age_hint": 13
  },
  "spec_version": 1,
  "theme": {
    "accent": "purple",
    "cover": "asset://covers/should-i-send-it.png",
    "paper": "torn"
  },
  "title": "Should I Send It?",
  "tool": {
    "audience": "everyone",
    "kind": "ai_verdict",
    "modality": "text",
    "model_policy": "cheap_text",
    "prompt_id": "send_it_v1",
    "tone": "honest"
  }
}
```


### Example: `weekend-picker` — choice input, rules

```json
{
  "category": "social",
  "cta": "pick for me →",
  "hook": "pick your mood. we pick the plan.",
  "input": {
    "kind": "choice",
    "multi": false,
    "options": [
      {
        "id": "stay_in",
        "label": "stay in"
      },
      {
        "id": "go_out",
        "label": "go out"
      },
      {
        "id": "broke",
        "label": "broke but bored"
      },
      {
        "id": "road_trip",
        "label": "road trip"
      }
    ]
  },
  "instruction": "PICK ONE AND STOP OVERTHINKING IT",
  "lineage": {
    "parent_hash": null
  },
  "min_client_version": 1,
  "output": {
    "advice_line": false,
    "headline": "{score}% WORTH IT",
    "kind": "verdict_card",
    "labels": [
      {
        "label": "stay home",
        "max": 33
      },
      {
        "label": "maybe",
        "max": 66
      },
      {
        "label": "go",
        "max": 100
      }
    ],
    "reasons": 2,
    "score": {
      "max": 100,
      "min": 0
    },
    "verdict_line": true
  },
  "remix": {
    "chips": [
      "make it a friday night",
      "make it for broke weeks",
      "add a chaos option",
      "make it for two people"
    ],
    "mutable": [
      "title",
      "hook",
      "instruction",
      "cta",
      "output.headline",
      "output.labels",
      "theme.accent",
      "remix.chips"
    ]
  },
  "safety": {
    "faces": "allow_no_rating",
    "min_age_hint": 13
  },
  "spec_version": 1,
  "theme": {
    "accent": "lime",
    "cover": "asset://covers/weekend-picker.png",
    "paper": "clean"
  },
  "title": "Weekend Picker",
  "tool": {
    "default": {
      "reasons": [
        "you didn't pick, so we picked for you"
      ],
      "score": 50,
      "verdict": "do the cheap thing and call it a weekend"
    },
    "kind": "rules",
    "rules": [
      {
        "reasons": [
          "the couch is undefeated",
          "nobody is checking on you"
        ],
        "score": 80,
        "verdict": "stay in and win",
        "when": {
          "option": "stay_in"
        }
      },
      {
        "reasons": [
          "you'll regret the bed more",
          "the group chat is already awake"
        ],
        "score": 65,
        "verdict": "go out, leave by midnight",
        "when": {
          "option": "go_out"
        }
      },
      {
        "reasons": [
          "free is a plan",
          "walking counts"
        ],
        "score": 55,
        "verdict": "walk somewhere new and call it a night out",
        "when": {
          "option": "broke"
        }
      },
      {
        "reasons": [
          "gas split four ways is nothing",
          "the drive is the plan"
        ],
        "score": 90,
        "verdict": "go, but leave before noon",
        "when": {
          "option": "road_trip"
        }
      }
    ]
  }
}
```



## 9. The voice

Every copy field a person reads — `title`, `hook`, `instruction`, `cta`, `output.headline`,
`output.labels`, `remix.chips` — is written in one voice. Get this wrong and the Drop looks
like an ad in a feed of jokes.

- all lowercase, except `instruction`, which is SHOUTED because it is the handwritten line
- deadpan. state the situation, do not comment on it
- no exclamation marks, no "unleash", no "supercharge", no em dashes in copy
- at most one emoji, and only from 😭 ✌️ 🥀 💔 🙏 🗿 💀 🔥 👀
- never explain the joke
- `cta` ends with an arrow: `let's find out →`
- every failure state gets its own sentence and a next step

Copy is moderated before it publishes, by a different model from the one that wrote it. Copy
aimed at a person rather than a situation is refused, and so is anything that reads as an
instruction to a model.


## 10. `rush-embed.js` — hosted results and share cards

A hosted app that tells Rush its result gets a Rush result card and a share token, exactly like
a native Drop. Add one script and one call:

```html
<script src="https://therush.fun/rush-embed.js"></script>
<script>
  Rush.ready();
  Rush.result({
    headline: "IT'S THE THIRD ONE",
    score: 72,
    one_liner: "the link was right there",
    reasons: ["same stitching", "same tag", "cheaper on the second one"],
    verdict: "buy the third one"
  });
</script>
```

The caps: `headline` 80 characters, `one_liner`
120, `verdict` 200, at most
5 `reasons` of 120 each, `score` an integer
0–100 or null. Anything longer is refused rather than truncated.

How it works, and what your page is and is not responsible for:

1. Your page posts `{ type: "rush:result", version: 1, payload }` to the parent window. That is
   all `rush-embed.js` does. It posts with `targetOrigin: "*"` because a framed page cannot know
   its parent's origin — that is the same-origin policy, not an oversight — so **never put
   anything secret in the payload**.
2. The Rush page checks `event.origin` against your Drop's launch origin before it believes the
   message.
3. Rush then calls `POST /api/drops/<slug>/hosted_result` itself, with a **launch token** it was
   given when it opened the frame. The token is signed by Rush, carries the Drop and the device
   it was issued to, and lasts an hour. A result posted without it is refused — a browser-side
   origin check is a check in a browser, and Rush does not take that as proof on its own.

Your page never holds a Rush credential and never sees the launch token: the person's own
session is what authorises the result. Report and moderation apply to a hosted result card like
any other user content.

This only works when the Drop's embed is `iframe`. An `external` Drop opens in a new tab, where
there is no parent window to talk to.


## 11. Definition of done

"Bring my app to Rush" is finished when all of these are true:

1. `rush_check_verification` says `verified: true`
2. `rush_register_hosted_drop` returned a slug, and `https://therush.fun/d/<slug>` opens
3. the embed is `iframe`, or the creator has decided `external` is what they want
4. the manifest has a `cover_url` and a hook in the voice
5. a remix template is published: `rush_publish_template` returned `remixable: true`
6. the template's spec passes `rush_validate_spec`, and you ran `rush_run_test_try` on it at
   least once and read the result
7. the creator has seen the preview URL and said yes
8. `rush_get_drop_analytics` answers for the slug

Give the creator this optional, accurate attribution link for their own app or launch page:

```html
<a href="https://therush.fun/d/<slug>?source=creator_badge">Try this app on Rush</a>
```

It links to the verified public Rush page and is not a paid endorsement. Do not present it as
a recommendation, approval, ranking guarantee or sponsored placement. If there is a paid,
affiliate, gifted or other material connection behind a promotion, the creator must disclose it
clearly where people see the promotion.

Then tell the human the URL and stop.


## 12. What to ask, and what never to do

**Ask the human for:**

- the hook line and the category, if the manifest has none — these are theirs, not yours
- their Rush handle. Never guess one, never register one
- permission to publish, every time, with the preview URL in front of them
- the verification token's placement, since only they can change their own site
- whether `external` is acceptable, if the app refuses to be framed

**Never:**

- publish anything without an explicit yes. `rush_make_remixable` and
  `rush_generate_rush_version` only ever write drafts; `rush_publish_template` and
  `rush_publish_native_drop` are the two calls that go live
- invent a handle, a domain, a cover image URL or a `prompt_id`
- put any secret in the manifest except the `verification` token — it is a public file
- commit the bearer token, echo it into a log, or paste it into an issue
- write a spec that names a model, embeds a prompt, or contains anything executable
- retry a failed call more than twice without reading `next_step` and changing something
- claim a Drop is live before `rush_list_my_drops` shows it

