> 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.27.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.27.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 (generated by default)
  vertical/              # 9:16 source creatives (opt-in via --kind vertical)
  landscape/             # 1.91:1 source creatives (opt-in via --kind landscape)
  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                                                                                                                                                                                               |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--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. |
| `--daily 35`                       | Initial per-leg daily budget in USD ($5–$500). Omit for the network default ($50/day app, $20/day web).                                                                                               |
| `--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.                                                                                                                                                      |

For every campaign type, `prepare` warns when the resolved per-leg daily budget is at or below \~$50/day and may gather too little signal during Meta's learning phase. After creatives exist, `generate` separately warns when the budget falls below \~$15/creative/day.

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` + `square` only.                                 |
| `--variants 4`                                         | How many variants to generate (default 4 for `square`, 3 otherwise; capped per kind).                                                                        |
| `--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 you set `--daily` and the budget ÷ variants falls below \~$15/creative/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.

### 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**. 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.                                                                              |
| `flighted`                | Approved and live. Spending against its daily budget.                                                                        |
| `rejected`                | Not approved. Not spending.                                                                                                  |
| `paused-by-creator`       | You paused it. Not spending.                                                                                                 |
| `auto-paused-performance` | Auto-paused by the performance guard for clear underperformance (unspent budget refunded). `stats`/`status` show the reason. |
| `cancelled`               | Permanently stopped.                                                                                                         |

### 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 daily budget (USD/day per platform)
rundot marketing budget --name spring-push --platforms ios,android --daily 200

# Pause spending (you can resume later)
rundot marketing pause --name spring-push
rundot marketing resume --name spring-push

# Permanently stop a campaign
rundot marketing cancel --name spring-push
```

{% 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.
* Ad spend is drawn from your prepaid credit balance — the platform never fronts your ad cost. If your balance can't cover the next day's budget, the campaign is paused automatically.
* Ad spend uses the provider-cost conversion of **1,000 credits per $1 USD**, before the ad-spend markup. 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 or cancelling a campaign refunds the unspent portion of its prepaid budget.

If a budget change 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 | Generated by default    |
| --------- | ------------ | ----------- | ------ | ------------- | ------------ | ----------------------- |
| Square    | 1:1          | 1080 × 1080 | PNG    | 8 MB          | 4            | yes                     |
| Vertical  | 9:16         | 1242 × 2208 | PNG    | 8 MB          | 4            | no (`--kind vertical`)  |
| Landscape | 1.91:1       | 1200 × 628  | PNG    | 8 MB          | 4            | no (`--kind landscape`) |

Only the `square` creative is required at submit; `vertical` and `landscape` are optional.

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 live campaign's daily budget.                  |
| `pause`     | Pause a live campaign (refunds unspent budget).         |
| `resume`    | Resume a paused campaign.                               |
| `cancel`    | Cancel a campaign permanently (refunds 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)?
```
