# Hubfluencer — make videos from a prompt

You have access to **Hubfluencer** through an MCP server (`@hubfluencer/mcp`).
Hubfluencer turns a text prompt into a finished, post-ready asset — a fast
single-clip short, a multi-scene ad, an image carousel, or a computer-vision
tracking-overlay clip — and it can run the surrounding content operation (ingest
a product page → plan a campaign → draft a batch → mint a review link → stage a
reminder calendar → learn from performance). Use it whenever the user asks for a
video ad, a short, a TikTok/Reel/Shorts clip, a carousel, or wants to "turn my
product page into a campaign" or "plan a month of ads".

## Creative export contract

- Production prompts (`product_prompt`, `scenario_prompt`, and scene `prompt`) are visual directions. Narration scripts are read aloud verbatim, so `set_narration_script.script` must contain only the exact spoken words—never camera notes, prompt syntax, labels, or scene directions. Never copy a creative brief into `script`; use `generate_narration` or Autopilot unless the user supplied final voice-over copy.
- Do not rely on the video model to spell essential brand names, UI, signs, prices, offers, or CTAs. Put exact copy in Hubfluencer overlays, narration, or supplied ending artwork.
- A short logo position is a preferred corner: the renderer moves it away from headline, captions, CTA, offer, and badge areas. Omit `treatment` so transparent marks default to `throughout` and opaque square lockups default to `end_card`. Use a tight transparent wordmark; use square or full-bleed art as the poster instead. If poster artwork already has its logo/CTA, set `poster_includes_lockup: true` and do not add an `end_card` logo.
- Short posters fill the ending frame: phone-shaped portrait art is cover-cropped, while square, classic portrait, and landscape art is contained over a blurred backdrop. Editor closing images are always cover-cropped. Keep important content in the central 9:16 safe area. An Editor closing image gets no generic CTA, so put the CTA in the artwork or state it in the preceding narration/scene.
- Pass `segments_count` (3–10) to `create_editor_ad` when length matters. It is the exact structural target used by the cost quote and Autopilot launch.
- Each Editor AI scene is 8 seconds. Default a concise social ad to 3 scenes (about 24 seconds) unless the user asks for a longer story.

## Generation lifecycle authority

Every Editor and Short product read includes a non-null `generation_summary`. Treat it as the sole
lifecycle authority even when product projections such as `stage`, `autopilot_status`, `batch_generation_status`, child
statuses, or `latest_render` disagree. Poll only while
`generation_summary.timing.poll_after_ms` is a positive integer, at that server cadence; `null`
means stop polling. Read terminality from `generation_summary.status`, invoke only
`generation_summary.available_actions`, and download only when `download_result` is advertised for
a verified delivery artifact. Product projection fields are display metadata or explicit route fences only;
never derive polling, retry, cancel, or download availability from them.

When a stopped summary advertises `edit_required_input` or `review_spend_cap`, call
`provide_generation_input` with that action and its non-empty `input` object or approved
`max_credits`. For a failed `editor.scene.generate` or `editor.scene.regenerate` summary that
advertises `retry_generation`, call `retry_segment_generation { slug, segment_id }`. Both tools
re-read the product and echo the run/revision/step fences; never call them from product projection fields
or guess a fence. Re-read status after the command.

## Native macOS Studio

When a bearer-gated `hubfluencer-studio` localhost MCP connection is available, use its advertised
tools for the open local project. Start with `get_project_digest` and pass the exact required
`doc_rev` on every call. Read+edit is the default; generation is opt-in and still requires approval
inside Studio. Do not bypass Studio's explicit cloud-context disclosure via direct REST.

## Connecting (the user does this once — never invent or echo a token)

Two ways to connect. Both are done by the user; you never create a token and
never print a token in your output.

- **Option A — access token:** In the Hubfluencer app the user buys credits,
  then goes to **Settings → Access tokens** and creates an access token with the
  **Generate videos** scope (`video:generate` + `video:read`). That token is
  pasted into the MCP config as `HUBFLUENCER_API_TOKEN`.
- **Option B — device link (no copy-paste):** The user runs
  `npx -y @hubfluencer/mcp@0.18.0 login`. It prints a URL and a short code;
  the user approves the connection in the signed-in app. A scoped token is saved
  to `~/.hubfluencer/credentials.json` and the MCP server picks it up
  automatically.

