# Virlo API: Agent Playbook You are an AI agent using the Virlo API to research short-form social media (TikTok, YouTube Shorts, Instagram Reels, Meta Ads) on behalf of a human user. Virlo is a data provider covering every subject: any niche, brand, creator, sound, or trend your user cares about can be researched with the same primitives. This guide tells you which endpoints to call for a given user intent, how to maximize the volume and relevance of data returned, and how to interpret what comes back. Human version: https://dev.virlo.ai/docs/agent-playbook ## 1. Protocol essentials - Base URL: `https://api.virlo.ai/v1`. Auth: `Authorization: Bearer virlo_tkn_…` (only the Authorization header works; `x-api-key` and `?api_key=` are not supported). - Params and most response fields are `snake_case`. Responses are wrapped: `{ "data": ... }`. Exception: webhook management (`/v1/webhooks…`) returns a bare array/object. - **Naming note:** `publish_date` is the canonical field everywhere. Satellite and outlier responses also carry a deprecated camelCase `publishDate` alias; read `publish_date`. Video `publish_date` values without a timezone suffix are UTC. - **Nullable fields:** `author_id` on digest videos may be `null` when the author row isn't linked yet. Image fields (`cover_url`, `thumbnail_url`, `avatar_url`) are full URLs or `null`; some point to HEIC files, which most non-Safari browsers can't render. - **Pagination differs by endpoint**, and a param an endpoint doesn't declare is either rejected with a 400 or silently ignored. Most lists take `page` (1-indexed) + `limit` (values over 100 are clamped to 100). `/satellite/runs*` take `limit` + `offset`. `/v1/agents/:id/videos|slideshows|ads|creators/outliers` return `{ total, limit, offset, }`. `/v1/agents/:id/sounds|hashtags` use the `pagination` envelope, where `total` is a lower bound while more rows exist, so page on `has_next_page`. - Billing: prepaid, 1 credit = $0.01. `X-Cost` (dollars) and `X-Credits-Used` (credits) are sent on every **successful** response ("0.00"/"0" on free calls) and are absent on errors. `X-Balance-Remaining` and `X-Credits-Remaining` appear **only on charged responses**. Rejected requests (4xx/5xx) are never charged, and an accepted request is charged even when it finds nothing, with a few automatic refunds: a creator, sound, or hashtag lookup that fails (for example, the handle doesn't exist, or the lookup stalls and is closed out as failed) is refunded in full, and so are a trend, audience, or Data Intelligence surcharge that delivered nothing and a post collection that finds no posts (§4, §10). An accepted one-shot agent or video outlier check is not refunded if the job later fails. `X-Cost` always shows the full charge; a refund is its own usage row, echoed as `credits_refunded` (credits) on a lookup's result or failed status, and as a top-level field on the saved run (`GET /satellite/runs/:run_id`), failed runs included. Recurring agent runs and tracking checks are billed in the background after they succeed and never show up in `X-Cost`. Over MCP, tool results carry no cost fields at all: compare `get_credit_balance` before and after. `402` = balance too low (top up at https://dev.virlo.ai/dashboard/billing). **Re-reading and polling a resource you already created is free**, with one exception: `GET /v1/agents/:id/hooks` costs $0.25 unless the agent has Data Intelligence or no analyzed hooks yet (`coverage.videos_with_hooks` is 0). - Errors carry a stable `code` (branch on it, not on `message`): `400` validation_error / invalid_date_range / unknown_region, `401` missing_api_key / invalid_api_key, `402` insufficient_credits, `404` not_found (wrong id, or another team's resource), `409` conflict, `429` rate_limit_exceeded (honor `Retry-After`; not a credit problem; 400 responses count toward the limit), `5xx` retry with backoff. Python's built-in urllib default User-Agent is blocked at the edge (403 "error code: 1010"); requests, curl, axios and node work. - **Agent status poll:** `GET /v1/agents/:id` returns config + `analysis`/`analysis_data` + `pending_jobs[]` + `finalized`, plus in-flight run progress (`progress_pct`, `stage` collecting → analyzing → finalizing → completed, `eta_seconds`). Pull the heavy arrays from the dedicated sub-paths (`/videos`, `/slideshows`, etc.) rather than expecting them inline. - **Agent summary (best first read):** `GET /v1/agents/:id/summary` (free) is a compact one-call digest: run rollup, collection counts, top creators + trends, and an analysis headline. Reach for it first once `finalized: true`, then fan out to the sub-paths. Over MCP it is `data_type: "summary"` and additionally returns a `next_actions[]` list of suggested follow-ups. ## 2. Route the user's intent Pick the workflow by what the user actually wants. Don't run more than you need; don't run less. | User intent sounds like | Workflow | Core call | Cost | |---|---|---|---| | "What's working in X right now?" / research or analyze a niche or keywords / competitor content | **Content Research Agent, one-shot** | `POST /v1/agents` with `is_recurring: false` | $0.50, charged at creation (+$1.00 with intelligence) | | "Keep watching X for me" / monitor or track a niche over time / recurring reports | **Content Research Agent, recurring** | `POST /v1/agents` with `is_recurring: true` + `cadence` | $0.50 per run, starting with the first run, which begins right away (+$1.00 per run with intelligence) | | "Research this in Spanish / Thai / any non-English niche" / global research | **Agent, all languages** | `POST /v1/agents` with `english_only: false` + keywords/intent in that language | same as any agent | | "Show me the collected content uploaded from Canada / Russia / Australia" | **Region filter (read time, beta)** | `GET /v1/agents/:id/videos?region=CA` (also `/slideshows`) | free | | "Tell me about this creator" / influencer vetting / partner discovery | **Satellite creator** | `GET /satellite/creator/:platform/:username` | $0.50 (+$0.50 trend analysis, +$0.25 for the lookup's own AI read of each video, which is separate from the agent add-on, +$0.50 audience); up to $1.75 | | "Why did this video pop?" / benchmark one video vs the channel | **Video outlier** | `POST /satellite/video-outlier` | $0.50 | | "What's happening with this sound?" / audio strategy | **Satellite sound** (TikTok & Instagram; not YouTube) + `/sounds/*` | `GET /satellite/sounds/:platform/:music_id` | $0.50 to $1.00 | | "What's working under #X?" / hashtag deep-dive / tag research | **Satellite hashtag** (TikTok, Instagram, YouTube Shorts) | `GET /satellite/hashtags/:platform/:hashtag` | $0.50 to $2.50 | | "Track this creator/video over time" / lifecycle, growth charts | **Tracking** | `POST /tracking/creators` or `/tracking/videos` | $0.25 to start (covers the first check), then $0.25 per check | | "Who watches this creator?" | **Audience** | `audience_demographics=true` on Satellite, or tracking audience-refresh | $0.50 per fresh snapshot | | "What's trending today, broadly?" | **Digests** | `GET /trends/digest`, `GET /videos/digest`, `GET /hashtags` | $0.05 to $0.25 | | "What's starting to take off?" | **Emerging trends** | `GET /trends/emerging` (new and rising trends, momentum-ranked) | $0.25 | | "What's trending in the US / UK / Australia / Singapore?" | **Regional trends** | `GET /trends?region=us` (`gb`, `au`, `sg`; default `global`; `GET /trends/regions` lists codes, free) | $0.25 | | "Find me a sound / what audio is breaking out?" | **Sounds** | `GET /sounds/trending`, `/sounds/breakout`, `/sounds/search?q=` | $0.10 to $0.25 | | "What hooks are working (in X)?" / "give me hooks" / "the strongest hooks in my niche" | **Hooks** | `GET /v1/agents/:id/hooks` when the user has an agent covering the niche, else `GET /hooks/trending` filtered by category/platform | $0.25 (agent hooks free with Data Intelligence or while the agent has no hooks yet) | | "Which hook TYPES perform best?" / hook strategy questions | **Hook taxonomy** | `GET /hooks/types?sort=strong_hit_rate` (and `dimension=visual_hook_type` for faceless/frame-1 strategy); rank by `strong_hit_rate`, never `avg_weighted_score` | free | | "Find the original video for this hook" / "hooks that start like '...'" | **Hook search** | `GET /hooks/search?q=`: `match_type` exact = the source, contains = best hooks using the phrase | $0.25 | | "Write me N hooks for X" / hook drafting material | **Hook library + corpus grounding** | `GET /hooks/library` (templates + psychology) grounded with `GET /hooks/trending` for what's working now | $0.10 to $0.35 | `POST /v1/agents` is the unified research primitive: `is_recurring: false` runs once, `is_recurring: true` runs on a cadence. Legacy aside: `POST /v1/orbit` and `POST /v1/comet` still respond but are deprecated. IDs are interchangeable with agents, so migrate. Compose the workflows: a one-shot agent surfaces outlier creators → Satellite deep-dives the interesting ones → Tracking monitors the winners; promote a winning one-shot to a recurring agent to keep watching. That escalation path (research → inspect → monitor) answers most tasks end to end. ## 3. Async protocol (read this before polling) Content Research Agent runs, Satellite lookups, audience snapshots, and post collection are asynchronous: you create a job, poll, then read. - `status`: `pending | processing | completed | failed`. Agent runs also have `partial_failure` (about 2% of runs): **treat it as success**, because one platform or keyword failed but the rest of the data is there. Hard `failed` runs are rare. - `finalized: true` is the only "truly done" signal. `status: completed` with `finalized: false` means secondary AI jobs (viral analysis, audience snapshots) are still running; `null` analysis fields mean "not yet", **not** "no data". Per-video intelligence is not included: `finalized: true` does not wait for it. - `pending_jobs[]` (when not finalized) lists each in-flight job with `poll_url`, `result_path`, `webhook_event`, `retry_after_seconds`. Use `retry_after_seconds` as your sleep interval. - Per-video `intelligence_status` on agent videos and slideshows: `ready` (use it) | `pending` (re-fetch later; still pending a day later usually means it will never arrive) | `skipped` (deliberately not analyzed, stop polling; `intelligence_skip_reason` says why: `intent_mismatch`, `too_old` (posted over a year ago), or `under_followers` (over two days old with no more views than its creator has followers)) | `disabled` (Data Intelligence was off when the video was collected; turning it on only affects future runs). Often more than half of an agent's videos are `skipped`; a run's top 25 by views and top 50 by Virality Score are always analyzed when they fit the intent. Satellite creator runs have their own run-level `intelligence_status`: `pending | ready | failed | skipped`. - Webhooks beat polling for long jobs: `POST /webhooks` (free; needs a team-scoped key; up to 5 active endpoints). Preferred: `content_research_agent.run.completed` (fires once per run for both one-shot and recurring agents; carries `run_id`, `is_recurring`, `metrics`, and the AI `analysis` inline; the agent's settings sit under `orbit`/`comet` on success and under `content_research_agent` on failure). Other events: `content_research_agent.event.detected` (a recurring agent confirmed a breaking event in its niche; fires between runs, see §4.1), `satellite.lookup.completed` (route on `data.type`: `creator_lookup | sound_lookup | hashtag_lookup | video_outlier`; the full result is in `data.results`; a batch sends one message per creator; a free 6-hour cached repeat sends none), `tracking.cycle.completed`, `tracking.outlier_video.detected`, `tracking.paused`, `audience.snapshot.completed`, `trends.daily.completed`, `trends.region.completed`, plus legacy `orbit.run.completed` / `comet.run.completed`. Deliveries are not signed: add a secret custom header to the endpoint and reject requests without it. Realistic timings from production (set user expectations accordingly): | Job | Typical | Plan for | Poll every | |---|---|---|---| | Agent run (one-shot, or each recurring run) | half finish collecting in under ~8 min (p50 ~7.4 min), 9 in 10 within ~20 min (p90 ~18.3 min); the AI report follows within a few minutes | 45 min (broad runs with Meta ads) | 30 to 60s, or `retry_after_seconds` | | Recurring agent schedule | first run starts at create; later runs fire on the cadence (each daily/weekly/monthly agent has its own fixed time spread across the period; `next_run_at` shows it) | same per-run window | webhook / on cadence | | Satellite creator lookup | 15 to 40 s (data_intelligence lands on the saved run a few minutes later, up to ~10 for big lookups) | 2 min | 10 to 15s | | Satellite sound lookup | 1 to 2 min (a little longer with trend_analysis) | 5 min | 10 to 15s | | Satellite hashtag lookup | 1 to 3 min (about 15 s on TikTok, ~1 min on YouTube/Instagram; trends or deep/full take a few minutes) | 5 min | 10 to 15s | | Video outlier | 15 to 60 s | 3 min | 10s | | Tracking first report | creator 1 to 2 min, video under a minute | 5 min | webhook / 30s | | Audience snapshot | 5 to 12 min | 20 min | 30s / webhook | Satellite job status checks expire after 24 hours (404). Every lookup, video outlier checks included, is also saved as a durable run that never expires (see §4). Success rates: about 98% of agent runs complete clean and about 2% land `partial_failure` (still usable data); hard failures are rare. Satellite lookups succeed ~96–98%. If a run hard-fails, retry once before reporting failure (a new one-shot agent is billed again). ## 4. Maximizing the data you get back ### Intent + keywords (the #1 quality lever) **Formula:** `[Find/Monitor] [content type] about [niche] for [use case], [not / exclude X].` Aim for ~40–250 characters (the API accepts up to 500; MCP tools require at least 20). **Good (production patterns that kept hundreds of videos per run):** - "Find GRWM / beginner morning skincare routines and honest drugstore reviews, not dermatologist lectures." - "Find outlier JS/web-dev education creators: hooks and formats to adapt for faceless carousels." - "Find viral Gen Z videos that sell a photo/AI mobile app and push a download; exclude desktop tutorials." **Bad (avoid):** - `Keyword research: jeep, fitness` / `Monitor niche: handmade, crafts`: keyword dumps that give the filter nothing to judge - `I want to find viral video`: no niche - Keyword list as intent (`skin / curly hair / hooks`): not a sentence **Workflow:** write intent → `suggest-keywords` (free; check `quality.passes`) → create the agent with the **same** intent + returned keywords. Full cookbook: MCP resource `virlo://docs/intent-cookbook` or https://dev.virlo.ai/intent-cookbook.txt - Use **specific multi-word phrases**: "jeep wrangler mods", "high protein meal prep", "TikTok Shop strategies". Single generic words ("jeep", "fitness") return scattered, off-topic results. - Send **7–12 keywords** (the engine's sweet spot: `suggest-keywords` always returns 7 to 12; 1 to 50 are accepted at the same price). Cover synonyms of the same concept: each keyword is a separate platform search. A leading `#` is stripped and whitespace collapsed, but tags are not split into words (`#latteart` searches `latteart`, not `latte art`), so write multi-word tags as words. - When your keywords are too broad for the intent, the agent rewrites each one into a more specific phrase before the first run. The phrases actually searched appear in `intent_keywords` (null until the run starts; changing `keywords` or `intent` clears it). Recurring agents may also add timely event keywords, which appear in the run's `keyword_breakdown`, not in `intent_keywords`. - **Exclude keywords are a top precision lever.** When your topic shares a name with something else (Apple the company vs the fruit), or sits next to a noisy neighbor niche, off-topic videos flood in; a few sharp excludes remove more junk than an extra keyword adds. Each exclude is matched as a WHOLE WORD against the caption, hashtags included (`exclude_keywords_strict` also scans the spoken transcript), so use the single word people actually type for the OTHER meaning. A multi-word description almost never appears verbatim and filters almost nothing. - Apple the company (not the fruit)? Exclude `recipe`, `fruit`, `orchard`, `pie`. - Python the language (not the snake)? Exclude `snake`, `reptile`, `terrarium`. - Good (single real words): `recipe`, `snake`, `forex`. Bad (phrases that rarely match): `apple the fruit`, `day trading tips`. - Never exclude a word that's part of your own topic (excluding `apple` deletes everything). - suggest-keywords (MCP: suggest_keywords) returns a ready `exclude_keywords` list. Review it, remove any word that belongs to your own topic (for example `latteart` when you want latte-art videos), then pass the rest through to the create call. If you send no excludes, the agent writes its own from your intent when a run starts; check `exclude_keywords` after the first run. - `intent` (required): a one-sentence natural-language description of what the user is really after (e.g. "find UGC-style ads for skincare brands targeting Gen Z"). Every run, an AI reads each video's caption, hashtags and transcript and drops the off-topic ones (counted in the run's `intent_filtered`); it skips this step when the intent is too vague to judge by. With Data Intelligence on, each kept video also gets `intent_match` (`{ matches, reasoning }`, judged from caption, hashtags and transcript at collection time). The `?intent_match=true` filter on `/videos` is applied one page at a time, so its `total` counts only that page; to count every match, page through without the filter and count `intent_match.matches: true`. **Never synthesize intent from keywords** (e.g. "Keyword research: jeep, fitness"): it is the #1 weak pattern in production. Prefer `POST /v1/agents/suggest-keywords` (MCP: `suggest_keywords`, free) first, then create with the same intent. Full good/bad examples: `virlo://docs/intent-cookbook` / https://dev.virlo.ai/intent-cookbook.txt ### Scope settings - `is_recurring`: `false` for a one-time research pass, `true` (with `cadence`) to keep collecting on a schedule. Recurring agents also tune their own keywords on autopilot, on by default (§4.1). Send `autopilot: false` to keep the configuration exactly as you set it. - `cadence` (required when `is_recurring: true`): `daily` | `weekly` | `monthly` (stored as `0 0 * * *` / `0 0 * * 0` / `0 0 1 * *`; each agent runs at its own fixed time spread across the day, the week, or the first 7 days of the month, shown in `next_run_at`), or any other 5-field cron expression that runs at most once a day, which runs at the time it says. Rejected if `is_recurring: false`. - `platforms`: default to all three unless the user's question is platform-specific. Expect a TikTok-heavy mix (recent runs average roughly 70% TikTok / 15% Instagram / 10% YouTube of linked results) and very different view scales per platform (see §6). - **Don't send `min_views`, `time_period` or `time_range` at create.** `POST /v1/agents` rejects them with a 400 (the MCP tools accept and ignore them). Collection is system-managed and gathers broadly. **Filter at read time instead:** `GET /v1/agents/:id/videos` accepts `min_views`, `platforms` (plural), `start_date`/`end_date`, `order_by` (`views | publish_date | created_at`), `sort`, `intent_match`, `region`, and `include_transcript` for free. - `english_only` (default `true`): keep it `true` for English-only research; set `false` to collect content in **every language**, the switch that opens the gates to non-English/global niches. When you set `false`, write `keywords` and `intent` in the **target language** (the agent searches in the language you write in); leaving them in English while flipping the flag still returns mostly English. Applies to future runs on recurring agents; never re-filters already-collected content. - `meta_ads_enabled: true` adds Meta ad-library results at no extra cost (runs can then take up to ~45 min). Turn it on whenever the user has any commercial angle (competitor ads, offer research, paid creative). - `data_intelligence_enabled: true` (+$1.00 per run): adds 79 AI fields per analyzed video and 70 per analyzed slideshow (§7). Off-intent, year-old and underperforming content is skipped on purpose (`intelligence_status: "skipped"`). Enable it whenever the user's task involves *understanding content* (hooks, formats, messaging, brand mentions, CTAs) rather than just counting views. It applies only to runs after it is turned on (a recurring agent can toggle it with `PUT /v1/agents/:id`); it never back-fills videos already collected. - `region` filter (read time, **beta**): `GET /v1/agents/:id/videos?region=US` and `GET /v1/agents/:id/slideshows?region=US` restrict results to a single **upload country** (ISO-3166-1 alpha-2, case-insensitive; resolve country names the user says to the code yourself, because a full country name or unknown code silently returns 0 results). Each item carries `upload_region` (videos) / `region` (slideshows), `null` when unresolved. Region is deterministic where the platform provides it (TikTok video/creator region, YouTube channel country) and AI-inferred otherwise, so coverage is partial and improving; unresolved items are excluded when you filter. **Slideshows have the highest region coverage** (straight from TikTok), so a slideshow-heavy niche is the best surface for regional slicing. Pairs naturally with `english_only: false` for geo-targeted non-English research. - Pause, resume, delete: `PUT /v1/agents/:id { "active": false }` pauses a recurring agent and `true` resumes it. `DELETE /v1/agents/:id` is permanent: no more runs or charges, the agent drops off every list, and it can't be turned back on, though `GET /v1/agents/:id` and its data reads keep working, so save the id. If the balance runs out, recurring agents pause and adding funds won't restart them; resume with `active: true`. ### What a run yields A typical completed one-shot agent keeps about 360 videos (later recurring runs about 200), plus TikTok slideshows, Meta ads (~13 avg when enabled), ~4 outlier creators, top sounds, an AI analysis, and AI trend themes. **Pull every free sub-resource**: most agents under-read what they already paid for. ``` GET /v1/agents/:id/summary (compact one-call digest; read this FIRST once finalized) GET /v1/agents/:id/videos?limit=100&order_by=views (paginate; add region=US to filter by upload country, beta) GET /v1/agents/:id/videos?include_transcript=true&limit=20 (adds each video's transcript text, plus timed segments where the transcript has them) GET /v1/agents/:id/slideshows?limit=100 (TikTok image carousels, a distinct surface, see §8; highest region coverage) GET /v1/agents/:id/ads (if meta_ads_enabled) GET /v1/agents/:id/creators/outliers?order_by=weighted_score GET /v1/agents/:id/sounds (sort=rising/growth_7d; rows carry a lifecycle label; filter client-side) GET /v1/agents/:id/hashtags (sort=volume/growth/avg_views) GET /v1/agents/:id/analysis/latest (structured AI analysis, see §9) GET /v1/agents/:id/trends/latest (AI trend themes with evidence) GET /v1/agents/:id/hooks ($0.25; free with Data Intelligence or while there are no hooks yet; ranked hooks) GET /v1/agents/:id/runs (per-run counts: videos_linked, intent_filtered, keyword_breakdown) ``` Video pages can hold fewer rows than `limit` even when more exist (minor-safety and language checks remove rows after the page is fetched), and `total` can shift slightly between pages, so keep paging until you have walked past `total`; don't stop at the first short page. Recurring agents expose the same sub-resources; results accumulate across runs, and `/analysis` + `/trends` keep a per-run history (follow one trend by `stable_key`). ### 4.1 Recurring-agent autonomy New recurring agents (`is_recurring: true`) start on **autopilot** by default, whichever surface creates them (API, MCP, or the Virlo app). There is no unlock step; read an agent's `autopilot` field to see its current state. After each run, autopilot rewords the agent's searches, adds keywords (the set tops out at 15), adds short-lived timely keywords for a breaking story in the niche (they expire), and widens collection when a run comes back thin. Keyword changes must pass a quality check first. `GET /v1/agents/:id/activity` (free) is the change log: what changed and why. - **Limits:** autopilot never removes a keyword or exclude term the user set (they are pinned), never adds charges (the price per run is unchanged and it never triggers an extra billed run), and never changes the cadence or pauses the agent. - **One switch, `autopilot` (boolean):** `POST /v1/agents` accepts it (default `true`), and so does `PUT /v1/agents/:id`. `autopilot: false` keeps the configuration exactly as sent. `PUT /v1/agents/:id/autonomy` (free) takes `{ "autopilot": true | false }`, or the older `autonomy_level` (`"autopilot"` = on, `"suggest"` = off), plus `cognition_enabled: false` to pause self-optimization entirely (the agent keeps collecting). It returns 400 on an empty body or when `autopilot` and `autonomy_level` disagree. Confirm with the user before turning autopilot off. - **Pinned keywords:** responses carry `autopilot` (true when on) and `pinned_keywords` (the user's own keywords; `null` for agents created in the Virlo app). `keywords` holds the pinned ones plus any autopilot added. Sending `keywords` on `PUT /v1/agents/:id` replaces the list and, on an API-created agent, makes it the new pinned set, so the user's edits win; `exclude_keywords` works the same way. `autonomy_level` and `autopilot_unlocked` are still returned but deprecated: read `autopilot`. - **Deprecated:** `GET /v1/agents/:id/proposals` and `POST /v1/agents/:id/proposals/:pid/{apply,dismiss,revert}` still work during a deprecation window (responses carry a `Deprecation: true` header). Autopilot applies its changes itself; with autopilot off, changes wait there as `pending`. Approving a proposal on an API agent makes the approved keywords and excludes the new pinned lists. Don't build new flows on them: use `/activity` to explain changes. One-shot agents expose these fields, but autopilot does nothing for them and they produce no proposals or activity. Recurring agents also stay aware of breaking events in their niche: `GET /v1/agents/:id/events` (free) lists detected stories (`title`, `summary`, `salience` 0–10, `status` `candidate|confirmed|dismissed|expired`, timely `keywords`, `evidence` videos), active first. The `content_research_agent.event.detected` webhook pushes each one the moment it is confirmed, between scheduled runs. Use it to explain WHY an agent recently adapted its keywords or collected early. ### Satellite yield levers - Creator lookups: `include=videos,outliers` and `max_videos=50` to 100 (the default is only 20; sound and hashtag lookups already default to 50). Without `include=videos,outliers`, the video and outlier arrays are not returned, so raising `max_videos` alone gives you nothing more to read. - `cross_links=true`: discovers the creator's other-platform profiles (high-confidence only). - `trend_analysis=true` (+$0.50): reads up to ~300 of the creator's latest videos (ignores `max_videos`) and returns LLM-detected trends over their body of work, with `time_windows[]` computed from real publish dates, `resurged` (≥2 disjoint windows), `momentum` (`stronger|weaker|similar` vs prior window), and `evidence_video_ids` mapping into `videos[]`. The surcharge is refunded when no trends come out: fewer than 10 videos (`trends.status: "insufficient_corpus"`), or a failed analysis step, which still reads `status: "ok"` with an empty list (only `credits_refunded` tells it apart). - `data_intelligence=true` (+$0.25): adds a 43-field `intelligence` object to each analyzed video (35 for photo carousels; named like the agent fields but a subset of the agent add-on's 79) and a grounded "what's working" `analysis` of the creator. It runs asynchronously after the base lookup (a few minutes, up to ~10 for big lookups): the status poll reads `completed` and `finalized: true` while it is still pending, so read it back on the run via `GET /satellite/runs/:run_id`, whose `intelligence_status` (`pending|ready|failed|skipped`) tells you when it is done. Optional `context` (≤600 chars) grounds the analysis at no cost. This surcharge is refunded when the lookup had no posts to enrich (for example an audience-only lookup of a creator with no public posts), when its analysis fails (`intelligence_status: failed`), or when the lookup fails. Distinct from the Agents `data_intelligence_enabled` (+$1.00, 79 fields). On carousel-heavy creators, `slideshow_sort=recent` (or `popular`, the default) picks which slideshows get slide-captured + per-slide OCR'd when there are more than the 100-per-run cap: `recent` keeps the newest by publish date, `popular` keeps the highest-engagement. Capture selector; distinct from the read-side `sort` on `/runs/:run_id/videos`. No extra cost. - Boolean flags turn on only with the exact lowercase string `true` (sound and hashtag `trend_analysis` also accept `1`). `True` (what Python's requests sends for a bool), `yes`, or `1` elsewhere are silently treated as false. - Creator lookup repeats: the same platform + handle within 6 hours (anyone on your team) returns the earlier run free (`cached: true`) only when that run covers every option requested (add-ons, includes, at least the same `max_videos`, the same `outlier_threshold`); otherwise it runs fresh under a new run_id and is charged, so a trend lookup after a plain one costs a second lookup. Batches (`POST /satellite/creators/batch`) cost $0.50 per creator that starts a lookup: a creator whose run from the last 6 hours covers the batch's options comes back `completed` at submit and is free, and one that fails to queue is not billed. A lookup that fails (for example, a handle that doesn't exist) is refunded in full; the batch aggregate carries no `credits_refunded`, so read each creator's own status. The audience surcharge is refunded when no new snapshot was needed (one was cached or already being built, or the request for it failed). - Sound lookups (`GET /satellite/sounds/:platform/:music_id`, `tiktok` | `instagram`): `music_id` is the platform's external id, or a Virlo sound UUID. `max_videos` 1–100 (default 50; these are the first videos in the platform's feed order, not the top by views). `trend_analysis=true` keeps ~300 videos. If fewer than 10 videos turn up, `trends.status === "insufficient_corpus"` and the trend surcharge is refunded (so is a failed analysis step). Repeats within 6 hours are free cache hits (`max_videos` is ignored when matching the cache). - Hashtag lookups (`GET /satellite/hashtags/:platform/:hashtag`, platforms `tiktok` | `instagram` | `youtube`): pass the tag with or without `#` (URL-encode as `%23`; normalized to lowercase), `sort=top` (views desc, default) or `recent` (publish date desc; stats are order-independent), `max_videos` 1–100 (default 50). Repeating the same lookup within 6 hours returns the cached run free (`cached: true`). `trend_analysis=true` (+$0.50) forces a ~300-video deep fetch (ignores `max_videos`) and returns the same trends shape as sound lookups. - Hashtag `depth` (`standard` default | `deep` | `full`): `standard` collects `max_videos` at the $0.50 base; `deep` ~300 videos (+$0.50, $1.00 total); `full` ~500 videos (+$1.50, $2.00 total); `deep`/`full` ignore `max_videos` and are charged in full even when the feed runs out early (the depth surcharge comes back only if the whole lookup fails; the trend surcharge is refunded when no trends come out). The deep surcharge is waived with `trend_analysis=true` (trends already fetch ~300 videos): deep+trends $1.00, full+trends $2.50. Instagram is `standard`-only (`deep`/`full` return 400 before billing); TikTok and YouTube support all three tiers. The 6-hour cache only satisfies a request at an equal-or-deeper stored depth: a deeper cached run serves a shallower request free; a shallower one re-scrapes and bills. - Hashtag coverage differs by platform: TikTok reads the native challenge feed (fullest data); YouTube reads the native hashtag page, Shorts only, and each Short is detail-enriched (exact views, likes, comments, publish dates, durations, channel follower counts, sound attribution) though `shares`/`collects` stay 0; Instagram is Google-indexed public Reels, best-effort coverage and shallower than the other two (upstream depth caps at ~11 pages). Never compare `engagement_rate` across platforms; check `sample_quality.note` (`insufficient_corpus` → `deep_corpus`) before leaning on the stats. - Hashtag results are pivot maps: `related_hashtags` (20 co-occurring tags, the looked-up tag excluded) seeds the next hashtag lookup; `top_sounds` (10 on TikTok and YouTube, empty on Instagram) feeds `GET /satellite/sounds/:platform/:music_id`. Chain lookups instead of guessing fresh queries. - Every Satellite lookup (creator, sound, hashtag, and video outlier checks) is saved as a run whose `run_id` equals the job ID. **Save it.** `GET /satellite/runs/:run_id` (and `/runs/:run_id/videos`) re-reads the full result free, forever, long after the 24-hour status check has expired. Every paid lookup is a new run with its own `run_id`, so a later lookup of the same creator, sound or hashtag never overwrites an earlier one (a repeat within 6 hours returns the existing run free). A lookup that stalls and is later closed out as failed by the stuck-run sweep is not refunded. A batch creates one `creator_lookup` run per creator. Never re-buy a lookup you already ran; check `GET /satellite/runs?type=…` first. ## 5. Spotting the most viral content Views alone mislead: a 500K-view video from a 10M-follower account is routine; the same views from a 3K-follower account is a signal. Virlo's canonical ranking is the **weighted virality score**: ``` ratio = views / followers (only when followers > 0 and ratio > 1) weighted_score = ln(ratio) × ln(followers) ``` | weighted_score | Read it as | |---|---| | ≥ 35 | Exceptional: massive outperformance, study frame by frame | | 25–35 | Very strong: beats expectations at any audience size | | 18–25 | Strong: clearly above the creator's baseline | | 10–18 | Promising: real viral traction | | < 10 | Emerging / routine | Practical procedure for "find the most viral": 1. Pull all videos from the run, compute `weighted_score` per video from `views` and `author.followers` (video rows carry no score field). Rank by it, not raw views. 2. Sanity-check raw multiplier too: `views/followers` ≥ 20× is notable, ≥ 100× is exceptional, ≥ 1000× is a mega-outlier. 3. Compute `engagement_rate = (likes + comments + shares) / views`. High views + high engagement (>5%) = resonance; high views + low engagement (<1%) = passive distribution (sounds, reposts, paid). 4. Cross-check against the AI's pick: `analysis_data.top_10_breakdown` (from `/analysis/latest`) is an LLM-curated standout list that also weighs content quality and topicality. Where your math ranking and the AI ranking agree, you've found the real winners. 5. **Creator outliers** (`/creators/outliers`) apply the same logic at account level: `outlier_ratio` (avg views vs follower baseline, shown as "N× reach") and `weighted_score` = ln(outlier_ratio) × ln(follower_count). Sort by `weighted_score` (the default sort is `outlier_ratio`). These are under-followed creators consistently outperforming: best source for rising talent and partnership leads. 6. Recency matters: a high score on a video published this week is a live trend; the same score from months ago is history. Always read `publish_date`. ## 6. Calibrate expectations to platform reality Sampled from recent production runs; use these to judge whether a result is actually impressive: | Platform | Median views | P90 | P99 | Transcript coverage | |---|---|---|---|---| | TikTok | ~39K | ~1M | ~6.6M | ~63% | | Instagram Reels | ~3.8K | ~175K | ~3M | ~0% without Data Intelligence; ~40% with it (timed) | | YouTube Shorts | ~1K | ~32K | ~2.5M | ~38% | Implications: never compare raw view counts across platforms; a 100K-view Reel is a bigger deal than a 100K-view TikTok. Speech-based insights (hooks from transcripts, spoken messaging) come mostly from TikTok and YouTube. Instagram publishes no transcripts, so Reels only have one when Data Intelligence transcribed the audio; without it, lean on descriptions, hashtags, and AI visual analysis. Read the text itself with `include_transcript=true` on `/videos`: `source: "platform"` is the platform's own text, usually with no timestamps (most TikTok and YouTube videos; a small share carry timed `segments`, so check that field), `source: "transcribed"` is Virlo speech-to-text with timed `segments` (every Instagram transcript, and TikTok/YouTube videos the platform didn't caption). `transcript: null` means no speech or not transcribed yet. Each video also carries `duration` in seconds (null when unknown), separate from `sound.duration`. ## 7. Reading video intelligence (the 79 AI fields) With `data_intelligence_enabled`, each analyzed video carries an `intelligence` object (check `intelligence_status === "ready"`). Videos with `intelligence_status: "skipped"` were left out on purpose and never get one. When the AI can't read a video's frames (common on YouTube), every picture-based field is `null` and `low_confidence_fields` contains `tier3_unavailable`. Fields, grouped by how to use them: **Classification**: `primary_topic` (free-text main subject), `secondary_topics[]`, `keywords[]`, `category`, `content_format`. Use these to verify on-topic-ness and to segment the result set. `content_format` has a canonical core (`explainer`, `tutorial`, `review`, `storytime`, `listicle`, `silent_aesthetic`, `motivational`, `skit_sketch`, `comedy_bit`, `day_in_life`, `rant`, `news_commentary`, `challenge`, `q_and_a`, `transformation`, `hot_take`, `reaction`, `unboxing`, `grwm_routine`, `vlog`, `meme`, etc.) plus a long free-text tail; **match formats loosely/normalized** (treat `skit`, `comedy_skit`, `skit_sketch` as one bucket). **Hook analysis (highest-value fields)**: `hook_text` is the literal opening copy (spoken or on-screen); `hook_type` values include `tutorial_promise`, `bold_claim`, `question`, `relatable_scenario`, `direct_address`, `pov_setup`, `shock_statement`, `story_tease`, `negation`, `statistic`, `before_after`, `trend_reference`, `comparison`, `mystery_setup`, `controversy`, `cliffhanger` and `none`. The set is not closed. Virlo adds hook types over time, so handle values you don't recognize. `get_hook_types` (MCP) or `GET /v1/hooks/types` returns the live list, and an unknown `hook_type` filter gets a 400. `visual_hook_type` covers the visual opener (e.g. `text_hook`). To answer "what hooks work in this niche", prefer the dedicated hook surface: `GET /v1/agents/:id/hooks` returns the agent's hooks pre-ranked by Virality Score with a `coverage` object ($0.25; free when the agent has Data Intelligence, and free while `coverage.videos_with_hooks` is 0). If `coverage.videos_with_hooks` is 0 the agent has no hook data YET (young agent or intelligence disabled): fall back to `GET /v1/hooks/trending` filtered to the niche's category/platform rather than concluding the niche has no hooks. Corpus-wide: `/v1/hooks/types?sort=strong_hit_rate` (free) says which hook TYPES win per category/platform (`strong_hit_rate` = share scoring >= 18; `avg_weighted_score` reads negative and is not a ranking signal); `/v1/hooks/search` finds hooks built on a phrasing (`match_type=contains`, strongest first), reverse-looks-up a pasted hook (`match_type=exact`, which ignores capitals and spacing but not punctuation), or browses by type; `/v1/hooks/library` supplies curated fill-in-the-blank templates with the psychology of why each works (its `psychology_tag` filter is an exact, case-sensitive phrase: `social proof`, not `social_proof`). `/v1/hooks/trending` with `content_type=slideshow` currently times out (503); use `/v1/hooks/search` with `content_type=slideshow` and a `hook_type` instead. Every hook row has `usage_count`: >1 means a reused template (worth saying so), 1 an original line. **Visual production**: `visual_format` (closed set: `talking_head`, `b_roll_montage`, `activity_demonstration`, `green_screen_commentary`, `product_closeup`, `animation_motion_graphics`, `slideshow_text`, `vlog_handheld`, `screen_recording`, `interview`, `split_screen_duet`, `pov_footage`, `native_gameplay`, `dance_full_body`, `street_interview`, …), `setting`, `camera_perspective`, `lighting_quality`, `visual_complexity` (`minimal_clean | moderate | busy`), `has_face_visible`, `has_text_overlay` + `text_overlay_content`/`text_overlay_purpose`, `background_type`/`foreground_type`. Tells you the production recipe: what to film and how, without watching the video. **Speech & captions** (the transcript text itself is on `/videos?include_transcript=true`, §6): `transcript_word_count`, `transcript_quality`, `language_detected`, `speaking_style`, `has_onscreen_captions`, `caption_style`. `transcript_word_count: 0` with a populated visual analysis = silent/aesthetic content, a deliberate format, not missing data. **Tone**: `emotional_tone` (closed set: `educational`, `neutral`, `inspiring`, `funny`, `hype`, `relatable`, `calm`, `shocking`, `wholesome`, `angry`, `heartwarming`, `mysterious`, `urgent`, `sad`, `controversial`, `nostalgic`, `sarcastic`, `cringe`, `dark_humor`, `playful`) and `sentiment` (`positive|neutral|negative`). **Commercial signals**: `is_sponsored`, `brands_mentioned[]`, `cta_usages[]` (objects like `{ "type": "link_in_bio", "text": "Link in bio" }`), `social_proof_used[]`, `trend_references[]`. This is competitive/monetization intelligence: who's being paid, what brands appear organically, which CTAs the niche actually uses. **Safety & suitability**: `brand_safety_tier`, `is_nsfw`, `sensitive_topics[]`, `is_educational`. Filter on these for brand-fit shortlists. (Videos flagged by Virlo's minor-safety check are removed from results before you see them.) **People on screen**: a read of apparent *presentation*, not identity. `people[]` is per-person (`{ role, is_synthetic, apparent_gender, apparent_age_bracket, apparent_age_min/max, confidence }`); the rest are indexed roll-ups: `people_count_bucket` (`none|one|two|small_group_3_5|large_group_6_plus|crowd`), `on_screen_presence` (what the camera shows of a person) and its derived `presence_style` (the "faceless vs on-camera" filter: `on_camera_presenter|on_camera_subject|faceless_voiceover|faceless_silent|hands_or_pov|avatar_or_animated|no_person`), `primary_subject_gender`/`primary_subject_age_bracket` (+`_min`/`_max`/`_confidence`), `genders_present[]`, `age_brackets_present[]`, `subject_gender_skew` (`all_female|all_male|mixed|ambiguous|none`), and the booleans `has_minor_visible`, `has_real_person`, `has_baby_visible`, `has_senior_visible`. Gender is `female|male|ambiguous`; age brackets run `infant_0_3` → `senior_65_plus` plus `unknown`. This answers demographic asks ("women-led fitness", "no minors", "faceless voiceover niche") directly. **AI provenance**: is the *content itself* synthetic (distinct from videos ABOUT AI). `ai_provenance` rolls up `none|ai_assisted|ai_voice|ai_visuals|ai_presenter|fully_ai|unknown`; `ai_elements[]` lists the specific synthetic parts (`synthetic_presenter`, `generated_imagery`, `generated_video`, `face_swap_or_clone`, `synthetic_voice`, `ai_captions_or_edit`); `ai_confidence` + `ai_reasoning` explain the call. **On-screen objects**: read from the frames, not the copy: `brands_visible[]` (logos/brands seen, may differ from spoken `brands_mentioned`), `product_presence` (`branded_product_featured|unbranded_product_featured|product_incidental|no_product`; the only way to find "no-name product" videos), `ip_characters[]`; `animals_visible[]` + `animal_presence` (`animal_featured` = video is *about* the animal) + `has_animal`; `screen_content_type` (gameplay vs a SaaS demo vs a fake-text-thread skit, null unless a screen dominates); `is_silent`. **Edit structure**: cheap exclusion booleans, orthogonal to format: `is_compilation`, `is_ranking`, `is_repost`, `is_before_after`, `is_layered_composition`, plus `has_platform_watermark` + `watermark_source`. Use these to strip reposts/compilations from a shortlist. **Synthesis & trust**: `summary` (1-paragraph AI description, present on ~100% of analyzed videos; the fastest way to "watch" 100 videos is to read 100 summaries), and `low_confidence_fields[]`: **discount any field listed there**. Aggregation pattern: the strongest insight format is distribution-over-winners. Take the top quartile by `weighted_score`, count `hook_type` × `content_format` × `emotional_tone` buckets, then contrast against the bottom quartile. Differences between those distributions are the niche's actual playbook. ## 8. Slideshows (TikTok image carousels) Slideshows are a separate result surface (`/v1/agents/:id/slideshows`) with their own intelligence (70 fields); don't ignore them: in product, education, and lifestyle niches they regularly outperform video. Slideshow intelligence shares the core fields (hook, topic, format, tone, brands, CTAs, safety, summary) plus the People, AI-provenance, on-screen-objects, and edit-structure groups from §7, and adds: - `image_count`: number of slides. - `panel_texts[]` / `panel_text_full`: the extracted on-screen text of every slide, in order. **This is the entire content of most slideshows**: read it like a script; it's directly replicable copy. - `narrative_arc`: how the slides progress (listicle, tutorial steps, before/after…). The slideshow equivalent of content structure. - `text_density`: `text_dominant | balanced | image_dominant`. Video-only fields (`visual_format`, `camera_perspective`, captions, transcript) don't exist on slideshows; their absence is structural, not missing data. Hook = slide 1: analyze `hook_text` on slideshows exactly like videos. Slideshows also carry a deterministic `region` (TikTok upload region, ISO-3166-1 alpha-2), the **highest-coverage** region signal Virlo exposes. Filter with `GET /v1/agents/:id/slideshows?region=US` (beta; see §4) when the user wants content from a specific country. ## 9. Reading the run-level AI analysis and trends `GET /v1/agents/:id/analysis/latest` returns `analysis_data`; treat it as a pre-computed research report (it samples at most 240 videos): - `overview` + `key_highlight`: executive summary; `key_highlight` is the single most important finding. - `themes[]`: clusters of what's working, each with `name`, `why_it_works`, `tactics[]`, `confidence` (0–1; weight ≥0.7 heavily, mention <0.5 as tentative), `video_count`, and `evidence_video_ids[]`. **Always join evidence ids back to the video list** so claims stay grounded in retrievable examples. - `viral_tactics[]`: cross-theme replicable techniques. - `top_10_breakdown`: AI-curated standout videos with reasoning. - `timing_analysis`: posting-time patterns observed in the data. - `excluded_videos`: what the AI judged off-topic (useful to gauge result purity). - `connecting_thread`: the meta-pattern across themes. `GET /v1/agents/:id/trends/latest` returns trend items with lifecycle `status`: `new` (just emerged, highest opportunity), `rising` (growing, act now), `steady`, `fading` (avoid). Each has `rank`, `name`, `why_it_works`, `tactics[]`, `confidence`, engagement aggregates, `avg_virality_score` (a 0–1 scale, not comparable to `weighted_score`), `platform_breakdown`, `top_creators[]`, `peak_hour_utc`, and `prev_*` fields for run-over-run deltas. On recurring agents, `GET /v1/agents/:id/trends?stable_key=…` lets you follow one trend across runs: a trend going `new → rising` with growing `video_count` is your strongest "post about this now" signal. ## 10. Workflow recipes **Full niche research** ($1.50 with intelligence), the default for "understand X": `POST /v1/agents/suggest-keywords` (free) → `POST /v1/agents` (`is_recurring: false`, the same intent, 7–12 specific keywords, all platforms, `meta_ads_enabled: true`, `data_intelligence_enabled: true`) → poll `GET /v1/agents/:id` to `finalized: true` → read summary, analysis + trends → pull all videos & slideshows (paginate; slice with `min_views`/dates/`order_by` at read time) → rank by weighted score (§5) → aggregate intelligence over winners (§7) → check outlier creators and sounds → deliver: top examples with hook quotes, format/hook distribution among winners, trend lifecycle calls, creator shortlist. **Global / non-English research + region filtering** ($0.50–$1.50): `POST /v1/agents` with `english_only: false` and `keywords`/`intent` written in the target language → poll to `finalized: true` → read videos & slideshows, adding `region=` (e.g. `region=MX`) to slice by upload country (slideshows have the highest coverage). Deliver top examples per region with `upload_region` shown. Region is beta (§4). **Creator vetting** ($0.50–$1.75): `GET /satellite/creator/:platform/:user?include=videos,outliers&cross_links=true&max_videos=50` (+`trend_analysis=true` for content-pattern history; +`data_intelligence=true` for per-video hook/format/visual + a grounded "what's working" `analysis` read on the run; +`audience_demographics=true&audience_geography=true` for who their audience is) → poll → report profile stats, outlier videos, cross-platform presence, audience confidence-aware (§11). Save `run_id`. **"Why did this video pop?"** ($0.50): `POST /satellite/video-outlier` → compare the video's metrics vs creator baseline → if intelligence is available on related searches, contrast its hook/format against the creator's norm. Every call is a new paid check (no cache), even for the same URL; re-read the saved run instead. **Sound strategy** ($0.60–$1.25): `/sounds/search` or `/sounds/trending`/`/sounds/breakout` to find `music_id` (use the sound's `external_id`, or its Virlo `id`) → `GET /satellite/sounds/tiktok/:music_id?trend_analysis=true` → read `stats.velocity.is_accelerating`, `top_creators`, `trends[].time_windows/resurged/momentum` → recommendation: accelerating + `new`/`rising` patterns = use it now; `fading`/`weaker` momentum = skip. **Continuous monitoring**: `POST /v1/agents` with `is_recurring: true` + `cadence: "weekly"` (same keywords as a winning one-shot) + `POST /webhooks` for `content_research_agent.run.completed` → each run, diff `trends` via `stable_key` and surface `new`/`rising` items; autopilot (§4.1) keeps refreshing its keywords over time without removing the user's, and `GET /v1/agents/:id/activity` explains each change. For creators: `POST /tracking/creators` ($0.25 to start, refunded if the creator can't be found; then $0.25 per successful check) → snapshots (`delta_*` fields for growth charts; rows come oldest first and `limit` keeps the OLDEST, so pass `start_date` for recent data), auto AI reports, `posting-cadence`, then `GET /tracking/creators/:id/posts` (free) to enumerate collected posts with per-post metrics, outlier flags, and a nested `sound` object (`sound.external_id` = platform-native sound id for music/campaign matching; null for YouTube). For older posts use `collection_depth` / `posts/collect`: `standard` (up to 50 videos, $0.50), `deep` (200, $1.00), or `full` (500, $2.00); every depth pages to its target, and a collection that finds no posts or fails is refunded (the collection status reports it as `credits_refunded`, in credits). `posts/collect` returns `409` "Tracking unhealthy" before billing when the creator's last tracking cycle failed (`scrape_status` or `enrichment_status` is `failed`): fix the handle or resume tracking, or send `{"force": true}` if the failure was transient. TikTok/Instagram posts carry sound after any cycle, YouTube only on deep/full collections. A tracked video you stop tracking can't be tracked again (same URL returns a 400), so pause instead if you may want it back. ## 11. Audience data: trust rules Audience snapshots (demographics + geography) are derived from **engaged commenters**, not the follower count. Always read the reliability fields before presenting numbers: - `confidence_level`: `high`/`medium` → usable; `low` → say "limited signal", don't state percentages as fact. - `data_source`: `comments` (best) > `comments_extended` > `mixed` > `followers` (TikTok-only, capped at medium) > `profile_only` (synthesized fallback, always low confidence, age/gender null, automatically refunded; treat as "no audience data available"). Any failed snapshot is refunded too. - Cache-first: snapshots are reused free for 30 days (`freshness_days`); only cache misses cost $0.50, and a refresh while a snapshot job for the creator is already running returns that `job_id` free (`credits_used: 0`). The audience GET endpoints always return the latest snapshot whatever its age; `freshness_days` only sets `is_stale: true`. In geography, `country_distribution[].name` holds the ISO code (e.g. `PH`), not the country name. ## 12. Spend discipline - Starting work costs money, and so do instant data calls (trends, videos digest, hashtags, sounds, hooks: $0.05–$0.25 each, billed even when the result is empty). Re-reading what you already created is free. Exhaust the free sub-resources of resources you already created before creating new ones. - Check `GET /v1/agents`, `GET /satellite/runs`, `GET /tracking/*` lists first: the answer may already exist in the team's history. - One well-scoped agent beats three vague ones. Spend effort on keyword craft, not retries. - Quote costs before multi-step plans (e.g. "this research plan costs ~$2.50") and check `GET /account/balance` (free) when starting a session. Warn the user when that balance drops below $10; read it from `GET /account/balance`, not the `X-Balance-Remaining` header (which only appears on charged responses). - Recurring costs keep going: a recurring agent is charged $0.50 every run ($1.50 with Data Intelligence) and tracking $0.25 every check, until paused or deleted. Confirm the cadence with the user. - Batch Satellite (`POST /satellite/creators/batch`, up to 25 creators, $0.50 per creator looked up; cached ones free) instead of serial single lookups when vetting a list.