Command Deck · v1.7

Costs, policy, and catalogue intelligence.

ChannelHelm started as a pipeline that turns one video into a publishing kit. v1.7 adds the business layer on top of the pipeline: know what everything costs, automate routine approval decisions by policy, take in whole backlogs at once, tune the system per business vertical, and query what the entire catalogue knows — not just what one package says.

Shipped All five features below are live: Helm Ledger (cost tracking + budgets), auto-approve rules, bulk backlog ingest, brand vertical presets, and Helm Atlas (the cross-package topic/entity index at /atlas).
The five features

One wave, one theme: run it like a business.

Each feature answers an operator question the pipeline alone couldn't: what does this cost me? Do I really have to click approve 40 times? How do I onboard a 200-video backlog? Why does a podcast brand get video-shaped output? What has this channel already covered?

LEDGER

Costs & budgets

Every LLM call, image, and render lands in an append-only ledger. Per-brand monthly budgets gate new generation spend.

POLICY

Auto-approve rules

Per-brand, per-asset-type trust thresholds. High-confidence assets skip review — with a full audit trail.

INTAKE

Bulk ingest

Paste up to 50 URLs or point at a folder. Dedupe + per-line outcomes; one bad line never sinks the batch.

FIT

Vertical presets

Six presets tune the default profile, prompt tone, and asset fan-out so output matches the business, not a generic creator.

ATLAS

Catalogue intelligence

Topics + entities from every analysis, indexed and queryable. Coverage signals close the loop back into planning.

💰

Helm Ledger — cost tracking & budgets

Know what every package, brand, and month actually costs — and cap it.

Why it exists

Once hosted models enter the provider mix, a pipeline that fans out a dozen LLM calls plus image generations plus renders per video has a real, invisible bill. Cost awareness is the difference between "this feels expensive" and "this brand spent $4.20 this month, mostly on thumbnails." And budgets turn awareness into policy: a brand can never silently overrun.

What gets recorded

Every LLM call, generated image, and clip render lands in the append-only cost_events table:

FieldMeaning
categoryllm · image · render
brand_id / package_id / asset_id / job_kindAttribution — best available at the call site
input_tokens / output_tokensLLM token usage from the provider response
image_count, duration_msImage events / render wall-clock
cost_usdPriced cost (see pricing below)

Capture happens at the single LLM choke point (complete() in the provider integration), at Runware image generation (provider-reported USD), and per rendered clip (wall-clock). Recording is best-effort: a ledger failure never fails the work that incurred the cost.

Pricing — COST_RATES_JSON

Local models cost $0 (and still record token volume — you see usage, not just spend). Hosted models are priced by the COST_RATES_JSON runtime setting on /settings: a JSON array of rules where match is a case-insensitive substring of the model name and the first hit wins.

// COST_RATES_JSON — USD per million tokens, first match wins
[
  { "match": "gpt-4o-mini", "input_per_mtok": 0.15, "output_per_mtok": 0.60 },
  { "match": "sonnet",      "input_per_mtok": 3.00, "output_per_mtok": 15.00 }
]
Approximations Built-in defaults cover common hosted models, but they are approximations — verify against your provider's current price list and override in COST_RATES_JSON.

Budgets — what's gated, what's never gated

Set "Monthly budget (USD)" on a brand. When the UTC calendar month's spend reaches the budget, new generation spend stops:

SurfaceAt budget
generate_asset workergated fails fast with a readable error
AI thumbnail generationdegrades falls back to the free ffmpeg frame-extraction path
Studio interactive regenerategated refuses with the budget message
Pipeline stages (transcribe / visual / fuse / analyze)never gated
Dispatch / publishingnever gated

The principle: a budget pauses drafting new copy, never understanding videos or shipping approved work. Nothing you've already approved gets stuck behind a cap.

Where you see it

/performance"Costs — month to date": per-brand spend by category, token volume, render minutes, budget status (warns at 80%), plus the top packages by spend.

Auto-approve rules

Stop clicking approve on assets you always approve anyway.

Why it exists

After a few dozen packages you know which asset types you trust. If you've approved every high-confidence LinkedIn post for a brand without edits, the review click is pure friction — and at multi-brand scale it's the bottleneck. Auto-approve turns that earned trust into an explicit, per-brand policy instead of a habit.

How scoring works

Rules live on the brand form (brands.auto_approve_rules) as per-asset-type thresholds:

[ { "asset_type": "linkedin_post", "min_score": 0.8 } ]

When generate_asset produces an asset, it derives a quality score from the payload — in order of preference:

SignalScore
Explicit confidence labelhigh = 0.9 · medium = 0.6 · low = 0.3
Else: best options[].scoreas scored
Else: top-level scoreas scored
No quality signal at allnever auto-approves

