# VHGENGINE (remote · engine-production-3bdc.up.railway.app)

Agents-first viral-hook engine: generate, score, and remix short-form hooks over MCP.

- Trust score: 65/100 (medium)
- Change this week: +13
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-08-03

## Components

- remote · `engine-production-3bdc.up.railway.app`: 65/100 (this document), [markdown](https://verifymcp.io/servers/app-railway-up-engine-production-3bdc-vhgengine/engine-production-3bdc.md), [page](https://verifymcp.io/servers/app-railway-up-engine-production-3bdc-vhgengine/engine-production-3bdc)

## Channel facts

- Endpoint: `https://engine-production-3bdc.up.railway.app/mcp`
- Transports: `streamable-http`
- Auth: `none`
- Version: `2.3.0`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically against the live server, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-08-03.

- **Endpoint Security**: 63/100
  - The endpoint's TLS certificate is valid, in date, and uses a strong key.
  - Authorisation check failed: no authorisation is required to call this server, and it exposes a tool marked destructive (delete_account).
  - HTTPS is enforced; there's no plaintext access path.
  - The HSTS (Strict-Transport-Security) header is present.
  - DNSSEC check failed: this domain isn't protected by DNSSEC.
- **Transport & Reachability**: 100/100
  - Verified streamable-http transport via a live MCP handshake.
- **Schema Quality & AI Usability**: 53/100
  - 43% of prompts and resources have a non-trivial description (not blank, and not just the item's name).
  - AI-judged instruction clarity (excellent).
  - Context-footprint check failed: tool/resource definitions use about 14279 tokens (~274/item across 52 items; 38 tools + 14 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 27/100
  - Stability observed for 8 of 30 days with no destabilising changes; credit accrues until the full window elapses.
- **Tool Coverage**: 100/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 100% of tool parameters carry a description.
  - Structured output schemas are declared (100% of tools); any adoption earns full credit.
- **Capabilities**: 100/100
  - Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.

## Install

### Claude

```bash
claude mcp add --transport http app-railway-up-engine-production-3bdc-vhgengine https://engine-production-3bdc.up.railway.app/mcp
```

### Codex

```toml
[mcp_servers.app-railway-up-engine-production-3bdc-vhgengine]
url = "https://engine-production-3bdc.up.railway.app/mcp"
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "app-railway-up-engine-production-3bdc-vhgengine": {
      "type": "remote",
      "url": "https://engine-production-3bdc.up.railway.app/mcp",
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add app-railway-up-engine-production-3bdc-vhgengine --url https://engine-production-3bdc.up.railway.app/mcp --transport streamable-http
```

### Hermes

```yaml
mcp_servers:
  app-railway-up-engine-production-3bdc-vhgengine:
    url: "https://engine-production-3bdc.up.railway.app/mcp"
```

### Other

```json
{
  "mcpServers": {
    "app-railway-up-engine-production-3bdc-vhgengine": {
      "type": "http",
      "url": "https://engine-production-3bdc.up.railway.app/mcp"
    }
  }
}
```

The mcpServers block is a cross-client convention. Remote transports vary, so check your client's docs.

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-08-03 (score 65, +6)

- [security] The server rewrote its instructions, which are the text every model session reads
- [security] New tool “cancel_job”, which the server declares destructive
- [security] Tool “delete_account” rewrote its description, which is the text the model reads
- [security] Tool “generate_hooks” rewrote its description, which is the text the model reads
- [security] Tool “generate_hooks_batch” rewrote its description, which is the text the model reads
- [security] Tool “get_account” rewrote its description, which is the text the model reads
- [security] Tool “get_usage” rewrote its description, which is the text the model reads
- [security] Tool “health” rewrote its description, which is the text the model reads
- [security] Tool “list_hooks” rewrote its description, which is the text the model reads
- [security] Tool “remix_hook” rewrote its description, which is the text the model reads
- [security] Tool “score_hook” rewrote its description, which is the text the model reads
- [security] Tool “signup” rewrote its description, which is the text the model reads
- [security] Tool “start_generate_job” rewrote its description, which is the text the model reads
- [functional regression] Schema quality: 138 → 274
- [functional improvement] Schema quality: 0% → 43%
- [functional improvement] Tool coverage: 0% → 100%
- [functional] Schema quality: good → excellent
- [functional] New resource “client-guide-claude-desktop”
- [functional] New resource “client-guide-codex”
- [functional] New resource “client-guide-cursor”
- [functional] New resource “client-guide-rest”
- [functional] New resource “sample”
- [functional] New resource “client-guide-chatgpt”
- [functional] New resource “client-guide-claude-code”
- [functional] New tool “quote”
- [functional] New tool “export_usage”
- [functional] New tool “list_billing_events”
- [functional] New tool “list_runs”
- [functional] New tool “wait_for_job”
- [cosmetic] “get_usage” added an optional parameter “offset”
- [cosmetic] “remix_hook” added an optional parameter “archetype”
- [cosmetic] “remix_hook” added an optional parameter “hook_id”
- [cosmetic] “remix_hook” added an optional parameter “platform”
- [cosmetic] “remix_hook” added an optional parameter “topic”
- [cosmetic] “score_hook” added an optional parameter “topic”
- [cosmetic] “score_hooks_batch” added an optional parameter “topic”
- [cosmetic] “add_credits” reworded the description of “admin_key”
- [cosmetic] “add_credits” reworded the description of “amount”
- [cosmetic] “add_credits” reworded the description of “api_key”
- [cosmetic] “add_credits” reworded the description of “idempotency_key”
- [cosmetic] “create_checkout” reworded the description of “api_key”
- [cosmetic] “create_checkout” reworded the description of “cancel_url”
- [cosmetic] “create_checkout” reworded the description of “pack”
- [cosmetic] “create_checkout” reworded the description of “success_url”
- [cosmetic] “create_key” reworded the description of “api_key”
- [cosmetic] “create_key” reworded the description of “daily_credit_cap”
- [cosmetic] “create_key” reworded the description of “name”
- [cosmetic] “create_key” reworded the description of “scopes”
- [cosmetic] “delete_account” reworded the description of “api_key”
- [cosmetic] “delete_account” reworded the description of “confirm”
- [cosmetic] “delete_webhook” reworded the description of “api_key”
- [cosmetic] “generate_hooks” reworded the description of “api_key”
- [cosmetic] “generate_hooks” reworded the description of “archetypes”
- [cosmetic] “generate_hooks” reworded the description of “count”
- [cosmetic] “generate_hooks” reworded the description of “deadline_ms”
- [cosmetic] “generate_hooks” reworded the description of “idempotency_key”
- [cosmetic] “generate_hooks” reworded the description of “mode”
- [cosmetic] “generate_hooks” reworded the description of “platform”
- [cosmetic] “generate_hooks” reworded the description of “style”
- [cosmetic] “generate_hooks” reworded the description of “tags”
- [cosmetic] “generate_hooks” reworded the description of “topic”
- [cosmetic] “generate_hooks” reworded the description of “verbosity”
- [cosmetic] “generate_hooks_batch” reworded the description of “api_key”
- [cosmetic] “generate_hooks_batch” reworded the description of “idempotency_key”
- [cosmetic] “generate_hooks_batch” reworded the description of “mode”
- [cosmetic] “generate_hooks_batch” reworded the description of “platform”
- [cosmetic] “generate_hooks_batch” reworded the description of “requests”
- [cosmetic] “generate_hooks_batch” reworded the description of “tags”
- [cosmetic] “generate_hooks_batch” reworded the description of “verbosity”
- [cosmetic] “get_account” reworded the description of “api_key”
- [cosmetic] “get_activity” reworded the description of “api_key”
- [cosmetic] “get_activity” reworded the description of “recent_limit”
- [cosmetic] “get_hook” reworded the description of “api_key”
- [cosmetic] “get_hook” reworded the description of “hook_id”
- [cosmetic] “get_job” reworded the description of “api_key”
- [cosmetic] “get_job” reworded the description of “job_id”
- [cosmetic] “get_usage” reworded the description of “api_key”
- [cosmetic] “get_usage” reworded the description of “key_prefix”
- [cosmetic] “get_usage” reworded the description of “recent_limit”
- [cosmetic] “get_usage” reworded the description of “request_id”
- [cosmetic] “get_usage” reworded the description of “tag”
- [cosmetic] “get_webhook” reworded the description of “api_key”
- [cosmetic] “list_hooks” reworded the description of “api_key”
- [cosmetic] “list_hooks” reworded the description of “limit”
- [cosmetic] “list_hooks” reworded the description of “mode”
- [cosmetic] “list_hooks” reworded the description of “offset”
- [cosmetic] “list_hooks” reworded the description of “since”
- [cosmetic] “list_hooks” reworded the description of “tag”
- [cosmetic] “list_hooks” reworded the description of “topic”
- [cosmetic] “list_hooks” reworded the description of “unreported”
- [cosmetic] “list_jobs” reworded the description of “api_key”
- [cosmetic] “list_jobs” reworded the description of “limit”
- [cosmetic] “list_jobs” reworded the description of “offset”
- [cosmetic] “list_keys” reworded the description of “api_key”
- [cosmetic] “list_outcomes” reworded the description of “api_key”
- [cosmetic] “list_outcomes” reworded the description of “hook_id”
- [cosmetic] “list_outcomes” reworded the description of “limit”
- [cosmetic] “list_outcomes” reworded the description of “offset”
- [cosmetic] “list_outcomes” reworded the description of “platform”
- [cosmetic] “list_outcomes” reworded the description of “since”
- [cosmetic] “list_webhook_deliveries” reworded the description of “api_key”
- [cosmetic] “list_webhook_deliveries” reworded the description of “limit”
- [cosmetic] “list_webhook_deliveries” reworded the description of “offset”
- [cosmetic] “list_webhook_deliveries” reworded the description of “status”
- [cosmetic] “redrive_webhook_delivery” reworded the description of “api_key”
- [cosmetic] “redrive_webhook_delivery” reworded the description of “delivery_id”
- [cosmetic] “remix_hook” reworded the description of “api_key”
- [cosmetic] “remix_hook” reworded the description of “count”
- [cosmetic] “remix_hook” reworded the description of “idempotency_key”
- [cosmetic] “remix_hook” reworded the description of “tags”
- [cosmetic] “remix_hook” reworded the description of “target_archetype”
- [cosmetic] “remix_hook” reworded the description of “text”
- [cosmetic] “remix_hook” reworded the description of “verbosity”
- [cosmetic] “report_outcome” reworded the description of “api_key”
- [cosmetic] “report_outcome” reworded the description of “hook_id”
- [cosmetic] “report_outcome” reworded the description of “idempotency_key”
- [cosmetic] “report_outcome” reworded the description of “likes”
- [cosmetic] “report_outcome” reworded the description of “platform”
- [cosmetic] “report_outcome” reworded the description of “posted_at”
- [cosmetic] “report_outcome” reworded the description of “retention_pct”
- [cosmetic] “report_outcome” reworded the description of “url”
- [cosmetic] “report_outcome” reworded the description of “views”
- [cosmetic] “revoke_key” reworded the description of “api_key”
- [cosmetic] “revoke_key” reworded the description of “prefix”
- [cosmetic] “score_hook” reworded the description of “api_key”
- [cosmetic] “score_hook” reworded the description of “idempotency_key”
- [cosmetic] “score_hook” reworded the description of “platform”
- [cosmetic] “score_hook” reworded the description of “tags”
- [cosmetic] “score_hook” reworded the description of “text”
- [cosmetic] “score_hook” reworded the description of “verbosity”
- [cosmetic] “score_hooks_batch” reworded the description of “api_key”
- [cosmetic] “score_hooks_batch” reworded the description of “compare”
- [cosmetic] “score_hooks_batch” reworded the description of “idempotency_key”
- [cosmetic] “score_hooks_batch” reworded the description of “platform”
- [cosmetic] “score_hooks_batch” reworded the description of “sets”
- [cosmetic] “score_hooks_batch” reworded the description of “tags”
- [cosmetic] “score_hooks_batch” reworded the description of “texts”
- [cosmetic] “score_hooks_batch” reworded the description of “verbosity”
- [cosmetic] “set_webhook” reworded the description of “api_key”
- [cosmetic] “set_webhook” reworded the description of “url”
- [cosmetic] “signup” reworded the description of “email”
- [cosmetic] “signup” reworded the description of “idempotency_key”
- [cosmetic] “signup” reworded the description of “name”
- [cosmetic] “start_generate_job” reworded the description of “api_key”
- [cosmetic] “start_generate_job” reworded the description of “archetypes”
- [cosmetic] “start_generate_job” reworded the description of “count”
- [cosmetic] “start_generate_job” reworded the description of “idempotency_key”
- [cosmetic] “start_generate_job” reworded the description of “mode”
- [cosmetic] “start_generate_job” reworded the description of “platform”
- [cosmetic] “start_generate_job” reworded the description of “style”
- [cosmetic] “start_generate_job” reworded the description of “tags”
- [cosmetic] “start_generate_job” reworded the description of “topic”
- [cosmetic] “start_generate_job” reworded the description of “topics”
- [cosmetic] “remix_hook” made “target_archetype” optional
- [cosmetic] “remix_hook” made “text” optional

### 2026-08-01 (score 59, +1)

No change was recorded against any check on this day. Stability & Change Management went from 17 to 20. That category is still filling its 30-day observation window: 5 days of observed history at the previous scan, 6 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-07-31 (score 58, +4)

- [functional] We updated how we score, so this day's move reflects our rubric, not a change to the server

### 2026-07-29 (score 54, +1)

No change was recorded against any check on this day. Stability & Change Management went from 7 to 10. That category is still filling its 30-day observation window: 2 days of observed history at the previous scan, 3 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-07-28 (score 53, +1)

No change was recorded against any check on this day. Stability & Change Management went from 3 to 7. That category is still filling its 30-day observation window: 1 days of observed history at the previous scan, 2 at this one. The score rises as the window fills, whether or not the server changes.

### 2026-07-27 (score 52, 0)

- [functional] We updated how we score, so this day's move reflects our rubric, not a change to the server

### 2026-07-26 (score 52)

First indexed and scored.

## MCP tools (38)

### `signup` (~241 tokens)

Create an account and return your API key exactly once. No auth needed.

        The entry point: an agent with nothing calls this and is productive immediately
        (no CAPTCHA); new accounts get 500 credits. An idempotent replay returns the SAME
        account with replayed:true and a NULL api_key (the plaintext is never stored), so
        branch on `replayed`, never on the empty key. Cost: free.
        Errors: invalid_request, idempotency_conflict, rate_limited.

Input parameters:

- `email`: OPTIONAL contact address. Never required, never verified, and never used to authenticate; the api_key is the only credential.
- `idempotency_key`: Caller-chosen replay key (any string, unique per intended effect). A repeat call with the SAME key returns the stored result and is NEVER charged twice; the same key with different arguments is an id…
- `name` (string): Display name for the account, 1-100 chars. Purely a label (it is never checked for uniqueness); defaults to 'agent'.

Output parameters:

- `account_id` (string|null): Your account id. Not a credential.
- `api_key` (string|null): The plaintext key, returned EXACTLY once. NULL on an idempotent replay (the plaintext is never stored), so store it on first sight: it cannot be re-read.
- `api_key_prefix` (string|null): First 12 chars of the key, safe to log and to pass to revoke_key.
- `created_at` (string|null): Account creation time, ISO-8601 UTC.
- `credits` (number|null): Starting balance (500 on a fresh account).
- `docs` (object|null): Where to read more, keyed by topic.
- `memory_card`: A compact block worth persisting into your own memory.
- `next_steps` (array|null): Ordered suggestions for what to call next.
- `note` (string|null): Human-readable summary of the above.
- `replayed` (boolean|null): true when this response replays an earlier signup with the same idempotency_key (and therefore carries no api_key); false on a fresh one.

### `getting_started` (~77 tokens)

The 5-step agent quickstart: modes, wait guidance, links. Free, no auth.

        Written to be parsed and acted on. Returns {what_this_is, five_steps, modes
        (cost + live latency + when to use), wait_guidance (expected_wait, estimates,
        progressToken, jobs), links}.

Output parameters:

- `data_and_deletion` (string|null): Retention and what delete_account keeps.
- `evaluate` (object|null): How to judge output quality, including the public bake-off.
- `five_steps` (array|null): The 5 ordered steps from nothing to hooks.
- `fleet_accounting` (string|null): How tags and per-key caps attribute spend across a fleet.
- `links` (object|null): Docs and endpoint links.
- `memory_card` (string|null): A compact block worth persisting into your own memory.
- `modes` (array|null): One entry PER MODE (a list, not a map): {mode, base, per_hook, formula, p50_ms, p90_ms, latency_source, when_to_use}.
- `payments` (string|null): How to top up: add_credits and create_checkout.
- `response_shaping` (string|null): How verbosity changes what you get back.
- `wait_guidance` (object|null): How to avoid blocking blind: expected_wait, progressToken, jobs.
- `webhooks` (string|null): How to receive job and low-balance events.
- `what_this_is` (string|null): One-paragraph description of the service.

### `list_archetypes` (~77 tokens)

List the hook archetypes with psychology, platforms, and templates. Free, no auth.

        Returns {archetypes:[{id, name, description, psychological_trigger, best_for,
        templates}]}. Use an id for generate_hooks(archetypes=[...]) or
        remix_hook(target_archetype=...).

Output parameters:

- `archetypes` (array|null): Each {id, name, description, psychological_trigger, best_for, templates}. Use `id` for generate_hooks(archetypes=[...]) / remix_hook(target_archetype=...).

### `pricing` (~121 tokens)

The machine-readable price list, with per-mode expected_wait. Free, no auth.

        Returns {unit, usd_per_credit (0 while credits are free), operations,
        pricing_modes:{instant:{base:0,per_hook:1},smart:{base:0,per_hook:2},
        research:{base:10,per_hook:4}} each with a formula, expected_wait, signup_grant,
        low_balance_threshold}. A generate charge is base + per_hook * hooks_returned;
        these are the exact constants the charge path uses.

Output parameters:

- `credit_packs` (array|null): Buyable packs; pass one of these credit amounts as create_checkout(pack=...).
- `expected_wait` (object|null): Per-mode {p50_ms, p90_ms, source} latency, same as get_estimates.
- `low_balance_threshold` (number|null): Balance at which the credits.low webhook fires.
- `operations` (object|null): Per-operation costs for the non-generate tools.
- `payments_enabled` (boolean|null): false when Stripe is not configured: create_checkout then returns payments_disabled and add_credits is the only top-up path.
- `pricing_modes` (object|null): Per mode {base, per_hook, formula, expected_wait}. A generate charge is base + per_hook * hooks_RETURNED.
- `signup_grant` (number|null): Credits a new account starts with.
- `unit` (string|null): Name of the billing unit (credits).
- `usd_per_credit` (number|null): Cash price per credit; 0 while credits are free.

### `get_estimates` (~73 tokens)

The measured/expected wait per generation mode. Free, no auth.

        Size a call before spending. Returns {op, modes:{instant|smart|research:
        {p50_ms, p90_ms, samples, source ("measured" once enough samples, else
        "default"), advice}}}.

Output parameters:

- `modes` (object|null): Per mode {p50_ms, p90_ms, samples, source ('measured' once enough samples exist, else 'default'), advice}.
- `op` (string|null): Which operation these estimates describe.

### `health` (~144 tokens)

Deep health: DB read+write probe, worker/queue, backup + integrity. Free.

        Same db.write_probe/backup.status/backup.offsite_status/integrity.status calls
        GET /health makes, so the two can never disagree. status is "ok" only when the
        DB both reads AND writes. Returns {status, version, time, llm_configured,
        outcomes_reported_total, job_worker_alive, job_queue_depth, last_backup_at,
        backup_count, offsite, integrity}. llm_configured never calls an LLM; CHECK
        job_worker_alive before start_generate_job (false: a queued job never runs).

Output parameters:

- `backup_count` (number|null): How many daily backups are retained. Null when status is unavailable.
- `integrity` (object|null): Weekly PRAGMA quick_check result: {last_check_at, ok}, both null before the first check has ever run. Null (the whole object) when status is unavailable.
- `job_queue_depth` (number|null): Jobs queued and not yet claimed, across all accounts. Null when status is unavailable.
- `job_worker_alive` (boolean|null): Whether this deployment runs the job worker. FALSE means start_generate_job would queue a job nothing executes: use generate_hooks instead.
- `last_backup_at` (string|null): Newest nightly backup's timestamp, ISO-8601 UTC. Null before the first backup runs, or when status is unavailable.
- `llm_configured` (boolean|null): Whether an LLM key is configured. false means smart/research are unavailable and mode=auto resolves to instant. A pure config read, never a live call.
- `offsite` (object|null): Off-volume upload health: {enabled, last_upload_at, last_status, remote_retained}. In-memory, no live S3 call. Null when status is unavailable.
- `outcomes_reported_total` (number|null): Size of the shared outcome corpus (60s-cached COUNT). Null when status is unavailable.
- `status` (string|null): 'ok' only when the DB both READS and WRITES (a write probe, not just a ping); 'unavailable' otherwise.
- `time` (string|null): Server time, ISO-8601 UTC.
- `version` (string|null): Server version.

### `get_account` (~175 tokens)

Return this account's state + remaining rate-limit budget. Free read.

        `rate_limit` carries {limit, remaining, reset_epoch, reset_at, window_seconds} for
        the per-account window, the same budget REST clients read from X-RateLimit-* headers.
        Pace a fleet off `remaining` instead of discovering the ceiling by taking a
        rate_limited mid-run; this read itself consumes one of those calls. No API key is
        ever echoed back. Errors: unauthorized, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…

Output parameters:

- `account_id` (string|null): Your account id.
- `api_key_prefix` (string|null): Prefix of the key that authenticated this call. Never the key itself.
- `created_at` (string|null): Account creation time, ISO-8601 UTC.
- `credits` (number|null): Current balance.
- `email` (string|null): Contact address if one was given at signup.
- `name` (string|null): Display name given at signup.
- `rate_limit` (object|null): {limit, remaining, reset_epoch, reset_at, window_seconds} for the per-account window. Pace off `remaining`; limit 0 means limiting is disabled and remaining/reset are null.

### `get_usage` (~425 tokens)

Return the credit balance, per-operation totals, and recent ledger. Free.

        Args: recent_limit (1-200, default 50), offset (>=0, pages `recent` past the newest
        rows), request_id (scope recent to that call's charges), tag + key_prefix (WP-J
        fleet filters), api_key. Returns {credits, totals:{by_operation}, recent:[...]}
        (each row carries key_prefix; tags in metadata).
        Errors: unauthorized, invalid_request, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `key_prefix`: Filter to charges made with ONE api key, identified by the 12-char prefix from list_keys (e.g. vhg_sk_ab12). Omit for every key.
- `offset` (integer): Rows to skip before `recent` starts, for paging past the newest page. Page with offset += recent_limit, exactly like list_hooks and list_outcomes; an offset past the end is an empty `recent`, never a…
- `recent_limit` (integer): Max recent ledger rows to return, 1-200. Totals are unaffected by this; it only sizes `recent`.
- `request_id`: Scope `recent` to the charges of ONE earlier call: pass the request_id that call RETURNED (or its X-Request-Id header), never a freshly minted id. An id that charged nothing matches no rows and comes…
- `tag`: Filter to rows carrying EXACTLY this fleet tag (exact match, not a substring). Omit for every tag.

Output parameters:

- `credits` (number|null): Current balance.
- `recent` (array|null): Recent charge rows, newest first, sized by recent_limit. Each carries the operation, credits, key_prefix, request_id and tags in metadata.
- `totals` (object|null): {by_operation: {op: {calls, credits}}} over the whole account lifetime.
- `unreported_hooks` (object|null): How many bought hooks still have no outcome, i.e. how much free reward is on the table. Find them with list_hooks(unreported=true).

### `export_usage` (~291 tokens)

Export EVERYTHING this account owns in ONE body: ledger, hooks, outcomes. Free.

        The take-it-with-you call; make it BEFORE delete_account. Same body as GET
        /v1/usage/export?format=json (csv is a download shape with no MCP carrier and its
        columns are a subset of `events`). UNBOUNDED, no paging: on a big account prefer
        get_usage / list_hooks / list_outcomes. `email` is masked without the admin scope.
        Returns {account_id, email, count, events, hooks, outcomes}; tag/key_prefix narrow events.
        Errors: unauthorized, forbidden (key lacks the read scope), rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `key_prefix`: Narrow the LEDGER to charges made with ONE api key, identified by the 12-char prefix from list_keys. `hooks` and `outcomes` are always the full set and are NOT narrowed by this. Omit for every key.
- `tag`: Filter to rows carrying EXACTLY this fleet tag (exact match, not a substring). Omit for every tag.

Output parameters:

- `account_id` (string|null): Your account id.
- `count` (number|null): How many ledger rows `events` carries.
- `email` (string|null): Contact address given at signup, MASKED (z***@d***.com) unless the calling key has the admin scope. Null when none was given.
- `events` (array|null): The FULL charge ledger, oldest first, narrowed by tag/key_prefix. Each {id, created_at, operation, credits_delta, balance_after, request_id, key_prefix, metadata}. This is the superset of the REST cs…
- `hooks` (array|null): EVERY hook this account bought: {hook_id, request_id, topic, mode, platform, text, archetype, claim_type, score_total, score, contains_placeholder_stat, prompt_version, tags, created_at}. NOT narrowe…
- `outcomes` (array|null): EVERY outcome this account reported, each with the hook snapshot taken at report time. NOT narrowed by tag/key_prefix.

### `list_runs` (~459 tokens)

Every call this account was charged for, newest first. Free read.

        Recover a lost response in two calls, never re-charged: list_runs(request_id=...)
        for the receipt, then follow `hooks_url` for the hooks it produced. Page with
        `cursor` (one pass total) or `offset`; stop only when `exhausted` is true, never
        on a short page. Same composed read GET /v1/runs makes, so the two can never
        disagree. Errors: unauthorized, invalid_request, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `charged_only` (boolean): true (default): only calls that COST credits. false: also include grants and zero-cost calls.
- `cursor` (integer): Ledger position to resume scanning from (see `next_cursor` on a prior page); not a run count, not an opaque token. Leave at 0 and follow `next_cursor` to walk your whole history in bounded reads.
- `limit` (integer): Max runs to return, 1-200. Above the ceiling is an invalid_request, never a silent truncation.
- `offset` (integer): Number of rows to skip for paging, 0-9223372036854775807. Page with offset += the limit you actually requested; `total` in the response is the unpaged count. The ceiling is SQLite's largest bindable…
- `operation`: Return only runs of this operation: generate_hooks, generate_hooks_batch, remix_hook, score_hook, score_hooks_batch, add_credits. An unknown value returns an empty page, never an error.
- `request_id`: Return only the run with this X-Request-Id (the response header on every call, including errors). This is the lookup for a lost response whose id you still have.

Output parameters:

- `credits_remaining` (number|null): Your balance right now.
- `exhausted` (boolean|null): true only when the read reached the END of your ledger. `total` is the COMPLETE count only when this is true.
- `has_more` (boolean|null): true when this page is not the whole remainder. Follow `next_cursor` (preferred) or `next_offset`; never infer 'that was all' from a short page.
- `limit` (number|null): Page size actually applied.
- `next_cursor` (number|null): Pass as `cursor` for the next page (leave offset at 0); null when nothing follows. PREFER this over next_offset for a full walk: it resumes exactly where this page stopped instead of re-scanning from…
- `next_offset` (number|null): Pass as `offset` for the next page; null when nothing follows.
- `offset` (number|null): Offset this page started at.
- `runs` (array|null): This page: {request_id, operation, created_at, credits_delta, credits_charged, balance_after, key_prefix, topic, topics, platform, mode, hooks_returned, tags, hooks_url, usage_url, metadata}. hooks_u…
- `scan_limit` (number|null): Ledger rows one page may examine, however many reads that takes.
- `scanned` (number|null): Ledger rows examined to build this page.
- `total` (number|null): Rows matching the filters WITHIN the scanned window (see `scanned`), not over all history unless `exhausted` is also true.

### `quote` (~455 tokens)

Price and time a generate BEFORE you commit to it, without spending. Free.

        Answers what you cannot otherwise learn without paying: which engine will BILL
        (mode=auto resolves here), credits_max, expected_wait, and whether this key can
        afford it now (balance AND any daily account/key cap, spelled out in `blocker`).
        Same validation as generate_hooks, so a bad topic/archetype/mode fails HERE.
        credits_max is a CEILING (you pay per hook RETURNED) and nothing is reserved.
        Errors: unauthorized, invalid_request, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `archetypes`: Restrict generation to these archetype ids (see list_archetypes). Omit to let the engine spread across archetypes, which is what you want unless you are deliberately narrowing a deck.
- `count` (integer): How many hooks you intend to ask for, 1-25. Drives the quoted ceiling: credits_max = base + per_hook * count.
- `mode`: Generation engine and therefore the price: instant (0 base + 1/hook, deterministic templates, sub-second), smart (0 + 2/hook, one LLM call, seconds), research (10 + 4/hook, brief->draft->judge, tens…
- `platform` (string): Target platform, which selects the length/format conventions the hooks are written and scored against. Defaults to tiktok when omitted.
- `topic` (string, required): The topic you intend to generate for, 3-200 chars. It is fully VALIDATED here, so a bad topic is an invalid_request now instead of after you commit to spending.

Output parameters:

- `affordable` (boolean|null): true when a generate with these arguments would pass every pre-flight gate (balance AND any daily account/key spend cap). Branch on THIS.
- `base_price` (number|null): Fixed part of the charge for pricing_mode.
- `blocker`: Null when affordable. Otherwise the SAME typed error block generate_hooks would have returned ({code, message, retriable, details, ...}), whose details.hint names the exact fix.
- `count` (number|null): The hook count quoted.
- `credits_max` (number|null): The CEILING: base_price + price_per_hook * count. The real charge bills hooks RETURNED, so a short deck costs less. Never more than this.
- `credits_remaining` (number|null): Your balance right now.
- `credits_short` (number|null): How many credits you are missing; 0 when affordable. Against a daily cap this is the gap to the remaining daily headroom, not to your balance.
- `expected_wait` (object|null): {mode, p50_ms, p90_ms, source} for pricing_mode. Compare p90_ms against your own patience to choose sync vs start_generate_job + wait_for_job.
- `formula` (string|null): The exact charge formula, so you can predict any other count.
- `llm_configured` (boolean|null): Whether this deployment has an LLM key. false means smart/research are unavailable and mode=auto resolves to instant.
- `mode_requested` (string|null): The mode string you passed, before resolution; null if omitted.
- `operation` (string|null): The operation this quote prices ('generate_hooks').
- `platform` (string|null): The platform quoted.
- `price_per_hook` (number|null): Per-hook part of the charge for pricing_mode.
- `pricing_mode` (string|null): The tier that will actually be BILLED after aliases and auto were resolved: instant|smart|research. This, not mode_requested, is what the price belongs to.
- `recommendation` (string|null): One sentence naming the next call to make.
- `topic` (string|null): The topic as validated (echoed back).

### `add_credits` (~353 tokens)

Top up your credit balance (1-10000). Self-serve by default.

        When VHGENGINE_ADMIN_KEY is configured on the deployment this requires a
        matching admin_key argument; otherwise it stays self-serve (unless
        VHGENGINE_FREE_CREDITS is off). Idempotent on idempotency_key (replay does NOT
        grant twice). Returns {credits (new balance), granted}. Errors: unauthorized
        (missing/wrong admin key or self-serve disabled), invalid_request (amount range
        / balance ceiling), idempotency_conflict, rate_limited.

Input parameters:

- `admin_key`: The deployment's VHGENGINE_ADMIN_KEY. Required ONLY when the operator configured one; omit on a self-serve deployment. A wrong or missing value where one is configured is unauthorized.
- `amount` (integer, required): Credits to add, 1-10000. Charged operations spend these; see pricing for the per-mode cost. The grant also has a per-account balance ceiling, so a large top-up on a full balance is invalid_request.
- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `idempotency_key`: Caller-chosen replay key (any string, unique per intended effect). A repeat call with the SAME key returns the stored result and is NEVER charged twice; the same key with different arguments is an id…

Output parameters:

- `credits` (number|null): Balance AFTER the grant.
- `granted` (number|null): Credits added by this call; 0 on an idempotent replay.
- `replayed` (boolean|null): true when an earlier call with the same idempotency_key already granted.

### `list_billing_events` (~251 tokens)

Recent billing events (usage.recorded, credits.granted, credits.low). Free.

        Newest first, paged with limit/offset like list_hooks and list_outcomes. Returns
        {events:[{id, event_type, payload, created_at}], limit, offset, total}.
        Errors: unauthorized, invalid_request, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `limit` (integer): Max billing events to return, 1-200. Above the ceiling is an invalid_request, never a silent truncation.
- `offset` (integer): Number of rows to skip for paging, 0-9223372036854775807. Page with offset += the limit you actually requested; `total` in the response is the unpaged count. The ceiling is SQLite's largest bindable…

Output parameters:

- `events` (array|null): This page: {id, event_type, payload, created_at}.
- `limit` (number|null): Page size actually applied.
- `offset` (number|null): Offset this page started at.
- `total` (number|null): Rows matching the filters IGNORING paging.

### `create_checkout` (~298 tokens)

Get a Stripe hosted-checkout link for a credit pack. You never touch a card (WP-PAY).

        Hand the returned checkout_url to your human; the webhook credits you after they pay.
        Args: pack (a credits amount from pricing.credit_packs), success_url/cancel_url
        (optional), api_key (spend scope). Returns {checkout_url, pack, credits, usd_cents,
        expires_at}. Errors: unauthorized, forbidden, invalid_request (bad pack),
        payments_disabled (503; use add_credits), rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `cancel_url`: Where Stripe sends the browser if your human abandons checkout. At most 2048 characters. Omit to use the deployment's default.
- `pack` (integer, required): Which credit pack to buy, given as its CREDITS amount and matched against pricing.credit_packs exactly (not a dollar figure and not an index). A value that is not an offered pack is invalid_request.
- `success_url`: Where Stripe sends the browser after a successful payment. At most 2048 characters. Omit to use the deployment's default landing page.

Output parameters:

- `checkout_url` (string|null): The URL your human opens to pay. Single use.
- `credits` (number|null): Credits that land on your balance after the webhook confirms payment.
- `expires_at` (string|null): When the checkout link stops working, ISO-8601 UTC.
- `pack` (number|null): Which pack this session buys.
- `usd_cents` (number|null): What your human is charged, in cents.

### `create_key` (~335 tokens)

Mint a new named API key; the plaintext is returned ONCE. Requires admin scope.

        Delegate safely (WP-SCOPE): optional scopes (subset of read|spend|admin; omit for
        full power) + daily_credit_cap (credits/day; omit for uncapped) hand a sub-agent a
        key that can only do what you allow. Args: name (1-100), api_key (an admin-scoped
        key). Returns {api_key (store it), prefix, name, scopes, daily_credit_cap,
        created_at}. Errors: unauthorized, forbidden, invalid_request, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `daily_credit_cap`: Credits this key may spend per UTC day, 1-100000; further charges on it are refused once reached (other keys are unaffected). Omit for uncapped. list_keys reports spent_today against this.
- `name` (string): Label for the new key, 1-100 chars, shown by list_keys so you can tell delegated keys apart. Defaults to 'key'.
- `scopes`: Powers the new key gets: read (free reads), spend (charged generate/score/remix), admin (key + account + webhook management). OMIT for a full-power key; pass a subset to hand a sub-agent strictly les…

Output parameters:

- `api_key` (string|null): The plaintext key. Store it now; it is never readable again.
- `created_at` (string|null): Creation time, ISO-8601 UTC.
- `daily_credit_cap` (number|null): Per-UTC-day spend ceiling, or null for uncapped.
- `name` (string|null): The label you gave it.
- `prefix` (string|null): First 12 chars, used by list_keys / revoke_key / get_usage(key_prefix=...).
- `scopes` (array|null): Powers granted, a subset of read/spend/admin (all three = full power).

### `list_keys` (~170 tokens)

List this account's API keys as PREFIXES only (never the raw key). Admin scope.

        Returns {keys:[{prefix, name, scopes, daily_credit_cap, spent_today, created_at,
        revoked_at}]}, oldest first; revoked_at is null for an active key, scopes lists the
        key's grant (WP-SCOPE), spent_today is its credits spent since UTC midnight. Your
        signup key shows as name "default". Errors: unauthorized, forbidden, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…

Output parameters:

- `keys` (array|null): Oldest first. Each {prefix, name, scopes, daily_credit_cap, spent_today, created_at, revoked_at}; revoked_at null means still active, and the signup key shows as name 'default'.

### `revoke_key` (~238 tokens)

Revoke the key with `prefix` (from list_keys): it stops working, others keep working.

        Use to kill a leaked or old key. Args: prefix (the 12-char key prefix, e.g.
        vhg_sk_ab12), api_key (a DIFFERENT active key to authenticate this call).
        Returns {prefix, name, revoked, revoked_at, already_revoked}. You cannot revoke
        your LAST active key (create a replacement first). Requires the admin scope. Errors:
        unauthorized, forbidden, invalid_request (last key), not_found, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `prefix` (string, required): The 12-char key prefix to kill, copied from list_keys (e.g. vhg_sk_ab12), NOT the plaintext key. Revoking is idempotent; you cannot revoke your last active key.

Output parameters:

- `already_revoked` (boolean|null): true when it was already revoked, i.e. this call changed nothing.
- `name` (string|null): Its label.
- `prefix` (string|null): The key that was revoked.
- `revoked` (boolean|null): Whether the key is now revoked (true after a successful call).
- `revoked_at` (string|null): When it was revoked, ISO-8601 UTC.

### `delete_account` (~242 tokens)

Permanently delete this account. TERMINAL, not reversible. Admin scope (WP-LIFE).

        Pass confirm="delete" (guards an accidental call). Revokes ALL your keys, deletes
        your hooks, webhook, deliveries, jobs and idempotency rows, KEEPS the immutable
        financial ledger, and ANONYMIZES your outcomes into the shared corpus. Take what
        you still want FIRST: export_usage returns the ledger, every hook and every outcome
        in ONE call (get_usage / list_hooks / list_outcomes are the paged alternative).
        Errors: unauthorized, forbidden, invalid_request, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `confirm` (string, required): Must be exactly the string "delete". A guard: any other value (or omitting it) fails argument validation before anything is touched, so an accidental invocation cannot destroy the account.

Output parameters:

- `deleted` (boolean|null): true once the account is tombstoned. Every key now 401s.
- `note` (string|null): Human-readable summary of what was and was not removed.
- `retained` (object|null): {financial_events, anonymized_outcomes}: the immutable ledger is kept and your outcomes are anonymized into the shared corpus rather than destroyed.

### `generate_hooks` (~800 tokens)

Generate ranked, scored viral hooks for a topic. Charged by mode.

        Cost = base + per_hook * hooks_returned (instant 0+1, smart 0+2, research 10+4,
        ~50 at count=10); a pre-flight gate never bills a broke account. Deterministic
        only in instant; a progressToken streams stages; the hook id is `hooks[].id`.
        CANCELLING DOES NOT REFUND: a timeout cannot stop the engine thread, so the call
        still charges and persists. Send idempotency_key, or recover via list_hooks.
        Errors: unauthorized, invalid_request, llm_unavailable, insufficient_credits.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `archetypes`: Restrict generation to these archetype ids (see list_archetypes). Omit to let the engine spread across archetypes, which is what you want unless you are deliberately narrowing a deck.
- `count` (integer): How many hooks to generate, 1-25. Drives the price (base + per_hook * hooks_RETURNED) and the pre-flight reservation, so a high count on research is the expensive combination.
- `deadline_ms`: Milliseconds you are willing to block, 1000-600000. If the chosen mode's p90 exceeds it the call returns IMMEDIATELY with auto_job:true + a job_id to poll with get_job instead of generating inline. O…
- `idempotency_key`: Caller-chosen replay key (any string, unique per intended effect). A repeat call with the SAME key returns the stored result and is NEVER charged twice; the same key with different arguments is an id…
- `mode`: Generation engine and therefore the price: instant (0 base + 1/hook, deterministic templates, sub-second), smart (0 + 2/hook, one LLM call, seconds), research (10 + 4/hook, brief->draft->judge, tens…
- `platform` (string): Target platform, which selects the length/format conventions the hooks are written and scored against. Defaults to tiktok when omitted.
- `style`: Voice/tone to match, <=200 chars. Honored as a real instruction by smart and research; on instant it only varies which deterministic template fillers are drawn, so it cannot change the voice there. O…
- `tags`: 1-5 lowercase slug tags ([a-z0-9_-], <=40 chars) stamped on this call's usage event so a fleet can attribute spend per campaign. Omit for no tagging. Filter later with get_usage(tag=...) / list_hooks…
- `topic` (string, required): What the hooks are about, 3-200 chars. A concrete subject ('cold plunges for desk workers') scores far better than a bare noun; on research it is also what the brief is researched against.
- `verbosity` (string): How much of the response envelope to return: minimal (ids/text/totals only), standard (the default), full (adds per-dimension score attribution and timings). Shapes the RESPONSE only, never what is g…

Output parameters:

- `auto_job` (boolean|null): Present and true when deadline_ms made this return a JOB instead of hooks. CHECK IT BEFORE READING `hooks`: on this path `hooks` is ABSENT and the body is the job pointer below (job_id, status, estim…
- `base_price` (number|null): Fixed part of the charge for this mode.
- `count_requested` (number|null): The count you asked for, echoed so you never have to diff an array length against your own request.
- `credits_charged` (number|null): Credits this call actually cost.
- `credits_remaining` (number|null): Your balance AFTER this charge.
- `degraded_reason` (string|null): verbosity=minimal ONLY, and only when non-null: hoisted out of `research` so the trimmed envelope still says WHY the run degraded. At standard/full read research.degraded_reason instead.
- `degraded_stages` (array|null): verbosity=minimal ONLY, and only when non-empty: hoisted out of `research`; which stages to blame. At standard/full read research.degraded_stages.
- `engine` (string|null): Which generator implementation produced the deck.
- `estimated_seconds` (number|null): auto_job path only: queue-aware estimate of total time to a result.
- `expected_wait` (object|null): {mode, p50_ms, p90_ms, source} for this mode, to size the NEXT call.
- `expires_at` (string|null): auto_job path only: when an unexecuted job is abandoned, ISO-8601 UTC.
- `grounding_refund` (object|null): Non-null ONLY when you asked for research and the VENDOR REFUSED the web-search tool: the full pipeline still ran, so you are not billed as smart, but part of the research premium is waived. {credits…
- `hint` (string|null): auto_job path only: what to do next, in one sentence.
- `hooks` (array|null): Ranked hooks, best first. Each is {id, text, archetype, score, claim_type, rank}. NOTE the key is `id`, and THAT is the hook id you pass to report_outcome(hook_id=...) / get_hook (where the same valu…
- `job_id` (string|null): auto_job path only: poll it with get_job or block on wait_for_job.
- `judge_fallback_reason` (string|null): smart/research only: why the judge did NOT rank this set, or null when it ran. Non-null means these score.totals are heuristic-scale, so never compare them against a judged run's totals.
- `llm_fallback` (boolean|null): true when the requested LLM tier was unavailable and a cheaper engine ran instead; llm_fallback_reason says why. You are billed for what RAN.
- `llm_fallback_reason` (string|null): Why the fallback happened, or null.
- `mode_requested` (string|null): The mode string you sent, before alias/auto resolution.
- `mode_used` (string|null): The engine that actually ran (instant|smart|research) after aliases and auto were resolved. May differ from mode_requested.
- `poll_after_seconds` (number|null): auto_job path only: wait at least this long before the first get_job.
- `price_per_hook` (number|null): Per-hook part of the charge for this mode.
- `pricing_mode` (string|null): The tier billed, which is what price_per_hook belongs to.
- `prompt_version` (string|null): Prompt build used, for reproducibility.
- `rank_basis` (string|null): verbosity=minimal ONLY, alongside degraded_stages: hoisted out of `research`; what the served ORDER means (llm_judge | judge_order_no_scores | heuristic | unranked). At standard/full read research.ra…
- `reason` (string|null): instant only, non-null when the template pool ran out: prose saying hooks were WITHHELD rather than duplicated. On a clause-topic it says outright that widening `archetypes` will not help, so do not…
- `replayed` (boolean|null): true when an idempotency_key replayed a stored result, so nothing was charged again. The credits_charged below is what the ORIGINAL call cost.
- `replayed_at_charge` (boolean|null): true when the replay was detected at the charge boundary rather than up front; either way you are billed exactly once.
- `request_id` (string|null): Id of this call. Keep it: get_usage(request_id=...) itemises exactly what it charged, and it identifies the call in a support question.
- `requeued` (boolean|null): auto_job path only: true when an idempotent resubmit revived an existing job.
- `research`: mode research ONLY (null on instant/smart). The grounding ledger you paid for. Keys, and what to DO with each: - brief_summary (str), angles (list of {angle, detail, why_it_stops_scroll}), vocabulary…
- `score_disclaimer` (string|null): The honest limits of the scores above. Absent at verbosity=minimal.
- `shortfall` (object|null): {requested, returned, cause} on EVERY response. `cause` is a closed vocabulary: 'none' on a full delivery, else template_pool / model_under_delivery / pool_exhausted. Branch on cause; never retry on…
- `status` (string|null): auto_job path only: the job's lifecycle state ('queued').
- `status_url` (string|null): auto_job path only: REST URL for the same status (HTTP clients).
- `timing` (object|null): What actually happened: {latency_ms, stages}. `stages` keys are mode specific (instant render_ms; smart llm_ms/judge_ms; research brief_ms/draft_ms/judge_ms).
- `usage` (object|null): LLM token usage {input_tokens, output_tokens, est_cost_usd} (+ web_searches and fallback_* counters when those paths ran). NULL means NO LLM ran, which is not the same as zero tokens.

### `generate_hooks_batch` (~592 tokens)

Generate hooks for many topics in ONE call, charged all-or-nothing.

        One transaction: over-balance charges nothing; each item bills base + per_hook *
        hooks_returned for the engine that ran (divergent tiers -> pricing_mode "mixed").
        One mode + idempotency_key + tags cover the batch; a progressToken streams item i/n;
        hook ids are results[i].hooks[j].id. CANCELLING DOES NOT REFUND: a timeout cannot
        stop the engine thread, so the batch still charges and persists. Send idempotency_key.
        Errors: unauthorized, invalid_request, llm_unavailable, insufficient_credits.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `idempotency_key`: Caller-chosen replay key (any string, unique per intended effect). A repeat call with the SAME key returns the stored result and is NEVER charged twice; the same key with different arguments is an id…
- `mode`: Generation engine and therefore the price: instant (0 base + 1/hook, deterministic templates, sub-second), smart (0 + 2/hook, one LLM call, seconds), research (10 + 4/hook, brief->draft->judge, tens…
- `platform` (string): Target platform, which selects the length/format conventions the hooks are written and scored against. Defaults to tiktok when omitted.
- `requests` (array, required): 1-20 items, each {topic, count?, archetypes?, style?}. Per-item `mode` is rejected: mode, tags, verbosity and idempotency_key are set ONCE for the whole batch. Unknown item keys are invalid_request.
- `tags`: 1-5 lowercase slug tags ([a-z0-9_-], <=40 chars) stamped on this call's usage event so a fleet can attribute spend per campaign. Omit for no tagging. Filter later with get_usage(tag=...) / list_hooks…
- `verbosity` (string): How much of the response envelope to return: minimal (ids/text/totals only), standard (the default), full (adds per-dimension score attribution and timings). Shapes the RESPONSE only, never what is g…

Output parameters:

- `base_price` (number|null): Fixed part of the charge, or null when pricing_mode is mixed.
- `credits_charged` (number|null): Credits this call actually cost.
- `credits_remaining` (number|null): Your balance AFTER this charge.
- `degraded_any` (boolean|null): Wider signal than judge_fallback_any: true when any item ran off-tier for any reason, including a partial stage that still delivered a full hook count.
- `degraded_reason` (string|null): verbosity=minimal ONLY, and only when non-null: hoisted out of `research` so the trimmed envelope still says WHY the run degraded. At standard/full read research.degraded_reason instead.
- `degraded_stages` (array|null): verbosity=minimal ONLY, and only when non-empty: hoisted out of `research`; which stages to blame. At standard/full read research.degraded_stages.
- `engine` (string|null): The generator implementation, or 'mixed' when items diverged.
- `expected_wait` (object|null): {mode, p50_ms, p90_ms, source} for the batch's tier.
- `judge_fallback_any` (boolean|null): true when at least one item's hooks are HEURISTIC-scale because its judge did not rank them. Then results[i].judge_fallback_reason says which, and you must not rank topics by comparing avg(score.tota…
- `llm_fallback` (boolean|null): true when ANY item fell back to a cheaper engine or a fallback provider.
- `llm_fallback_reason` (string|null): Why the first such fallback happened, or null.
- `mode_requested` (string|null): The mode string you sent, before alias/auto resolution.
- `mode_used` (string|null): The engine that ran, after aliases and auto were resolved; 'mixed' when items diverged.
- `price_per_hook` (number|null): Per-hook part of the charge, or null when pricing_mode is mixed.
- `pricing_mode` (string|null): The tier billed across the batch, or 'mixed' when items fell back to different engines. On 'mixed', base_price/price_per_hook are null and each results[i] carries its own.
- `prompt_version` (string|null): Prompt build used, for reproducibility.
- `rank_basis` (string|null): verbosity=minimal ONLY, alongside degraded_stages: hoisted out of `research`; what the served ORDER means (llm_judge | judge_order_no_scores | heuristic | unranked). At standard/full read research.ra…
- `replayed` (boolean|null): true when an idempotency_key replayed a stored result, so nothing was charged again. The credits_charged below is what the ORIGINAL call cost.
- `replayed_at_charge` (boolean|null): true when the replay was detected at the charge boundary rather than up front; either way you are billed exactly once.
- `request_id` (string|null): Id of this call. Keep it: get_usage(request_id=...) itemises exactly what it charged, and it identifies the call in a support question.
- `results` (array|null): One entry per item, in request order: {topic, hooks, pricing_mode, price_per_hook, base_price, credits, grounding_refund, timing, judge_fallback_reason, count_requested, shortfall, reason, research}.…
- `score_disclaimer` (string|null): The honest limits of the scores above. Absent at verbosity=minimal.
- `timing` (object|null): {latency_ms} for the whole batch.

### `score_hook` (~598 tokens)

Score any hook text on 5 dimensions with rewrite tips. 1 credit.

        Deterministic heuristic scorer (no LLM). Args: text (3-300), platform, topic
        (optional, reproduces generate's score via the verbatim-echo penalty), tags (1-5
        fleet slugs, WP-J), verbosity (full keeps per-dimension attribution), api_key,
        idempotency_key (replay not re-charged). Returns {score:{...,total}, verdict,
        suggestions, confidence, disclaimer, credits_charged, credits_remaining, request_id}.
        Errors: unauthorized, invalid_request, insufficient_credits, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `idempotency_key`: Caller-chosen replay key (any string, unique per intended effect). A repeat call with the SAME key returns the stored result and is NEVER charged twice; the same key with different arguments is an id…
- `platform` (string): Target platform, which selects the length/format conventions the hooks are written and scored against. Defaults to tiktok when omitted.
- `tags`: 1-5 lowercase slug tags ([a-z0-9_-], <=40 chars) stamped on this call's usage event so a fleet can attribute spend per campaign. Omit for no tagging. Filter later with get_usage(tag=...) / list_hooks…
- `text` (string, required): The hook line to score, 3-300 chars. Any text is accepted (it does not have to be one VHGENGINE generated) and it is never persisted as a hook you own.
- `topic`: The subject this text was written FOR. Pass the SAME topic string a generate call used and this reproduces the exact score.total that generate served for the hook: the scorer's verbatim-echo penalty…
- `verbosity` (string): How much of the response envelope to return: minimal (ids/text/totals only), standard (the default), full (adds per-dimension score attribution and timings). Shapes the RESPONSE only, never what is g…

Output parameters:

- `confidence` (string|null): How much weight to put on this score.
- `credits_charged` (number|null): Credits this call actually cost.
- `credits_remaining` (number|null): Your balance AFTER this charge.
- `disclaimer` (string|null): The honest limits of the scorer. Worth surfacing to your human.
- `replayed` (boolean|null): true when an idempotency_key replayed a stored result, so nothing was charged again. The credits_charged below is what the ORIGINAL call cost.
- `replayed_at_charge` (boolean|null): true when the replay was detected at the charge boundary rather than up front; either way you are billed exactly once.
- `request_id` (string|null): Id of this call. Keep it: get_usage(request_id=...) itemises exactly what it charged, and it identifies the call in a support question.
- `score` (object|null): Per-dimension scores plus `total`.
- `suggestions` (array|null): Concrete rewrite tips.
- `verdict` (string|null): One-line read of the total.

### `score_hooks_batch` (~688 tokens)

Score many hooks ranked with `best`, or compare named SETS. 1 credit per text.

        All-or-nothing charge. Plain: texts (1-25, each 3-300 chars). Self-test (E13):
        compare=true + sets (2-4 named lists, <=25 texts total) INSTEAD of texts -> per-set
        rankings + avg_score + winner + an honest winner_summary (same heuristic scorer
        on every set, never view prediction). Also: platform, tags, verbosity, api_key,
        idempotency_key. Returns {results, best, ...} or {sets, winner, winner_summary, ...}.
        Errors: unauthorized, invalid_request, insufficient_credits, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `compare` (boolean): Switch on compare mode, which requires `sets` and returns per-set rankings + a winner instead of one flat ranking. Leave false for the ordinary texts ranking.
- `idempotency_key`: Caller-chosen replay key (any string, unique per intended effect). A repeat call with the SAME key returns the stored result and is NEVER charged twice; the same key with different arguments is an id…
- `platform` (string): Target platform, which selects the length/format conventions the hooks are written and scored against. Defaults to tiktok when omitted.
- `sets`: Compare mode input: 2-4 NAMED variant lists, e.g. {"curiosity": ["..."], "contrarian": ["..."]}, each 1-25 texts and <=25 texts across all sets. Requires compare=true and excludes `texts`. Every set…
- `tags`: 1-5 lowercase slug tags ([a-z0-9_-], <=40 chars) stamped on this call's usage event so a fleet can attribute spend per campaign. Omit for no tagging. Filter later with get_usage(tag=...) / list_hooks…
- `texts`: 1-25 hook lines (3-300 chars each) to score and rank. Use this OR sets, never both: texts is the plain ranked mode, sets is the compare mode. 1 credit per text either way.
- `topic`: What the hooks are about, 3-200 chars. Supply it to score these lines the way they were generated: the scorer penalises a line that only echoes its own topic back, and it cannot apply that penalty to…
- `verbosity` (string): How much of the response envelope to return: minimal (ids/text/totals only), standard (the default), full (adds per-dimension score attribution and timings). Shapes the RESPONSE only, never what is g…

Output parameters:

- `best`: Plain mode: the highest-scoring entry.
- `credits_charged` (number|null): Credits this call actually cost.
- `credits_remaining` (number|null): Your balance AFTER this charge.
- `replayed` (boolean|null): true when an idempotency_key replayed a stored result, so nothing was charged again. The credits_charged below is what the ORIGINAL call cost.
- `replayed_at_charge` (boolean|null): true when the replay was detected at the charge boundary rather than up front; either way you are billed exactly once.
- `request_id` (string|null): Id of this call. Keep it: get_usage(request_id=...) itemises exactly what it charged, and it identifies the call in a support question.
- `results` (array|null): Plain mode: one scored entry per text, ranked.
- `sets` (object|null): Compare mode: per-set rankings and avg_score, keyed by your set names.
- `winner` (string|null): Compare mode: the winning set name.
- `winner_summary` (string|null): Compare mode: an honest reading of the win. The same heuristic scorer runs on every set, so this is a craft comparison, never a view prediction.

### `remix_hook` (~860 tokens)

Rewrite an existing hook into a target archetype. 2 credits per variant.

        Args: EXACTLY one of text or hook_id; target_archetype OR its alias archetype
        (disagreeing spellings are a 400); count, tags, verbosity, api_key, idempotency_key.
        Deterministic; billed only for variants returned. Returns {original:{text, score},
        remixes:[{id, text, archetype, score, rank}], topic_core, reason, score_disclaimer,
        credits_charged, credits_remaining, request_id}. Errors: unauthorized,
        invalid_request, not_found, insufficient_credits, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `archetype`: Alias for `target_archetype`, spelled the way every hook object in every response spells it. Send either one; sending both is fine only if they are equal, and a disagreement is a typed invalid_reques…
- `count` (integer): How many variants to produce, 1-5, at 2 credits each. You are billed only for variants actually returned.
- `hook_id`: Remix a hook you ALREADY bought, by the id a generate/batch/remix response returned (also listed by list_hooks): the stored text is looked up for you, so you do not have to carry it back. Account-sco…
- `idempotency_key`: Caller-chosen replay key (any string, unique per intended effect). A repeat call with the SAME key returns the stored result and is NEVER charged twice; the same key with different arguments is an id…
- `platform`: The platform whose SCORING WINDOW grades these variants, as on generate_hooks and score_hook: LinkedIn's ideal hook length is 10-18 words against 8-14 elsewhere, so it moves both score.total and the…
- `tags`: 1-5 lowercase slug tags ([a-z0-9_-], <=40 chars) stamped on this call's usage event so a fleet can attribute spend per campaign. Omit for no tagging. Filter later with get_usage(tag=...) / list_hooks…
- `target_archetype`: Archetype id to rewrite INTO (see list_archetypes for ids and their psychology). `archetype` is an accepted alias for this argument; one of the two is required, there is no default angle.
- `text`: The existing hook to rewrite, 3-300 chars. Its subject is kept; only the angle moves to target_archetype. Pass this OR `hook_id`, never both and never neither.
- `topic`: The subject the hook was written FOR, scored as on score_hook: the verbatim-echo penalty only fires when the scorer is told the topic, so a hook (or a variant) that repeats its own subject scores up…
- `verbosity` (string): How much of the response envelope to return: minimal (ids/text/totals only), standard (the default), full (adds per-dimension score attribution and timings). Shapes the RESPONSE only, never what is g…

Output parameters:

- `credits_charged` (number|null): Credits this call actually cost.
- `credits_remaining` (number|null): Your balance AFTER this charge.
- `llm_fallback` (boolean|null): true when the LLM remixer was unavailable and the deterministic one ran; llm_fallback_reason says why. You are billed for what RAN.
- `llm_fallback_reason` (string|null): Why the fallback happened, or null.
- `original` (object|null): {text, score} of what you passed in.
- `platform` (string|null): The platform scoring window every total above was produced on. Score a variant through score_hook with this SAME platform to reproduce its number; a different window gives a different total and a dif…
- `reason` (string|null): Why these rewrites fit the target archetype, or null.
- `remix_engine` (string|null): Which remixer produced the variants.
- `remixes` (array|null): The variants: {id, text, archetype, score, rank}. Billed per variant RETURNED, so this can be shorter than count.
- `replayed` (boolean|null): true when an idempotency_key replayed a stored result, so nothing was charged again. The credits_charged below is what the ORIGINAL call cost.
- `replayed_at_charge` (boolean|null): true when the replay was detected at the charge boundary rather than up front; either way you are billed exactly once.
- `request_id` (string|null): Id of this call. Keep it: get_usage(request_id=...) itemises exactly what it charged, and it identifies the call in a support question.
- `score_disclaimer` (string|null): The honest limits of the score attached above.
- `topic_core` (string|null): The subject the remixer preserved from the original.

### `list_hooks` (~589 tokens)

List the hooks this account has bought, newest first. Free (WP-G).

        Persisted hooks stay retrievable for 90 days. Args: mode (instant|smart|research|
        remix, or any generate alias such as template/search), since (ISO timestamp),
        tag (exact fleet-tag match), topic (substring), unreported (true: only hooks a
        first outcome report can still reward), limit (1-100), offset, api_key. Returns
        {hooks:[{hook_id, text, archetype, mode, score_total, created_at, request_id}],
        limit, offset, total}. Errors: unauthorized, invalid_request, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `limit` (integer): Max hooks to return, 1-200. Above the ceiling is an invalid_request, never a silent truncation.
- `mode`: Filter to hooks bought from this engine. Canonical stored values: instant|smart|research|remix; the generate aliases (template|off|quick, llm|on|fast, search|deep|deep_research) are accepted and reso…
- `offset` (integer): Number of rows to skip for paging, 0-9223372036854775807. Page with offset += the limit you actually requested; `total` in the response is the unpaged count. The ceiling is SQLite's largest bindable…
- `since`: Return only rows created at or after this UTC timestamp. Compared LEXICALLY against stored 'YYYY-MM-DDTHH:MM:SSZ' values, so pass that exact format (a date-only string or a unix epoch silently select…
- `tag`: Filter to hooks carrying EXACTLY this fleet tag: a lowercase slug of 1-40 chars of [a-z0-9_-], matched LITERALLY. `_` is a legal tag character and is NOT a wildcard here, and neither is `%`; there is…
- `topic`: Case-insensitive SUBSTRING match against the topic a hook was generated for (unlike `tag`, which is exact). Omit for every topic.
- `unreported`: true = only hooks with no outcome yet, i.e. the ones a first report_outcome can still earn a reward on. false = only hooks already reported. Omit for both.

Output parameters:

- `hooks` (array|null): This page: {hook_id, text, archetype, mode, score_total, created_at, request_id}. hook_id is what report_outcome and get_hook take.
- `limit` (number|null): Page size actually applied.
- `offset` (number|null): Offset this page started at.
- `total` (number|null): Rows matching the filters IGNORING paging: the number to page through, not the number returned here.

### `get_hook` (~246 tokens)

Fetch one bought hook in full, including its parsed score. Free (WP-G).

        Account-scoped: a foreign or unknown id is not_found (no existence leak). Args:
        hook_id (from a generate/batch/remix response or list_hooks), api_key. Returns
        {hook_id, text, archetype, claim_type, mode, platform, topic, score_total, score,
        prompt_version, request_id, created_at, outcomes:[...], outcome_summary:{count,
        max_views, avg_views}}. Report results with report_outcome. Errors: unauthorized,
        not_found, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `hook_id` (string, required): The hook's id, as returned in the `hooks[].hook_id` of a generate/batch/remix response or by list_hooks. Account-scoped: an id you do not own reads as not_found.

Output parameters:

- `archetype` (string|null): Archetype it was written in.
- `claim_type` (string|null): What kind of claim it makes.
- `contains_placeholder_stat` (boolean|null): true when the text carries an unverified number you must replace before posting (e.g. '90% of people'). Treat as an edit-before-use flag.
- `created_at` (string|null): When it was generated, ISO-8601 UTC.
- `hook_id` (string|null): The hook's id.
- `mode` (string|null): Engine that produced it (instant|smart|research|remix).
- `outcome_summary` (object|null): {count, max_views, avg_views} over those outcomes.
- `outcomes` (array|null): Outcomes you have reported against this hook.
- `platform` (string|null): Platform it was written for.
- `prompt_version` (string|null): Prompt build that produced it, for reproducibility.
- `request_id` (string|null): The generate call that bought it.
- `score` (object|null): Per-dimension score breakdown.
- `score_total` (number|null): Total craft score.
- `text` (string|null): The hook line.
- `topic` (string|null): Topic it was generated from.

### `list_outcomes` (~430 tokens)

List the posted outcomes THIS account has reported, newest first. Free (WP-LIFE).

        Retrieve your own submitted telemetry in bulk (per-hook it rides get_hook). Args:
        platform (tiktok|instagram|youtube|x|other), since (ISO timestamp), hook_id (one
        hook), limit (1-200, default 50), offset (>=0), api_key. Each row carries the outcome
        plus its snapshot (hook_text, topic, mode, archetype, claim_type, score_total,
        score_source, prompt_version, tags). Returns {outcomes:[...], limit, offset, total}.
        Errors: unauthorized, invalid_request, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `hook_id`: Filter to the outcomes reported against ONE hook (its id from list_hooks / a generate response). Omit for every hook.
- `limit` (integer): Max outcomes to return, 1-200. Above the ceiling is an invalid_request, never a silent truncation.
- `offset` (integer): Number of rows to skip for paging, 0-9223372036854775807. Page with offset += the limit you actually requested; `total` in the response is the unpaged count. The ceiling is SQLite's largest bindable…
- `platform`: Filter to outcomes reported for one platform. Omit for all.
- `since`: Return only rows created at or after this UTC timestamp. Compared LEXICALLY against stored 'YYYY-MM-DDTHH:MM:SSZ' values, so pass that exact format (a date-only string or a unix epoch silently select…

Output parameters:

- `limit` (number|null): Page size actually applied.
- `offset` (number|null): Offset this page started at.
- `outcomes` (array|null): Each row is the reported outcome plus a snapshot of the hook at report time (hook_text, topic, mode, archetype, claim_type, score_total, score_source, prompt_version, tags).
- `total` (number|null): Rows matching the filters ignoring paging.

### `report_outcome` (~516 tokens)

Report what a bought hook actually did once posted. FREE (WP-H).

        Honest telemetry feeding the outcome corpus (no view prediction claimed). Args:
        hook_id, platform (tiktok|instagram|youtube|x|other), posted_at (ISO, not >48h
        future), views/likes (0..1e11), retention_pct? (0-100), url? (http(s)+host),
        api_key, idempotency_key. Caps 20/hook, 500/day; an exact duplicate is a conflict.
        Returns {outcome, aggregate, reward_credits, credits_remaining?}. Errors:
        unauthorized, not_found, invalid_request, conflict, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `hook_id` (string, required): Which bought hook this result belongs to: the hook_id from a generate/batch/remix response or list_hooks. Must be a hook you own.
- `idempotency_key`: Caller-chosen replay key (any string, unique per intended effect). A repeat call with the SAME key returns the stored result and is NEVER charged twice; the same key with different arguments is an id…
- `likes`: Likes at report time, >=0. Omit if you cannot read it.
- `platform` (string, required): Where the hook was actually posted. Required.
- `posted_at` (string, required): When it went live, ISO-8601 UTC ('YYYY-MM-DDTHH:MM:SSZ'). May be in the past; more than 48h in the FUTURE is invalid_request.
- `retention_pct`: Average view-through as a PERCENT, 0-100 (not a 0-1 fraction). Omit if the platform does not expose it.
- `url`: Public http(s) URL of the post, for your own audit trail. Must carry a host; omit entirely rather than passing a placeholder.
- `views` (integer, required): Views the post had accrued at report time, >=0. Required: it is the only field the corpus calibrates against. Report again later (up to 20 reports per hook) to record how it matured.

Output parameters:

- `aggregate` (object|null): Rolled-up totals for this hook across every report you have made.
- `credits_remaining` (number|null): Your balance after the reward. Present only when one was granted.
- `outcome` (object|null): The stored row, as recorded.
- `replayed` (boolean|null): true when an idempotency_key replayed a stored report.
- `reward_credits` (number|null): Credits granted for this report. Only a hook's FIRST report earns one, so later reports return 0.

### `start_generate_job` (~667 tokens)

Submit a generate as an async job; poll get_job for the result.

        Same args as generate_hooks (tags persist); charge runs on the worker; results are
        always full-verbosity; prefer for research. EITHER topic OR topics (1-20 subjects,
        ONE job + one atomic charge, result = the batch envelope); both or neither is
        invalid_request. A job only runs where a job worker runs: when none does the
        response carries worker_alive:false + a warning, so call generate_hooks instead.
        Errors: unauthorized, invalid_request, insufficient_credits, idempotency_conflict.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `archetypes`: Restrict generation to these archetype ids (see list_archetypes). Omit to let the engine spread across archetypes, which is what you want unless you are deliberately narrowing a deck.
- `count` (integer): Hooks per topic, 1-25. With `topics` this applies to every subject, so the job's cost scales with count * len(topics).
- `idempotency_key`: Caller-chosen replay key (any string, unique per intended effect). A repeat call with the SAME key returns the stored result and is NEVER charged twice; the same key with different arguments is an id…
- `mode`: Generation engine and therefore the price: instant (0 base + 1/hook, deterministic templates, sub-second), smart (0 + 2/hook, one LLM call, seconds), research (10 + 4/hook, brief->draft->judge, tens…
- `platform` (string): Target platform, which selects the length/format conventions the hooks are written and scored against. Defaults to tiktok when omitted.
- `style`: Voice/tone to match, <=200 chars. Honored as a real instruction by smart and research; on instant it only varies which deterministic template fillers are drawn, so it cannot change the voice there. O…
- `tags`: 1-5 lowercase slug tags ([a-z0-9_-], <=40 chars) stamped on this call's usage event so a fleet can attribute spend per campaign. Omit for no tagging. Filter later with get_usage(tag=...) / list_hooks…
- `topic`: Single subject for the job, 3-200 chars. Pass EITHER this or `topics`, never both and never neither.
- `topics`: 1-20 subjects run as ONE job with ONE atomic charge; the result is the batch envelope instead of a single generate body. Pass EITHER this or `topic`.

Output parameters:

- `estimated_seconds` (number|null): Queue-aware estimate of total time to a result.
- `expires_at` (string|null): When an unexecuted job is abandoned, ISO-8601 UTC.
- `job_id` (string|null): Pass this to get_job.
- `poll_after_seconds` (number|null): Wait at least this long before the first get_job. Honor it.
- `replayed` (boolean|null): true when an idempotency_key returned an EXISTING job rather than queuing a new one. Poll the returned job_id either way.
- `requeued` (boolean|null): true when an idempotent resubmit revived an existing job.
- `status` (string|null): Lifecycle state; 'queued' immediately after submit.
- `status_url` (string|null): REST URL for the same status (HTTP clients only).
- `warning` (string|null): Present only alongside worker_alive:false; says what to do instead.
- `worker_alive` (boolean|null): Present and FALSE only when no job worker will ever run this job. Then read `warning` and use generate_hooks instead of polling.

### `get_job` (~231 tokens)

Poll an async job: status, real engine stage, progress, ETA, result/error.

        Poll after poll_after_seconds until status is "succeeded" (result holds the full
        generate body) or "failed" (error holds the typed envelope). Only the owning
        account can read a job; a foreign/unknown id is not_found (no existence leak).
        Returns {job_id, status, stage, progress_pct, eta_seconds, elapsed_ms,
        poll_after_seconds, result, error}. Errors: unauthorized, not_found, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `job_id` (string, required): The job's id, as returned by start_generate_job (or by a generate_hooks call that auto-jobbed on deadline_ms). Account-scoped: an id you do not own reads as not_found.

Output parameters:

- `elapsed_ms` (number|null): Milliseconds since the job started running.
- `error` (object|null): On failure: the same typed error envelope a synchronous call returns. details.cancelled true means YOU cancelled it with cancel_job, not a fault.
- `eta_seconds` (number|null): Estimated seconds remaining.
- `job_id` (string|null): The job polled.
- `poll_after_seconds` (number|null): How long to wait before polling again.
- `progress_pct` (number|null): 0-100 progress within the run.
- `result` (object|null): On success: the FULL generate (or batch) body, always full-verbosity.
- `stage` (string|null): The real engine stage while running (e.g. brief, draft, judge).
- `status` (string|null): queued (waiting on a worker), running, succeeded (see `result`), failed (see `error`). Stop polling on the last two.

### `wait_for_job` (~312 tokens)

Block until a job is terminal, then return it. Free, bounded, no poll loop.

        Returns the EXACT get_job body plus timed_out, waited_ms and polls, so branch on
        `status` exactly as you would with get_job. timed_out:true is NOT a failure, it
        means the budget ran out: call again with the SAME job_id. Waiting neither cancels
        nor charges; the worker charges when it runs the job either way. It returns
        IMMEDIATELY with worker_alive:false + `warning` when no worker exists here.
        Errors: unauthorized, not_found (unknown or foreign job_id), rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `job_id` (string, required): The job to wait on, as returned by start_generate_job (or by a generate_hooks call that auto-jobbed on deadline_ms). Account-scoped: an id you do not own reads as not_found.
- `timeout_seconds` (integer): How long to block, 1-300 seconds. Keep it BELOW your own MCP client's request timeout, or the client gives up before this tool answers. Running out is not an error: you get timed_out:true plus the jo…

Output parameters:

- `elapsed_ms` (number|null): Milliseconds since the job started running.
- `error` (object|null): On failure: the same typed error envelope a synchronous call returns. details.cancelled true means YOU cancelled it with cancel_job, not a fault.
- `eta_seconds` (number|null): Estimated seconds still remaining.
- `job_id` (string|null): The job waited on.
- `poll_after_seconds` (number|null): How long to wait before polling again; 0 once finished.
- `polls` (number|null): How many get_job reads this call made on your behalf.
- `progress_pct` (number|null): 0-100 progress within the run.
- `result` (object|null): On success: the FULL generate (or batch) body, always full-verbosity.
- `stage` (string|null): The real engine stage reached (e.g. brief, draft, judge).
- `status` (string|null): queued, running, succeeded (see `result`) or failed (see `error`). With timed_out:false this is always succeeded or failed unless worker_alive is false.
- `timed_out` (boolean|null): true when timeout_seconds elapsed before the job finished. NOT a failure: call wait_for_job again with the SAME job_id, or poll get_job.
- `waited_ms` (number|null): How long this call actually blocked.
- `warning` (string|null): Present only alongside worker_alive:false; says what to do instead.
- `worker_alive` (boolean|null): Present and FALSE only when the wait returned immediately because no job worker will ever run this job. Then read `warning`.

### `cancel_job` (~242 tokens)

Cancel a job that is STILL QUEUED. Free, repeatable, never refunds.

        ALWAYS branch on `cancelled`; reason + next_action say what to do. A queued job
        was never charged (the worker charges), so credits_refunded is always 0. A RUNNING
        job cannot be stopped: it finishes, CHARGES and persists. Cancelled reads as
        status "failed" with error.error.details.cancelled true; to re-run, resubmit with
        a FRESH idempotency_key (reusing the cancelled one replays the cancelled result).
        Errors: unauthorized, forbidden (key lacks the spend scope), not_found, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `job_id` (string, required): The job to cancel. Account-scoped: an id you do not own reads as not_found, exactly like get_job.

Output parameters:

- `already_cancelled` (boolean|null): true when an earlier cancel_job had already cancelled this job, so this call was a no-op rather than a miss.
- `cancelled` (boolean|null): true only when this call moved a still-QUEUED job to terminal. false means nothing changed; `reason` says why and `next_action` says what to do.
- `credits_refunded` (number|null): Always 0. A queued job was never charged (the charge runs inside the worker), and a running job cannot be stopped, so a cancel never refunds.
- `error`: Not returned by this tool. Named here because get_job(job_id) afterwards carries error.error.details.cancelled true (code 'conflict', details.reason 'cancelled'): THAT is how you tell your own cancel…
- `job_id` (string|null): The job the cancel targeted.
- `next_action` (string|null): The exact next call to make, if any.
- `reason` (string|null): Why the call ended the way it did, in one sentence.
- `status` (string|null): The job's status AFTER the attempt: 'failed' when cancelled, else whatever it really is (running / succeeded / failed).

### `list_jobs` (~254 tokens)

List this account's jobs, newest first (summaries without the result blob).

        Args: limit (1-200, default 50), offset (>=0), api_key. Returns {jobs:[{job_id,
        status, stage, progress_pct, created_at, started_at, finished_at}], limit,
        offset, total}. Errors: unauthorized, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `limit` (integer): Max jobs to return, 1-200. Above the ceiling is an invalid_request, never a silent truncation.
- `offset` (integer): Number of rows to skip for paging, 0-9223372036854775807. Page with offset += the limit you actually requested; `total` in the response is the unpaged count. The ceiling is SQLite's largest bindable…

Output parameters:

- `jobs` (array|null): Summaries: {job_id, status, stage, progress_pct, created_at, started_at, finished_at}. Fetch a result with get_job.
- `limit` (number|null): Page size actually applied.
- `offset` (number|null): Offset this page started at.
- `total` (number|null): Jobs matching ignoring paging.

### `set_webhook` (~264 tokens)

Register (or replace) this account's webhook; the secret is returned ONCE. Admin scope.

        VHGENGINE POSTs signed job.succeeded/job.failed/credits.low (WP-M). `url` https, no
        creds, public host; re-registering ROTATES the secret (a repeat call is not a no-op).
        Verify via X-VHG-Signature: sha256=HMAC_SHA256(secret, raw_body); a webhook.test pings.
        Args: url (1-2048), api_key (admin). Cost=free. Errors: unauthorized, forbidden,
        invalid_request, rate_limited. Returns {url, secret, created_at, events, note,
        test_delivery_id}.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `url` (string, required): https:// endpoint that will receive signed event POSTs. Must be a public host with no embedded credentials. Re-registering ANY url rotates the signing secret, so this is not safe to blind-retry.

Output parameters:

- `created_at` (string|null): Registration time, ISO-8601 UTC.
- `events` (array|null): Event types that will be delivered.
- `note` (string|null): Human-readable summary, including the rotation warning.
- `secret` (string|null): HMAC signing secret, shown exactly once. Verify deliveries with X-VHG-Signature: sha256=HMAC_SHA256(secret, raw_body).
- `test_delivery_id` (string|null): A webhook.test ping enqueued immediately; look it up with list_webhook_deliveries to prove your receiver works.
- `test_note` (string|null): What that test delivery is and how to check it.
- `url` (string|null): The endpoint now registered.

### `get_webhook` (~133 tokens)

This account's webhook (url + last delivery status; never the secret). Free read.

        Args: api_key (read scope). Cost=free. Returns {url, created_at, last_delivery_status,
        last_delivery_at, events}. Errors: unauthorized, not_found (none registered),
        rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…

Output parameters:

- `created_at` (string|null): Registration time, ISO-8601 UTC.
- `events` (array|null): Event types being delivered.
- `last_delivery_at` (string|null): When that attempt happened, ISO-8601 UTC.
- `last_delivery_status` (string|null): Status of the most recent delivery attempt.
- `url` (string|null): The registered endpoint.

### `delete_webhook` (~117 tokens)

Remove this account's webhook (WP-M). Admin scope.

        Args: api_key (admin scope). Cost=free. Returns {deleted: true}. Errors: unauthorized,
        forbidden, not_found (none registered), rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…

Output parameters:

- `deleted` (boolean|null): true once removed. No further events are delivered.

### `list_webhook_deliveries` (~383 tokens)

List this account's webhook deliveries, newest first. Free read (WP-HOOKS).

        Verify your receiver end-to-end (a webhook.test ping is enqueued at registration) and
        diagnose failures without waiting out a real event. Args: status (pending|retrying|
        delivered|dead|retired), limit (1-200, default 50), offset (>=0), api_key. Each row has
        {delivery_id, event_type, status, attempts, last_status_code, error, timestamps,
        next_attempt_at, payload_preview (200 chars; the full body is never returned)}. Dead
        rows are kept 7 days. Errors: unauthorized, invalid_request, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `limit` (integer): Max deliveries to return, 1-200. Above the ceiling is an invalid_request, never a silent truncation.
- `offset` (integer): Number of rows to skip for paging, 0-9223372036854775807. Page with offset += the limit you actually requested; `total` in the response is the unpaged count. The ceiling is SQLite's largest bindable…
- `status`: Filter to deliveries in this state: pending (queued, not yet attempted), retrying (failed, backing off), delivered (2xx), dead (retries exhausted; redrive_webhook_delivery can requeue it), retired. O…

Output parameters:

- `deliveries` (array|null): Each {delivery_id, event_type, status, attempts, last_status_code, error, timestamps, next_attempt_at, payload_preview (200 chars; the full body is never returned)}. `dead` rows are kept 7 days and c…
- `limit` (number|null): Page size actually applied.
- `offset` (number|null): Offset this page started at.
- `total` (number|null): Deliveries matching the filter ignoring paging.

### `redrive_webhook_delivery` (~240 tokens)

Requeue a dead-lettered webhook delivery: reset to pending, due now. Admin scope.

        Valid ONLY on a `dead` delivery (a live receiver that exhausted its retries); any other
        status is a 409 conflict and a delivery you do not own is not_found. It re-attempts
        through the normal pipeline and, if it dies again, dead-letters normally. Args:
        delivery_id (from list_webhook_deliveries), api_key. Returns the refreshed row. Errors:
        unauthorized, forbidden, not_found, conflict, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `delivery_id` (string, required): The delivery to requeue, from list_webhook_deliveries. Only a `dead` row can be redriven; any other status is a conflict and a delivery you do not own is not_found.

Output parameters:

- `attempts` (number|null): Attempts so far; the redrive adds to this count.
- `created_at` (string|null): When the delivery was first enqueued, ISO-8601 UTC.
- `dead_at` (string|null): When it was dead-lettered, or null once redriven.
- `delivered_at` (string|null): When it finally succeeded, or null.
- `delivery_id` (string|null): The delivery that was requeued.
- `error` (string|null): Last failure reason, or null.
- `event_type` (string|null): The event this delivery carries.
- `last_status_code` (number|null): HTTP status of the most recent attempt, or null.
- `next_attempt_at` (string|null): When the pipeline will try again, ISO-8601 UTC. Null on a terminal row.
- `payload_preview` (string|null): First 200 chars of the body; the full body is never returned.
- `status` (string|null): Reset to 'pending' and due now.

### `get_activity` (~215 tokens)

See what this account's agents are doing: in-flight ops + recent ops. Free.

        `in_flight` merges the live-ops registry (real stage/pct/eta mid-run) with your
        queued/running jobs; `recent` is the last completed charged ops. Every row has a
        human-readable message. The REST surface also offers an SSE feed at
        GET /v1/activity/stream. Args: recent_limit (1-100, default 20), api_key.
        Errors: unauthorized, rate_limited.

Input parameters:

- `api_key`: API key for this call. Omit to fall back to the Authorization: Bearer / X-API-Key request header (streamable-HTTP only), then the VHGENGINE_API_KEY env var (the stdio default). No key resolvable -> u…
- `recent_limit` (integer): Max completed rows in `recent`, 1-100. Does not limit `in_flight`, which always shows everything currently running.

Output parameters:

- `in_flight` (array|null): Live operations: the running-ops registry (real stage/pct/eta) merged with your queued and running jobs. Every row has a human-readable message.
- `recent` (array|null): The last completed charged operations, sized by recent_limit.

## Diagnostics

Captured diagnostic sections: TLS, DNSSEC, Authorisation, Transports. The full working is on the page: https://verifymcp.io/servers/app-railway-up-engine-production-3bdc-vhgengine/engine-production-3bdc#diagnostics

## Score history

- 2026-08-03: 65
- 2026-08-02: 59
- 2026-08-01: 59
- 2026-07-31: 58
- 2026-07-30: 54
- 2026-07-29: 54
- 2026-07-28: 53
- 2026-07-27: 52
- 2026-07-26: 52

## Links

- Remote endpoint: https://engine-production-3bdc.up.railway.app/mcp
- Website: https://engine-production-3bdc.up.railway.app/llms.txt
- Changelog RSS feed: https://verifymcp.io/servers/app-railway-up-engine-production-3bdc-vhgengine/engine-production-3bdc/changelog.xml
- Changelog JSON feed: https://verifymcp.io/servers/app-railway-up-engine-production-3bdc-vhgengine/engine-production-3bdc/changelog.json
- HTML version of this page: https://verifymcp.io/servers/app-railway-up-engine-production-3bdc-vhgengine/engine-production-3bdc
