> For the complete documentation index, see [llms.txt](https://series-1.gitbook.io/rundot-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://series-1.gitbook.io/rundot-docs/v5.28.0/readme/marketing-your-game.md).

# Marketing Your Game (BETA)

## Marketing Your Game (BETA)

Run paid ad campaigns for your game straight from the `rundot` CLI. You write a short prompt, RUN.world generates the source ad creatives for you, and a campaign is submitted to Meta (Facebook / Instagram) or Google Ads to drive new players to your game.

{% hint style="info" %}
This is **paid user acquisition** — *spending* to bring new players to your game. To *earn* revenue by showing ads inside your game, see the [Ad Monetization API](/rundot-docs/v5.28.0/readme/ads.md).
{% endhint %}

{% hint style="warning" %}
**Beta feature.** Marketing commands are hidden by default. Enable them for your shell session before you start:

```bash
export RUNDOT_BETA_FEATURES=1
```

Without this set, `rundot marketing` commands won't appear in `--help`.
{% endhint %}

### How it works

You build a campaign locally, RUN.world generates the assets, and you submit it. Submitting **does not start spending money** — every campaign is reviewed by the RUN.world team and only goes live once it's approved ("flighted").

```
rundot marketing prepare   →  scaffold a campaign + write its prompt
rundot marketing generate  →  RUN.world generates source ad creatives (AI)
rundot marketing submit    →  upload + submit for review (created PAUSED)
        │
        ▼
  RUN.world review  →  approved campaigns are flighted (go live) and start spending
        │
        ▼
rundot marketing stats     →  track spend, installs, ROAS, and more
```

All commands run from your game's project directory and use the same `game.config.prod.json` as `rundot deploy`. Add `--game-id` to target a specific game.

### 1. Prepare a campaign

```bash
rundot marketing prepare --name spring-push
```

This creates a local campaign folder under `.rundot/marketing/spring-push/`:

```
.rundot/marketing/spring-push/
  campaign.json          # the campaign manifest (prompts, targeting, etc.)
  logo/                  # AI-generated logo (auto-reference for style consistency)
  square/                # 1:1 source creatives (every network uses this)
  vertical/              # 9:16 source creatives (auto-generated for networks that use it, e.g. Meta)
  landscape/             # 1.91:1 source creatives (auto-generated for networks that use it, e.g. Meta, Google)
  video/                 # optional hand-uploaded video
  references/            # reference images that guide AI generation
```

Source creatives are **network-agnostic**: each ad network (Meta, Google, Reddit) adapts the same images to its own slots. Prepare a separate campaign per network when you want distinct creative per network.

Useful options:

| Option                             | Purpose                                                                                                                                                                                               |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--network meta`                   | Ad network to run on: `meta` (default), `google`, `reddit`, or `run` (RUN's own in-app house cross-promo network, mobile-web). `google` is traffic/install only.                                      |
| `--platforms web,ios,android`      | Which platforms to advertise on. **Omit to run Android only** (or web for the web-only `run` network); web and iOS are explicit selections.                                                           |
| `--genre puzzle`                   | Steer creative concepts (`puzzle`, `arcade`, `idle`, `rpg`, `strategy`, `simulation`, `action`, `casual`, `card`, `platformer`).                                                                      |
| `--art-style chibi`                | Steer the art style toward proven lower-CPI looks (`chibi`, `pixel`, `anime`, `blocky`, `flat-cartoon`, `hand-drawn`; `painterly`/`cinematic` selectable). Defaults to a clean stylized cartoon look. |
| `--budget 500`                     | Total campaign budget in USD, spent over the whole flight — **not** a daily amount. $50–$25,000. Omit for the server default.                                                                         |
| `--days 14`                        | Length of the flight window in days (default 14, max 90). Your budget paces itself over this many days automatically.                                                                                 |
| `--countries US,BR,MX`             | Target specific countries (2-letter ISO codes). Default: US.                                                                                                                                          |
| `--age-min 21` / `--age-max 45`    | Target an age range (13–65). Default: 18–65.                                                                                                                                                          |
| `--prompt-square "..."`            | Override the square-creative prompt inline (also `--prompt-vertical`, `--prompt-landscape`, `--prompt-logo`).                                                                                         |
| `--prompt-square-file ./prompt.md` | Provide the prompt from a file.                                                                                                                                                                       |
| `--override`                       | Overwrite an existing campaign of the same name.                                                                                                                                                      |

There's no daily budget to set directly — the network paces your total `--budget` over the `--days` flight window on its own. `prepare` warns when that implied pace (budget ÷ days ÷ number of platform legs) is at or below \~$50/day, since that may gather too little signal during the learning phase. After creatives exist, `generate` separately recommends a number of distinct creative concepts ("creative families") sized to the same budget, and warns if it supports fewer than 3.

Prompts default to built-in templates that fill in your game's name and description automatically. Edit `campaign.json` (or use the `--prompt-*` flags) to customize them.

### 2. Generate creatives

```bash
rundot marketing generate --name spring-push
```

RUN.world generates the ad images server-side, resizes them to spec, and downloads them into your campaign folder.

| Option                                                 | Purpose                                                                                                                                                                                                                                                    |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--kind square`                                        | Generate only one source-creative kind (`square`, `vertical`, `landscape`, `logo`). Omit to generate `logo` **plus every image kind your chosen network uses** — e.g. all three for Meta, `square`+`landscape` for Google, `square` only for Reddit/`run`. |
| `--variants 4`                                         | How many variants ("creative families") to generate (default 4 for `square`, 3 otherwise; capped at 10 per kind). On Meta this doesn't map to separate ads — see the delivery-mode note below.                                                             |
| `--no-diversify`                                       | Disable distinct per-variant concepts (diversification is on by default).                                                                                                                                                                                  |
| `--hi-fi`                                              | Use the higher-fidelity Pro image model for `square`/`landscape`.                                                                                                                                                                                          |
| `--reference ./logo.png --reference https://…/art.png` | Reference images (local files or HTTPS URLs) to guide style. Repeatable (max 4) — pass the flag once per image; comma-separated lists are **not** supported.                                                                                               |
| `--regen square/0.png`                                 | Regenerate a single asset.                                                                                                                                                                                                                                 |
| `--new-seed`                                           | Use a fresh random seed when regenerating.                                                                                                                                                                                                                 |
| `--force`                                              | Overwrite existing assets.                                                                                                                                                                                                                                 |

Before a paid square or landscape generation starts, the CLI preflights every reference image with OCR. Detected text produces a warning but does not block the run; use text-free references to reduce copied text. Generated output at or above the advisory threshold is still saved and flagged for review.

By default `generate` produces **distinct** concepts per variant (not N samples of one prompt). If your budget divided across the recommended number of creative families falls below \~$15/family/day, it warns that some creatives may not clear the learning phase. You can also hand-place your own images or a video into the folders instead of generating them.

{% hint style="info" %}
**How images actually deliver on Meta.** New Meta campaigns default to **asset-pool** delivery: every selected image across `square`/`vertical`/`landscape` ships as one pool (up to 30 images) and **Meta** decides which ones actually get shown — that's normal, not a bug, and per-image reporting for this mode isn't guaranteed. The legacy **placement** mode instead ships exactly one image per kind (lowest filename wins) and warns you at `submit`/`preview` about any image that won't serve.
{% endhint %}

### 3. Ad copy

Ad copy is **campaign-level**, not per-image, so you author it once in **`campaign.json`**. `generate` seeds `headlines` and `primaryTexts` arrays from your game's name and description — edit them there before submitting.

* **Meta:** 1–5 headlines (≤ 40 characters each) and 1–5 primary texts (≤ 125 characters each).
* **Google:** exactly 5 headlines (≤ 30 characters each) and exactly 5 descriptions (≤ 90 characters each). The manifest still calls the descriptions `primaryTexts` for the shared schema; the Google adapter maps them to its `descriptions` assets. The shorter 30-character headline limit keeps one copy set valid for Google App campaigns and Demand Gen.

The images themselves stay text-free, and the numbered per-creative sidecars (`square/0.json`, …) carry only prompt / seed / references — **not** copy. Putting copy in a sidecar does nothing (a legacy `square/0.json` is read once for back-compat, but `campaign.json` is the source of truth). `submit` rejects placeholder, overlong, or incorrectly sized copy before uploading assets.

### 4. Submit for review

```bash
rundot marketing submit --name spring-push
```

This uploads your assets and submits the campaign. The campaign is created **paused** — it is **not spending yet**, and your credits aren't touched yet either (the budget is only charged once the campaign flights). The RUN.world team reviews it, and once approved it's flighted (goes live).

You'll see the campaign move through these states (visible via `rundot marketing status`):

| Status                    | Meaning                                                                                                                                                                                                                                                     |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pending-review`          | Submitted and waiting for review. Not spending. Left unreviewed for 14 days, it's auto-rejected.                                                                                                                                                            |
| `flighted`                | Approved and live. Spending against its total budget, paced over the flight window.                                                                                                                                                                         |
| `rejected`                | Not approved. Not spending.                                                                                                                                                                                                                                 |
| `paused-by-creator`       | You paused it. Not spending, but its prepaid budget is **retained**, not refunded — `resume` picks up right where it left off.                                                                                                                              |
| `paused-external`         | RUN.world detected a budget/config edit made directly in the ad network's own dashboard rather than through the CLI, and paused the campaign to stop it. Budget is retained; `resume` after fixing it (or just re-adjusting via `rundot marketing budget`). |
| `auto-paused-performance` | Auto-paused by the performance guard for clear underperformance. Budget is **retained**, not refunded — `cancel` it if you're done, or leave it paused. `stats`/`status` show the reason.                                                                   |
| `completed`               | Finished on its own — spent its full budget or reached the end of its flight window. The unspent remainder (if any) is refunded automatically.                                                                                                              |
| `cancelled`               | You stopped it permanently. The unspent remainder is refunded.                                                                                                                                                                                              |

### Reviewing performance

Check a campaign's status and budgets:

```bash
rundot marketing status --name spring-push
```

Pull performance metrics (spend, impressions, clicks, CTR, installs, CPI, revenue, ROAS, and more):

```bash
# Last 30 days as a table (default)
rundot marketing stats --name spring-push

# As CSV for a spreadsheet
rundot marketing stats --name spring-push --days 30 --format csv > spring-push.csv
```

`stats` keeps cost (CPI / CPA / ROAS) as the primary signal and adds a **CVR** column (installs ÷ clicks). It flags a campaign whose CTR is well below the \~1% gaming benchmark with a benchmark-relative, post-learning advisory — not a hard pass/fail gate (judge creatives only after the \~7-day learning phase). If a campaign was auto-paused by the performance guard, `stats` shows that and the reason it was killed.

{% hint style="info" %}
Some metrics (reach, CTR, conversions) show `—` in a table or a blank cell in CSV when the ad network didn't report them. That means **no data**, not zero.
{% endhint %}

List all your campaigns with their latest numbers:

```bash
rundot marketing list
```

### Managing a live campaign

```bash
# Change the total budget (USD, spent over the remaining flight — not a daily amount)
rundot marketing budget --name spring-push --budget 750

# Turn specific platform legs on/off without changing the total
rundot marketing budget --name spring-push --platforms ios,android

# Lengthen the flight window without touching the budget (money-neutral)
rundot marketing extend --name spring-push --add-days 7

# Pause spending — budget is retained, not refunded — and resume later
rundot marketing pause --name spring-push
rundot marketing resume --name spring-push

# Permanently stop a campaign and refund whatever's unspent
rundot marketing cancel --name spring-push
```

A few rules worth knowing:

* **Before it's flighted**, changing the budget is free — nothing has been charged yet.
* **After it's flighted**, raising the budget charges the difference immediately; lowering it refunds the difference only once the campaign ends.
* **Budget changes work pre-review or once flighted; extending the window needs a flighted campaign.** Either way, a paused campaign must be resumed first — you can't change the budget or the window while it's paused.
* **A campaign already submitted can't have its creative, copy, targeting, or platforms changed** — only its budget and flight length are ever editable. To change anything else, `cancel` and submit again under a new name (names can't be reused, even after cancelling).

{% hint style="warning" %}
Always change budgets through the CLI (or the controls the RUN.world team uses), **not** directly in Meta Ads Manager. Out-of-band edits in Ads Manager are detected automatically and will pause the campaign.
{% endhint %}

### Paying for ad spend

Campaigns are funded from your RUN.world **credits**. A few things to know:

* A campaign never spends until it's approved and flighted by the RUN.world team — nothing is charged at `submit`.
* **Your entire campaign budget is charged up front, the moment it's flighted** — not a little each day. There's also a flat, non-refundable **5,000-credit flight fee**. If your balance can't cover the budget plus that fee, flighting is blocked until you top up.
* Ad spend uses the provider-cost conversion of **1,000 credits per $1 USD**, times a small ad-spend markup (currently **1.05×**) applied to the USD amount first. Credit top-ups use a separate purchase rate: at the current default, **$1 USD adds 500 credits**. Use the ad-spend rate, not the top-up rate, when estimating campaign debits.
* **Pausing does not refund anything** — your prepaid budget is retained so `resume` can pick up exactly where it left off. Only a **terminal** stop refunds the unspent remainder: `cancel`, or the campaign concluding on its own (`completed`) when it spends its full budget or its flight window ends.

If a budget change or flight is blocked for insufficient credits, top up your balance and try again.

### Asset specifications

Generated and hand-uploaded assets must meet these specs (the CLI validates them before submit):

#### Images

These are network-agnostic source creatives; each provider reshapes them to its own slots (Meta dynamic creative; Google letterboxes the square/vertical into 1.91:1 + 1:1 and uses the landscape source directly; Reddit as-is).

| Kind      | Aspect Ratio | Size (px)   | Format | Max File Size | Max Variants | Used by       |
| --------- | ------------ | ----------- | ------ | ------------- | ------------ | ------------- |
| Square    | 1:1          | 1080 × 1080 | PNG    | 8 MB          | 10           | every network |
| Vertical  | 9:16         | 1242 × 2208 | PNG    | 8 MB          | 10           | Meta only     |
| Landscape | 1.91:1       | 1200 × 628  | PNG    | 8 MB          | 10           | Meta, Google  |

`generate` produces whichever of these your `--network` actually uses — you don't have to opt in with `--kind`. Only the `square` creative is required at submit; `vertical` and `landscape` stay optional even for a network that normally uses them.

Generated `square` and `landscape` creatives are checked for baked-in text after generation. At **4% or more** detected text coverage, generation still succeeds and the CLI flags the creative for review. The 4% value is an internal advisory threshold, not a Meta rejection rule; creatives are not rejected or discarded solely because they cross it.

#### Video (optional)

| Property      | Value         |
| ------------- | ------------- |
| Format        | MP4           |
| Duration      | 15–30 seconds |
| Max file size | 100 MB        |

Video is hand-uploaded (not AI-generated).

#### Reference images

| Property          | Value                                  |
| ----------------- | -------------------------------------- |
| Max count         | 4                                      |
| Max size per file | 5 MB                                   |
| Formats           | PNG, JPEG, WebP                        |
| URLs              | HTTPS URLs accepted (no upload needed) |

### Content moderation

All campaigns are moderated before they reach the ad network. Headlines, body copy, and prompts are checked for profanity and policy violations, and generated/uploaded images and video are scanned. Anything that fails moderation rejects the submission with a descriptive error, so keep creatives and copy advertising-policy compliant.

### Command reference

All commands live under `rundot marketing` and require `RUNDOT_BETA_FEATURES=1`.

| Command     | Purpose                                                               |
| ----------- | --------------------------------------------------------------------- |
| `prepare`   | Scaffold a campaign and write `campaign.json`.                        |
| `generate`  | Generate source creatives (AI).                                       |
| `composite` | Overlay a logo or image onto a generated asset.                       |
| `refs`      | Build the reference palette for generation.                           |
| `tips`      | Print prompt-writing guidance and asset requirements.                 |
| `submit`    | Upload assets and submit the campaign for review.                     |
| `status`    | Show a campaign's status, budgets, and IDs.                           |
| `preview`   | Render a local preview of a prepared campaign.                        |
| `stats`     | Show performance metrics.                                             |
| `list`      | List all campaigns with their latest snapshot.                        |
| `budget`    | Adjust a campaign's total budget, or turn platform legs on/off.       |
| `extend`    | Lengthen a live campaign's flight window without changing its budget. |
| `pause`     | Pause a live campaign (budget is retained, not refunded).             |
| `resume`    | Resume a paused campaign.                                             |
| `cancel`    | Cancel a campaign permanently (refunds the unspent budget).           |

### How-tos

#### Launch a campaign end-to-end

```bash
export RUNDOT_BETA_FEATURES=1
rundot marketing prepare  --name spring-push --genre puzzle --art-style chibi
# edit prompts + fill headlines/primaryTexts in campaign.json, then:
rundot marketing generate --name spring-push
rundot marketing submit   --name spring-push
```

The campaign is submitted **paused** and starts spending only after the RUN.world team approves and flights it.

#### Target specific countries and ages

```bash
rundot marketing prepare  --name brazil-mx --countries BR,MX --age-min 21 --age-max 45
rundot marketing generate --name brazil-mx
rundot marketing submit   --name brazil-mx
```

Omit any flag to keep the default for that dimension (default audience: US, ages 18–65).

#### Read performance as a spreadsheet

```bash
rundot marketing stats --name spring-push --days 30 --format csv > spring-push.csv
```

## Share an update (launch packet)

`rundot marketing` is *paid* user acquisition. A **launch packet** is the *organic* counterpart: when you ship an update, RUN.world writes platform-ready posts (X, Reddit, TikTok, Instagram, Discord), mints tracked links so plays from your posts are attributed back to you, and gives you a posting checklist.

{% hint style="info" %}
**Paid vs. organic.** Use `rundot marketing` to *spend* on ads. Use `rundot socials` to share an update to your own audience for free. They're independent — use either or both.
{% endhint %}

### How it works

```
rundot socials prepare       →  RUN.world writes drafts + tracked links for each platform
rundot socials status        →  see the posting checklist (what's posted, what's pending)
rundot socials open <plat>   →  print the caption + a prefilled composer URL to post
rundot socials mark-posted   →  record the URL once you've posted (so @RUN can amplify it)
```

Discord is **auto-posted** server-side (if you've configured a webhook). Every other platform is **composer-assisted**: RUN.world gives you the caption and a prefilled composer link, and you click publish.

All commands run from your game's project directory and use the same game config as `rundot deploy`. Add `--game-id <id>` to target a specific game.

### 1. (One-time) configure your social profile

Your social profile is **per-creator, not per-game** — set it once and it applies to every game you publish. It customizes the generated copy and enables Discord auto-posting. It's optional — `prepare` works without it — but setting at least a Discord webhook is recommended.

```bash
rundot socials profile set \
  --discord-webhook "https://discord.com/api/webhooks/…" \
  --tone "hyped but humble" \
  --hashtags "indiegame,h5games" \
  --cta "Play now,Drop a comment" \
  --footer "Made with RUN.world" \
  --discord-username "yourname" \
  --discord-role-ping "123456789012345678"
```

| Option                         | Purpose                                                                        |
| ------------------------------ | ------------------------------------------------------------------------------ |
| `--discord-webhook <url>`      | Discord incoming webhook. Stored as a write-only secret and used to auto-post. |
| `--tone <text>`                | Copy tone, e.g. `"hyped but humble"`.                                          |
| `--hashtags a,b,c`             | Hashtags to weave into captions.                                               |
| `--cta "a,b"`                  | Preferred calls-to-action.                                                     |
| `--footer <text>`              | Footer appended to copy.                                                       |
| `--discord-username <name>`    | Your handle in RUN's community Discord.                                        |
| `--discord-role-ping <roleId>` | Discord role id to ping on announcements.                                      |

Inspect the current profile any time:

```bash
rundot socials profile show
```

#### Setting up the Discord webhook

In **your** Discord server: **Server Settings → Integrations → Webhooks → New Webhook**, pick the announcement channel, **Copy Webhook URL**, then pass it to `profile set --discord-webhook`. RUN.world stores it as a write-only secret; `profile show` only reports whether it's configured, never the URL itself. Your webhook is stored once on your creator profile and reused across all your games. Without a webhook, Discord is skipped (the other platforms are unaffected).

### 2. Prepare a launch packet

```bash
rundot socials prepare
```

This generates drafts for all platforms against your latest public version. If a Discord webhook is configured, the Discord announcement is **posted immediately**; the rest are left as drafts for you to post.

| Option                                          | Purpose                                                                                  |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `--update latest`                               | Which release to promote. `latest` (default) or a specific `versionNumber`.              |
| `--platforms x,reddit,tiktok,instagram,discord` | Limit to specific platforms (default: all).                                              |
| `--force`                                       | Re-post to Discord even if this version was already auto-posted (idempotent by default). |
| `--json`                                        | Machine-readable output.                                                                 |
| `--game-id <id>`                                | Target a specific game.                                                                  |

Each draft includes three caption variants (`punchy` / `sincere` / `playful`), a tracked link, and any `warnings` (for example, a variant that exceeds X's 280-character limit — the text is never silently truncated).

### 3. Check the posting checklist

```bash
rundot socials status
```

Shows each platform's mode (auto/composer), status (ready / auto-posted / posted), and tracked link.

### 4. Open a composer and post

```bash
# Prints the caption + a prefilled composer URL for X
rundot socials open x

# Pick a caption variant (1–3) and, for Reddit, a target subreddit
rundot socials open reddit --variant 2 --target IndieGaming
```

Copy the printed caption, open the URL, and publish.

To be walked through the checklist one platform at a time, use `next` instead of picking a platform yourself — it prints the next unposted platform with its caption and prefilled composer URL:

```bash
rundot socials next                       # the next unposted platform to act on
rundot socials next --variant 2           # pick a caption variant (1–3)
rundot socials next --target IndieGaming  # subreddit, when the next step is Reddit
```

`next` accepts the same `--packet`, `--json`, and `--game-id` options as the rest of the flow.

#### Per-platform behavior

| Platform               | Behavior                                                                                               |
| ---------------------- | ------------------------------------------------------------------------------------------------------ |
| **Discord**            | Auto-posted via your webhook. No composer to open.                                                     |
| **X**                  | Composer-assisted. `open x` prints a prefilled tweet URL.                                              |
| **Reddit**             | Composer-assisted. `open reddit --target <subreddit>` prints a prefilled submit URL with title + body. |
| **TikTok / Instagram** | No prefilled web composer — copy the caption and post from the app.                                    |

#### Link placement

Captions aren't equally link-friendly across platforms, so each draft tells you where the tracked link goes:

* **inline** — the tracked link goes directly in the post (e.g. X, when it doesn't hurt reach).
* **reply** — post the caption first, then add the link + `@RUN` in a reply, which keeps the main post's reach. `open` prints the reply text for you.
* **search** — the platform isn't clickable (TikTok / Instagram). Put the game name and key art on-screen and use the `Search "{game}" on @RUN` call-to-action so viewers can find it in the RUN app.

### 5. Mark posts as published

Once a post is live, record its URL. RUN.world amplifies marked posts from **@RUN**, and it keeps your checklist accurate.

```bash
rundot socials mark-posted x --url https://x.com/you/status/123
```

| Option            | Purpose                                        |
| ----------------- | ---------------------------------------------- |
| `--url <url>`     | The published post URL (required).             |
| `--packet latest` | Which packet to mark (defaults to the latest). |
| `--game-id <id>`  | Target a specific game.                        |

### 6. Verify which steps are finished

Posting isn't the finish line. A step counts as **finished** once it's both:

1. **Posted** — Discord auto-posts; the others are recorded via `mark-posted` (and, where we can, confirmed live on the platform).
2. **Clicked by someone who isn't you** — at least one click on that step's tracked link from a profile that isn't yours.

```bash
rundot socials verify
```

Each platform reports one of three states:

* **not posted** — nothing's up yet.
* **awaiting click** — it's posted, but no one other than you has clicked the link yet. It's *not* done — RUN.world is waiting for **1 click that isn't you** before the step counts.
* **finished ✓** — posted *and* clicked by someone other than you.

TikTok and Instagram have no tracked link (search-only), so they can be marked posted but aren't finishable here.

```bash
rundot socials verify --json            # machine-readable, includes each row's state
rundot socials verify --packet <id>     # a specific packet
```

### (Optional) Generate a promo image

`promo` renders platform-sized promo art grounded in the game's description, thumbnail, and useful images from its deployed build. Every image returned by the provider is saved; multiple results receive numbered filename suffixes. TikTok and Instagram are search-only, so add the game name when composing the final post.

```bash
# Generate from the game in the local config
rundot socials promo --platform instagram

# Add creative direction without losing the server-selected game references
rundot socials promo --platform x --prompt "focus on the crystal portal" --out ./key-art.png

# Compare a supported Gemini or GPT image model with the same grounding
rundot socials promo --platform x --model gpt-image-2 --out ./gpt-key-art.png
```

| Option            | Purpose                                                                                  |
| ----------------- | ---------------------------------------------------------------------------------------- |
| `--platform <p>`  | Platform to size for (required): `discord`, `x`, `reddit`, `tiktok`, `instagram`.        |
| `--game-id <id>`  | Game id; defaults to the local game config.                                              |
| `--prompt <text>` | Optional creative direction; canonical game grounding still applies.                     |
| `--model <id>`    | Supported Gemini or GPT image model; list choices with `rundot generate image-models`.   |
| `--out <path>`    | Output path; multiple results receive `-1`, `-2`, … suffixes.                            |
| `--json`          | Machine-readable output, including the resolved model and every saved image in `images`. |

### Command reference

All commands live under `rundot socials` and are generally available.

| Command                              | Purpose                                                             |
| ------------------------------------ | ------------------------------------------------------------------- |
| `prepare`                            | Generate a launch packet (auto-posts Discord if configured).        |
| `status`                             | Show the posting checklist.                                         |
| `open <platform>`                    | Print the caption + composer URL for a platform.                    |
| `next`                               | Show the next unposted platform to act on (caption + composer URL). |
| `promo --platform <p>`               | Generate a platform-sized promo image.                              |
| `mark-posted <platform> --url <url>` | Record a published post URL.                                        |
| `verify`                             | Check which steps are finished (posted + ≥1 non-creator click).     |
| `profile set`                        | Configure webhook / tone / hashtags / footer / CTAs / role-ping.    |
| `profile show`                       | Show the current social profile.                                    |

### How-tos

#### Promote an update end-to-end

```bash
rundot socials profile set --discord-webhook "https://discord.com/api/webhooks/…"  # one-time
rundot socials prepare                       # Discord auto-posts; others become drafts
rundot socials open x                         # copy caption, open composer, publish
rundot socials mark-posted x --url https://x.com/you/status/123
rundot socials status                         # confirm the checklist
rundot socials verify                         # which steps are finished (posted + a non-creator click)?
```