If the score clears the rule's threshold, the asset skips ready_for_review and routes through the existing auto-dispatch path.

The audit trail

Every auto-approved asset is stamped with payload.auto_approved = {score, threshold, reason, at} — so you can always answer "why did this go out without me?" after the fact. Policy without auditability would be a footgun; this is the receipt.

Hard limits

The evaluation logic is a pure function (src/lib/auto-approve.ts) and is unit-tested.

📦

Bulk backlog ingest

Onboard a whole channel — or a whole client — in one paste.

Why it exists

One-at-a-time intake is fine for a weekly upload; it's hopeless for agency-scale onboarding or mining a years-deep back catalogue. Bulk ingest makes "take this entire channel" a single operation on /sources/new"Bulk ingest (backlog)", with two input modes:

ModeLimitHow it works
Paste URLs 50 / run One YouTube URL per line (# comments allowed). Lines are validated, deduped against each other AND against the brand's existing sources, then each runs the normal source → package → ingest flow.
Local folder 25 files / run Absolute path on this Mac; mp4 / mov / webm / m4v / mkv / mp3 / m4a / wav. Each file is copied into MEDIA_ROOT/{brand}/{src}/original.{ext} and ingested like an upload. Dedupe is by title (filename without extension) per brand.

Per-line outcomes

Every line gets its own verdict — created (with a package link), duplicate, invalid, or error. One bad line never sinks the batch. Each created source enqueues with the standard idempotency key (ingest:{sourceId}), so re-running a batch is safe.

Pairs with Backlog Revival The cheap path for an old channel: bulk-ingest it under the transcription_only profile (audio-only, no visual phase), then revive selected packages at a richer profile once you know which ones deserve it.
🎯

Brand vertical presets

A podcast brand shouldn't get video-shaped output. Now it doesn't.

Why it exists

A generic pipeline writes generic copy. A podcaster never wants "as you can see on screen…" in a post; a webinar brand cares about LinkedIn and article briefs far more than vertical clips; an agency client needs conservative, brand-safe phrasing by default. Verticals encode that fit once, per brand, instead of the operator correcting it in review forever.

The six presets

PresetTuned for
YouTube creatorThe default video-first shape — full packaging, Shorts, social fan-out.
PodcasterAudio-first: fast_audio_only by default, tone guidance forbids referencing on-screen visuals, skips long_clip_plan.
Webinar / B2BFronts linkedin_post + article_brief at higher queue priority — the derivatives that matter ship first.
EducatorInstructional tone; emphasis on chaptered, reference-style derivatives.
Agency clientBrand-safe, conservative copy by default — review-friendly output for someone else's brand.
Local businessLocal-audience tone and the network mix a storefront actually uses.

What a preset actually tunes

Each preset (catalogue in src/lib/verticals.ts, stored as brands.vertical) adjusts three things — all advisory, all overridable:

Brands without a vertical behave exactly as before — generic is still a valid choice.

🗺️

Helm Atlas — cross-package intelligence

Your catalogue becomes queryable knowledge, not a list of packages.

Why it exists

Each package knows what it covers; nothing knew what the channel covers. After 50 videos, "have we done this topic? when? how hard?" was a memory exercise. Atlas indexes the topics[] and entities[] the pipeline already extracts per package into atlas_entrieszero additional LLM calls — and makes the whole catalogue queryable at /atlas.

What you can query

Coverage signals

SignalDefinitionWhy it matters
Revisit candidates Topics covered ≥2× whose last coverage is >90 days old Proven themes going stale — your safest follow-up bets.
One-shot topics High-weight topics covered exactly once Strong material with no follow-up — unmined depth.

Promote to idea

One click writes a ranked idea onto the Plan board (evidence.source = 'atlas_coverage_gap', provenance.model = 'atlas.v1'), closing the loop from catalogue knowledge back into pre-production. The Atlas isn't a report — it feeds the Idea Board.

Sync & backfill

Sync is automatic at the tail of analyze_intelligence (delete + insert per package, idempotent — Backlog Revival refreshes it too). To index a catalogue that predates v1.7, run the one-time backfill:

pnpm tsx scripts/backfill-atlas.ts
Under the hood

Data model, in one breath.

Migration 0027_verticals_bi_wave.sql adds two tables — cost_events (cst_ ids) and atlas_entries (atl_ ids, unique on (package_id, kind, normalized_label)) — plus three brand columns: vertical, monthly_budget_usd, and auto_approve_rules. All five features are covered by pure-function test suites (pnpm test).

Where next See the roadmap for the full release history, the storage lifecycle for what stays on disk, and the A/B testing guide for the other half of the feedback loop.