If a tool call fails with **403 insufficient_scope**, the connected token is
missing the `video:generate` scope — tell the user to create a token with the
**Generate videos** scope. Do not try to work around it.

## The simplest path — `make_video`

For almost every request, call **`make_video`** with a `prompt` and a
`save_path`:

```
make_video({
  prompt: "A punchy 15-second ad for a cold-brew coffee brand, energetic, urban morning vibe",
  save_path: "./out/coffee-ad.mp4"
})
```

`make_video` does the whole pipeline: it creates the project, starts generation,
polls to completion, and downloads the finished MP4. Shorts charge atomically
at launch; editor Autopilot authorizes `max_credits` and spends incrementally as
work starts (short = **15 credits**; autopilot editor ad ≈ **28**), and
credits are real money — **$0.99 each** bought singly, less in bulk. Unless the
user already approved the spend, run `make_video({ prompt, dry_run: true })`
first (free; returns the estimate and a draft `slug`), quote the credits +
~dollar cost, and generate only after an explicit yes.

- `kind` defaults to **`auto`** — a fast single-clip **short** for simple
  prompts, a multi-scene **editor** ad for story / multi-scene prompts.
- Override with `kind: "short"` or `kind: "editor"` when the user is explicit.
- If `make_video` returns **`terminal: false`**, the render is still running.
  Call **`wait_for_completion({ slug, kind })`** again. Generation takes a few
  minutes — that is normal, keep waiting, do not restart.
- For paid shorts, status exposes `segments_completed`, `segments_total`, and
  `active_segment_positions`; report those clip-progress signals while waiting.

**Don't ship a bare short.** For a short, pass `headline` (the on-screen TITLE,
≈4 words / ≤40 chars), `subheadline` (secondary title), and a `music_vibe` right
in the `make_video` call — and **always `cta_text`** (a short without a CTA
doesn't convert; add `offer_text` only if there's a real deal). Set
`visual_language` explicitly (`kinetic_creator` is a strong default). These are
SHORTS fields (`theme` applies to both kinds;
`headline`/`subheadline`/`music_vibe` are ignored for editor). If copy is left
blank the server auto-writes the headline + caption beats at generate time
(`skip_auto_text: true` renders a deliberately bare clip). For the user's own
product image, brand logo, or closing card, `make_video` can't attach files —
use `create_editor_ad`'s asset paths for an editor, or the granular tools for a
short. If no title/assets were given, infer a sensible
`headline`/`subheadline` from the prompt, or ask.

## Granular path (control / recovery)

Use these when you need step-by-step control or are recovering from an
interruption:

1. `get_credits` — check the balance before spending.
2. `create_short` or `create_editor_ad` — creates the draft (**0 credits**;
   `create_editor_ad` without `max_credits` also returns a free quote and does
   **not** start generation).
3. `generate_short` (short) or `start_autopilot { slug, max_credits }` /
   re-calling `create_editor_ad { ..., max_credits }` (editor ad) — starts the
   render. A bare `start_autopilot { slug }` only re-quotes; pass `max_credits`
   ≥ the estimate to launch.
4. `wait_for_completion({ slug, kind })` — poll until done.
5. `download_result` — save the finished MP4.

Act on Editor batch recovery only when `generation_summary.available_actions`
advertises `retry_generation` or `cancel_generation`; then call
`retry_editor_batch { slug }` or `cancel_editor_batch { slug }` respectively.
The MCP tools re-read the canonical summary before writing. Raw REST retry
callers send `generation_run_id` and `generation_revision` from that summary;
cancel callers send `generation_run_id`. Product batch fields are display
projections, never command fences or action availability. A stale 409 means
re-read and issue a new command only if the action is still advertised; never
guess fences. Retry targets the exact failed canonical step. Only a scene named
by `current_step` may be repaired before retry. Resolve the child batch before
starting Autopilot again; retrying it does not authorize a new parent run. For
an insufficient-credit blocker, top up or reduce scope and follow the action in
the newly read summary.

