> 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.23.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.game generates the source ad creatives for you, and a campaign is submitted to Meta (Facebook / Instagram) 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.23.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.game generates the assets, and you submit it. Submitting **does not start spending money** — every campaign is reviewed by the RUN.game team and only goes live once it's approved ("flighted").

```
rundot marketing prepare   →  scaffold a campaign + write its prompt
rundot marketing generate  →  RUN.game generates source ad creatives (AI)
rundot marketing submit    →  upload + submit for review (created PAUSED)
        │
        ▼
  RUN.game 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 web only** — app-install (iOS/Android) is an explicit opt-in.                                                                                          |
| `--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 `--campaign-type purchase|value`, `prepare` warns when the daily budget is too low to clear Meta's learning phase.

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.game 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,https://…/art.png` | Reference images (local files or HTTPS URLs) to guide style.                                                                 |
| `--regen square/0.png`                     | Regenerate a single asset.                                                                                                   |
| `--new-seed`                               | Use a fresh random seed when regenerating.                                                                                   |
| `--force`                                  | Overwrite existing assets.                                                                                                   |

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: Meta applies one headline × primary-text set across every creative, so you author it once. `generate` seeds `headlines` and `primaryTexts` arrays into **`campaign.json`** from your game's name and description — edit them there (up to 5 of each; headline ≤ 40 chars, primary text ≤ 125) before submitting.

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 the campaign if the resolved copy is still a placeholder, too long, or has more than 5 options.

### 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.game 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.game 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.game **credits**. A few things to know:

* A campaign never spends until it's approved and flighted by the RUN.game 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.
* 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.

#### 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.game 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.game 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 %}

{% hint style="warning" %}
**Beta feature.** Like `marketing`, `socials` commands are hidden until you enable beta features for your shell session:

```bash
export RUNDOT_BETA_FEATURES=1
```

{% endhint %}

### How it works

```
rundot socials prepare       →  RUN.game 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.game 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.game" \
  --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.game 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.game 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.game 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 a platform-sized promo image you can attach to a post. TikTok and Instagram are search-only, so a strong image with the game name on-screen matters most there.

```bash
# Size a promo image for a platform (required), described by game name…
rundot socials promo --platform instagram --name "My Game"

# …or with a full prompt (takes precedence over --name), to a custom path
rundot socials promo --platform x --prompt "neon arcade key art, bold title" --out ./key-art.png
```

| Option            | Purpose                                                                           |
| ----------------- | --------------------------------------------------------------------------------- |
| `--platform <p>`  | Platform to size for (required): `discord`, `x`, `reddit`, `tiktok`, `instagram`. |
| `--prompt <text>` | Full image prompt. Takes precedence over `--name`.                                |
| `--name <text>`   | Game name — builds a default promo prompt when `--prompt` is omitted.             |
| `--out <path>`    | Output file path (defaults to `./promo-<platform>.png`).                          |
| `--json`          | Machine-readable output.                                                          |

### Command reference

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

| 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
export RUNDOT_BETA_FEATURES=1
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)?
```