Cancellation is fenced to the exact observed work. A paid short cannot be
cancelled at all once `generate_short` starts, and its draft cannot be deleted
until the run is terminal; a failure still refunds its 15 credits. Call
`cancel_segment_generation { slug, segment_id, generation_claim_token }` with
the current token from `get_editor` for an individually generated scene; batch
and Autopilot scenes use their workflow controls. `cancel_autopilot { slug }`
re-reads and forwards the authoritative summary run/start fence, so it stops only
the observed parent chain and fails stale instead of cancelling a successor.
Completed and already charged work remains. A stale fence means re-read state;
never guess a token, run, or start time.

**Minimum sellable short** — check all seven before you spend:

- hook (`headline`) ≤4 words
- `text_beats` = hook → proof → payoff
- `cta_text` set (a short without a CTA doesn't convert)
- product image attached (`set_short_product`) AND described
- poster (`set_short_poster`) or closing lockup (`brand_name` / `closing_claim`)
  carries the ask
- `visual_language` set explicitly (`kinetic_creator` is a strong default)
- iterate: hooks are free to re-render (`update_short` → `rerender_short`,
  0 credits after the first generate)

Iterate the hook before you spend: `generate_hook_variations` writes 3–6 hook
options on the free assist quota. If `headline`/`subheadline`/`text_beats` are
all blank, the server auto-writes the headline + caption beats at generate time
(pass `skip_auto_text: true` to `generate_short` for a deliberately bare clip).

**Never re-roll — iteration is free after the first generate.** The first
`generate_short` (15 credits) buys the footage + music; after that, every
overlay edit re-renders over them for **0 credits**: `update_short` the fields,
then `rerender_short { slug }` (REST: `POST /api/shorts/:slug/rerender`, no
body, `Idempotency-Key` honored, rate limit 10/min).

- **Free to re-render:** `headline`, `subheadline`, `text_beats`, `cta_text`,
  `offer_text`, `badge_text`, `star_rating`, headline/subheadline/accent colors,
  `font_family`, `text_position`, `text_animation`, `closing_claim`,
  `brand_name`, poster (+ `poster_includes_lockup`), `end_card`, logo overlay
  (`set_short_logo` / `short_logo_position` / `short_logo_treatment`).
- **Needs `generate_short` again (15 credits):** `product_prompt`, product
  image, `creative_format`, `visual_language`, `theme`, `music_vibe`,
  `music_instruments`, `language`.

Edits that add/remove the end card (poster, `end_card`) can shift the video
between 12s and 14s. The pinned music bed is reused: when it is shorter than
the export it crossfades into a repeated continuation, then fades at the export
end, without charging for regenerated music.

Iteration recipe: generate once → `update_short` a hook/CTA variant →
`rerender_short` (free) → compare → repeat → keep the winner. **409** = a render
is already running (keep polling); **422 `short_not_ready`** = nothing generated
yet — run `generate_short` first; **422 `short_paid_regeneration_required`** = a
footage/music input changed — run `generate_short` again. A re-render is never a credit error.

`update_short` returns a compact receipt (`updated`, `end_card_mode`,
`resolved_cta_text`, `next`), not the complete Short; read status again for
authoritative state.

**Short creative fields** (`create_short` / `update_short`, also carried by
`make_video`): `headline` (on-screen TITLE — ≈4 words; ≤40 chars hits hardest;
hard cap 80), `subheadline` (≤200),
`visual_language` (kinetic_creator / premium_editorial / cinematic_product /
ugc_realism / startup_explainer / luxury_minimal — drives Veo direction AND the
render's typographic identity), `creative_format` (problem_solution /
mistake_fix / myth_vs_reality / before_after / proof_demo / product_reveal),
`music_vibe` (Upbeat default / Cinematic / Minimal / Luxury / Playful / Jazz),
`theme` (deprecated for shorts — legacy fallback when `visual_language` is
unset), plus `text_position` (top/center/bottom), `text_animation` (**auto**
default / reveal / typewriter / fade_in / pop / bounce / word_stagger /
word_spotlight), `font_family` (**auto** default, or one of 15 ShortFont*
faces) — both default to `auto`, which follows the visual_language's typography;
every concrete value (incl. fade_in / ShortFontSpaceGrotesk) is an explicit pick
that wins — `headline_color` / `subheadline_color` / `accent_color`, `text_beats`
(≤8 caption lines shown one at a time in place of the static subheadline),
`music_instruments` (≤10, each ≤40 chars), and conversion graphics: **always set
`cta_text`** (≤24, e.g. "Shop now" — blank `cta_text` → the server renders a
neutral localized ask (e.g. "Learn more"); a truly CTA-less short is not
currently supported), `offer_text` (≤16) only if there's a real
deal, and `badge_text` (≤24) / `star_rating` (0–5) — opt-in, user-supplied only,
never AI-written; **only ratings/badges you can substantiate — fabricated social
proof is deceptive advertising**. End-card fields: `closing_claim` (≤80 —
end-card claim, falls back to the headline), `brand_name` (≤40 — shown on the
closing brand-lockup slate when no poster is set), `end_card` (`"auto"` default —
poster or brand-lockup slate, 14s; `"none"` — no end card, 12s, loop-style).
Populate at least the title/subtitle and a visual_language. `brand_profile_id`
(from `list_brand_profiles`) on `create_short` seeds blank fields from a saved
brand; `language` is a closed set — `en fr es de it nl pl pt ru zh ja ko ar` (no
region subtags like `en-US`).

Other useful tools: `get_status`, `list_voices` (choose a voice whose normalized
`languages` contains the editor language), `list_projects`
(`include_completed: true` recovers a finished short's slug),
`list_renders { slug }` (every editor render version + presigned URL, thumbnail,
duration, retryable/stale flags, and snapshot hash), `retry_render { slug, render_id }` (re-run a
**failed** editor render from its saved snapshot, **0 credits**).

**Polling:** when you poll with `get_status` (rather than the blocking
`wait_for_completion`), wait exactly the positive
`generation_summary.timing.poll_after_ms` returned by the server; `null` means
stop. A 2–5 minute generation is normal, not a hang. Never re-`POST` a new
generation unless the newly read `generation_summary.available_actions`
advertises `start_generation`.

## Image carousels (sliders)

A **slider** is an image carousel (N still slides + a ready-to-post caption +
hashtags), not a video — so `wait_for_completion` / `download_result` do **not**
apply; poll `get_slider` instead.

1. `create_slider { prompt, mode, slide_count? }` — one prompt → a carousel draft
   (**0 credits**). `mode` is `creative` (story) or `ad_driven` (facts);
   `slide_count` 3–10.
2. `generate_slider { slug }` — render it. **1 credit per slide.**
3. Poll `get_slider { slug }` until `completed`, then download each
   `slides[].image_url` and post them with the returned `caption` + `hashtags`.

Tweak a finished carousel without re-paying: `restyle_slider` (new
template/accent — re-renders all slides) or `edit_slider_slide` (rewrite one
slide's text). Both **0 credits**; poll `get_slider` again.

## Tracking-overlay videos (burn a CV overlay on your own clip)

`create_tracking_video { video_path, save_path?, classes?, color? }` — one shot:
uploads a local clip (**≤ 60s, ≤ 1080p**), burns a computer-vision
object-tracking overlay, renders (**1 credit**, refunded on failure), polls, and
downloads. `classes` = COCO-80 class ids (e.g. `[0]` person, `[16]` dog; omit to
track all); `color` = `[r,g,b]` or `[r,g,b,a]`, each 0–255 (omit for white).

- On `terminal: false` (slow upload / cold GPU), call the tool **again with the
  returned `slug`** to resume — it reuses the in-flight upload (no re-upload, no
  re-charge). Or poll `wait_for_completion({ slug, kind: "tracking" })` then
  `download_result`.
- A completed slug is **final** (its source clip is deleted on success). For a
  variant (different `classes`/`color`), start a **new** project (omit `slug`,
  pass `video_path`).

## Granular control (author the ad yourself, no autopilot)

When the user wants you to direct every step — the scenario, the exact number of
scenes, each scene's prompt, the narration — drive the editor by hand instead of
autopilot. Autopilot (`create_editor_ad`) stays the default; this is the
controlled path, and it is fully agent-driven (no human approval step):

1. `create_editor_draft { product_prompt, language?, theme?, voice_id?, export_aspect_ratio?, project_intent? }`
   — creates an editor project **without** starting autopilot (0 credits).
2. `generate_scenario { slug, segments_count? }` (1 AI assist) **or**
   `set_scenario { slug, scenario_prompt }` (free). `segments_count` is 3..10.
3. `get_editor { slug, response_format: "concise" }` — **review** the scenario, `segments[]`,
   `narration_status`, `current_audio`, `current_music`, `latest_render`, and
   `ai_assist_quota` before you spend anything. Omit the format only for the
   backward-compatible full payload.
4. `set_scene_count { slug, count }` — 1..20; only removes trailing
   un-generated scenes, never a completed or in-progress one. Each AI scene is a
   fixed **8s**, so this sets the ad's length (`count × 8s`); uploaded clips keep
   their own native length.
5. `set_segment_prompt { slug, segment_id, prompt }` per scene (free, 1..2000
   chars).
6. `generate_segment { slug, segment_id }` one at a time, **or**
   `generate_all_segments { slug }` to run pending scenes **sequentially** so
   visual continuity carries forward. 5 credits per scene; stop on the first 402.
7. `set_narration_script { slug, script }` (free, ≤10000 chars) **or**
   `generate_narration { slug }` (1 AI assist). AI narration requires every
   scene completed with a known positive duration and at least one scene without
   retained original audio; an exact retry deduplicates for free. Then write a
   music `description` (≤1200 chars).
8. `generate_voice { slug, voice_id }` (3 credits; `voice_id` **required** —
   pick one from `list_voices`) and
   `generate_music { slug, description, mood?, genre?, tempo?, instruments? }`
   (5 credits). `instruments` is an array of up to 10 non-empty strings (≤80
   chars each), never one comma-separated string.
9. `render { slug }` (0 credits — charges ungenerated or previously failed scenes) →
   `wait_for_completion({ slug, kind: "editor" })` → `download_result`.

An editor `video_url` can be the last delivered MP4 after later edits. Treat it as current only
when normalized status says `ready:true` and `stale:false`; `stale:true` requires regeneration,
and absent/unknown staleness is not proof that the old render matches the live timeline.

## Bring your own media (local uploads)

An **editor** project can use the user's **own local files** — video clips on the
timeline, or a product / closing / logo image — all **0 credits**:

1. `upload_video { slug, file_path, add_to_timeline? }` — uploads a local clip
   and waits until it is processed (`status: ready`). With `add_to_timeline`
   (the default) it also appends the clip as a finished scene; otherwise place it
   later with `add_segment_from_upload { slug, upload_id }`, or set a specific
   scene with `use_asset_for_segment { slug, segment_id, upload_id }` (also takes
   `source_segment_id` to reuse a finished scene).
2. Images: `set_product { slug, file_path, description? }`,
   `set_closing_image { slug, file_path }`, `set_logo { slug, file_path }`.
   For ordinary/new projects, the Editor product photo is a required
   product-identity reference for every generated AI scene; its confirmed
   description grounds scenario/narration, and the exact photo becomes the
   closing image unless separately authored. Small generated label text can
   still vary. Replacing/removing it or changing its identity description
   invalidates completed AI scenes; re-read/retry on `editor_references_changed`.

A **short** can also take an optional product reference image — `set_short_product { slug, file_path,
description? }` — an exact deterministic logo overlay — `set_short_logo { slug, file_path, position?, treatment? }` (≤5 MiB; never sent to the video model) — and an end-card poster —
`set_short_poster { slug, file_path, poster_includes_lockup? }` (a closing still
that extends the render to 14s; set `poster_includes_lockup: true` if the poster
already contains brand name/CTA/badges, so the overlaid claim/CTA is suppressed
and doesn't collide). Same 0 credits; product/poster images are jpeg/png ≤20 MiB (20,971,520 bytes).

Video: mp4 / mov / webm / mkv, **≤500 MB decimal (500,000,000 bytes), ≤5 min**;
multipart starts at **50 MiB (52,428,800 bytes)**. Editor product images: jpeg/png,
**≤8 MiB (8,388,608 bytes)**. Editor closing and short images: jpeg/png,
**≤20 MiB (20,971,520 bytes)**. Editor and short logos: jpeg/png, **≤5 MiB (5,242,880 bytes)**. No WebP.
A freshly uploaded clip must finish processing before it
can be placed — `upload_video` waits for you. **Only upload files the user
explicitly gave you** — never read arbitrary local paths.

## AI assists

Helper calls where the model writes content for you draw from a **free daily
quota of 20** per account, separate from credits. Check it with `get_ai_assists`;
buy more with `unlock_ai_assists` (**1 credit → +10 assists**).

- **Consume 1 assist:** `generate_scenario`, `enhance_prompt`,
  `suggest_next_scene`, `suggest_music_prompt`, `generate_narration`
  (per-segment regeneration is REST-only —
  `POST /api/editor/:slug/segments/:id/regenerate-narration`, not an MCP tool),
  `generate_short_text`.
- **Free writes:** `set_scenario`, `set_segment_prompt`, `set_narration_script`,
  add/reorder segments, `apply_scenario`, `render`.
- **Cost credits (not assists):** segment 5, regenerate 4, voice 3, music 5.

**Narration, voice, and music are three separate meters — don't conflate them:**
*drafting* the narration script (`generate_narration`) costs **1 free-quota
assist** (its "free" endpoint summary means free of *credits*, not of *quota*);
*synthesizing* the voice-over (`generate_voice`) costs **3 credits**; *generating*
music (`generate_music`) costs **5 credits**. A **429 `ai_assist_quota_exceeded`**
means you are out of assists (unlock once or write the content yourself — never
loop); a **402 `credits_insufficient`** means you are out of credits (stop).

## Delegate the whole content operation (plan → draft → review → schedule → measure)

Beyond one asset, you can run the surrounding content operation. **Everything
through drafting is $0** — the planning tools draw the free AI-assist quota (not
credits), and materializing a plan produces **0-credit drafts**.
**Generating/rendering each draft is a separate, explicit, paid step** — create
the drafts, then generate only the ones the user approves; never auto-spend a
month of content. The whole loop is agent-scoped (`video:read` +
`video:generate`); nothing needs `account:admin`.

1. **Onboard a product.** `extract_product_page { url }` pulls facts + candidate
   image/logo URLs from a public page (SSRF-guarded server-side, 0 credits).
   Persist the identity once so later drafts inherit it:
   `create_product_profile { name, ... }` + `create_brand_profile { name, ... }`
   (`list_*` / `update_*` manage them). Pull the primary image + logo in with
   `import_product_image { product_profile_id, url, link_as }` (`primary_image`
   default, or `logo`).
2. **Plan (free quota).** `plan_campaign { product_profile_id | product }`
   proposes angles/formats + a per-item credit **estimate**;
   `generate_hook_variations { ..., brand_profile_id? }` writes 3–6 draftable
   hook options (each a script + caption + hashtags + suggested format). On 429,
   `unlock_ai_assists` once or write the copy yourself.
3. **Pack & materialize ($0).**
   `create_campaign_pack { title?, product_profile_id?, brand_profile_id? }` is
   the container; `add_pack_variations { pack_id, variations, materialize: true }`
   persists the picks and turns each into an editable **draft** (≤12/call).
   `materialize_pack_item { pack_id, item_id }` (re)tries one;
   `get_campaign_pack { id }` / `list_campaign_packs` show each item's status +
   **draft slug** + estimate. Shortcut:
   `create_campaign { url | product_profile_id, brand_profile_id?, item_count?, formats? }`
   runs steps 1–3 in one call.
4. **Generate per draft (credits — a deliberate step).** For each approved draft
   slug: `start_autopilot { slug, max_credits }` (video — a bare
   `start_autopilot { slug }` only re-quotes; pass `max_credits` ≥ the estimate
   to launch) or `generate_slider { slug }` (carousel). Nothing above this line
   spent video credits.
5. **Review (no-login).** `create_review_link { video_result_id | slider_id }`
   mints an expiring page where a human approves / requests changes / comments
   with **no account**. The token + URL come back **once** — surface them
   immediately. `list_review_links` shows activity; `revoke_review_link { id }`
   kills a link. Gate spend or posting on approval.
6. **Calendar (reminder-only).**
   `schedule_post { video_result_id, platform, scheduled_at }` stages a calendar
   entry that pushes a **reminder to the user** at the due time — it does **not**
   auto-publish. `list_scheduled_posts` is the calendar; `reschedule_post` /
   `cancel_scheduled_post` manage it; after the user posts,
   `mark_scheduled_posted { id }`. All 0 credits, all `video:generate`.
7. **Measure → double down.**
   `record_performance { item_id, platform, views, ... }` logs a snapshot for a
   pack item; `set_item_attributes { item_id, ... }` tags its creative.
   `get_recommendations` then returns a **cautious**, sample-size-aware read of
   your own best hook/format — **present its `confidence` + `note` verbatim;
   never claim causation, ROAS, or attribution.**
   `make_more_like_winner { item_id }` spins fresh hooks from a proven item →
   feed a chosen one back into `add_pack_variations` (step 3).
8. **Recurring shows.**
   `create_series { name, template, brand_profile_id?, product_profile_id? }`
   defines a repeating format; `plan_series_episodes { series_id }` drafts
   upcoming ideas (free quota); `materialize_episode { series_id, episode_id }`
   turns one into a 0-credit draft; `get_series_dashboard { series_id }` shows
   the upcoming / drafted / published lanes; `mark_episode_posted` closes the
   loop after a manual post.

**Reusable assets.** `list_assets` / `get_asset_quota` /
`upload_asset { file_path, ... }` manage a cross-project media catalog (images +
videos) so a clip/logo can be reused across projects. These need an **active
subscription** (402 `subscription_required` otherwise) and cost **0 credits**;
`upload_asset` reads are confined to `HUBFLUENCER_INPUT_DIR`.

## Credits

- **A credit is $0.99** bought singly; bulk packs and subscriptions cost less
  per credit — translate credit counts into dollars for the user (short 15cr
  ≈ **$15 max**; autopilot ad ~28cr ≈ **$28 max**).
- **Confirm before spending:** quote the estimated credits + approximate dollar
  cost and get an explicit yes before the first charging call; for a batch,
  confirm the total once up front. Skip only when the user already approved
  that specific spend.
- Any draft create / edit / reorder (`create_short` / `create_editor_ad` /
  `create_editor_draft` / `create_slider` / campaign & series drafts):
  **0 credits**.
- Rendering a short: **15 credits**.
- Granular editor steps: generate segment **5**, regenerate **4**, **batch
  per-segment (batch of ≥3) 4** (so `render` bills any still-ungenerated scenes
  at the batch rate), voice **3**, music **5**, render **0** (charges ungenerated
  or previously failed scenes). Unlock AI assists = **1 credit → +10**.
- Multi-scene editor ads (autopilot) ≈ **28** — preflight the exact cost with
  `GET /api/editor/:slug/autopilot/cost` (or just check `get_credits`).
- Carousel render: **1 / slide** (3–10 slides); restyle / per-slide edit: **0**.
  Tracking render: **1** (refunded on failure).
- Onboarding / planning / packs / series / calendar / performance / review /
  profiles: **0 credits** (planning tools draw the free daily assist quota).
- Always check the balance with `get_credits` before a render the user did not
  explicitly pre-approve.

## Failure handling

- **402 credits_insufficient** — state the **required** vs **available** credits
  and **stop**. Do not loop or retry. Tell the user to buy more credits in the
  app.
- **429 ai_assist_quota_exceeded** — the daily AI-assist quota is used up (NOT a
  credit problem). `unlock_ai_assists` once or write the content yourself; never
  loop.
- **409 already-running** — a render is already in progress. **Keep polling**
  with `wait_for_completion`; do **not** start a new one.
- **403 insufficient_scope** — the connected token lacks `video:generate` (see
  Connecting above).
- Long waits are normal. A render taking a few minutes is expected, not a
  failure.

## Result links

Result URLs are **presigned and short-lived (~24h TTL)** — they are not
permanent links. Download the MP4 promptly via `download_result` (or the
`save_path` you passed to `make_video`).

## Out of scope — do not auto-publish

Publishing directly to TikTok / Instagram requires a human-linked social account
(interactive OAuth) and `account:admin`, which agent tokens never carry. **Do
not** attempt to auto-publish. When the asset is done, return the finished MP4
(or carousel images) **plus a ready-to-paste caption** so the user can post it
themselves. You **can** stage a **reminder-only** calendar with `schedule_post`
(it pushes a reminder to the user, never a publish) and close the loop with
`mark_scheduled_posted` after they post — that's the supported "run my calendar"
path.

**Label synthetic media.** These renders are AI-generated: TikTok requires an
AI-generated content label (and attaches C2PA content credentials since 2025),
and Meta requires disclosure for AI-generated/altered content in paid ads.
Whenever you hand over an export or stage a posting reminder, tell the user to
enable the platform's AI label.
