{
  "name": "researcher-now",
  "kind": "agent_handoff_contract",
  "version": "v1",
  "contractVersion": "2026-06-26",
  "stayingCurrent": {
    "rule": "Every API response carries an X-Researcher-Contract header (run-creating responses also a contractVersion field). When it differs from contractVersion here, re-fetch https://researcher.now/agent.txt and https://researcher.now/agent.json before continuing. Response fields are authoritative over your own saved notes.",
    "noticeChannel": "Time-boxed change notices ride responses as X-Researcher-Notice headers and contractNotices body fields for ~2 weeks after a change.",
    "pluginFloor": "The X-Researcher-Min-Plugin header advertises the oldest supported researcher-now plugin; upgrade with pip install -U researcher-now when a response includes pluginUpdateNote."
  },
  "whatsNew": [
    {
      "date": "2026-06-26",
      "change": "Keyless access now works over MCP. Connect to https://researcher.now/mcp with no API key — the unauthenticated handshake (initialize/tools/list) is allowed, and paid tools (deep_research, analyze_article, analyze_video, ask_persona) answer 402 with an MPP session challenge. Authorize it from your wallet (send the credential on Authorization with the Payment scheme, or X-Payment-Authorization) and the call provisions a persistent account and returns a durable key in the X-Researcher-Agent-Key header — the same wallet-direct flow as the HTTP API. Reads still require a key; use the one returned on first paid call, or pass a customer rk_ key as the bearer token."
    },
    {
      "date": "2026-06-25",
      "change": "Keyless persona consults + billing clarity. POST /v1/entities/:id/chat now works with no API key: authorize the returned MPP session challenge (intent=\"session\") from your wallet to pay a flat per-question price (quick vs deep tiers); the verified payer becomes a persistent account and a durable key is returned in the X-Researcher-Agent-Key header. Keyless consults must be synchronous (omit async); funded customers are still metered against balance. Billing clarity: keyless runs AND consults are fixed-price — you authorize and are captured the quoted budget/price for that request (control a run's price with limits.maxCostUsd); metered settle-to-actual with refund applies only to funded server-wallet accounts."
    },
    {
      "date": "2026-06-24",
      "change": "Keyless agent access — no human needed to provision a key. POST /v1/runs (or /v1/analyze) with no Authorization and fund it with a client-authorized MPP/402 session: when no payment is attached the response is 402 with a tempo session challenge (WWW-Authenticate, intent=\"session\"); authorize it from your wallet and retry with the session credential in the Authorization or Payment-Authorization header. The session's verified on-chain channel payer becomes a persistent account, and a paid run's response returns a durable rk_ API key in agentApiKey plus the X-Researcher-Agent-Key header — store it and send it as a Bearer token for reads, management, and future runs. Keyless runs are fixed-price: you authorize and are captured the run's quoted budget for that request (set limits.maxCostUsd to control it) — there is no metered refund, which applies only to funded server-wallet accounts. No deposit step and no Privy login. The /account/ browser signup still works unchanged for humans."
    },
    {
      "date": "2026-06-23",
      "change": "Persona consults are session-aware and scale on a worker pool. ask_persona accepts an optional sessionId — carry it across turns and the persona remembers the conversation — and a depth dial (\"quick\" default, or \"deep\" for a wider reranked retrieval on hard questions); it returns sessionId. A new tool/endpoint lists your resumable sessions per persona (MCP: list_persona_sessions; HTTP: GET /v1/entities/:slug/chat/sessions). POST /v1/entities/:slug/chat now accepts depth and async; async returns a turnId you poll at GET /v1/entities/:slug/chat/turns/:turnId/events, and the terminal 'done' event carries the final answer. Heavy/deep turns run on the bounded worker queue so many concurrent agent consults stay responsive."
    },
    {
      "date": "2026-06-22",
      "change": "Visual data on topic feed items and Daily briefs: scan_digest items and each brief's new sourceItems[] carry optional fields so clients can render tweets-as-tweets and og thumbnails, each present only when the provider supplied it — text (full post text), authorName, authorAvatarUrl, media (an array of { type: photo|video|animated_gif, url, previewUrl? }), image (thumbnail/og image for non-tweet items), and domain. All existing item fields are unchanged. sourceItems is the day's notable scanned items, deduped by url, ranked by engagement/recency, capped at 12."
    },
    {
      "date": "2026-06-21",
      "change": "Topic Daily briefs: topics now generate a once-per-day Daily brief, a thesis-relative narrative of the day's notable scanned items (skipped on quiet days). GET /v1/topics/:idOrSlug returns dailyBriefs, newest first (up to 30), each { id, briefDate, windowStart, windowEnd, contentMarkdown, summary, itemCount, thesisDeltas, createdAt } — an immutable dated row covering [windowStart, windowEnd]; thesisDeltas is an array of { claimId, direction, note } where direction is reinforced | contradicted | raised and claimId references a thesis claim id (like C1) or is null. The response also carries briefsSeenAt (when you last marked the briefs seen, or null). POST /v1/topics/:id/briefs/seen marks them seen and returns { briefsSeenAt }. A new daily_brief_generated topic event carries the brief's one-line summary."
    },
    {
      "date": "2026-06-20",
      "change": "Persona and topic tools on MCP. list_personas and ask_persona consult durable expert personas (Paul Graham, Warren Buffett, Elon Musk, Patrick Collison, Jeff Bezos, Stanley Druckenmiller, and more) — each a research-grounded corpus of that person's own writing, talks, and posts — and ask_persona answers in their voice with citations. list_topics and topic_brief read your standing research topics and their living briefs. Personas are public; topics are scoped to your account."
    },
    {
      "date": "2026-06-19",
      "change": "Topic slugs and renaming: every topic object now includes a slug (a URL-friendly form of its title, unique within your account). GET /v1/topics/:idOrSlug accepts either the slug or the uuid. PATCH /v1/topics/:id with { \"title\": \"...\" } (1-200 chars after trimming) renames a topic and regenerates its slug; the response is { topic } with the new slug, and a title_updated event is appended."
    },
    {
      "date": "2026-06-18",
      "change": "Topic status cleanup: topic.status now represents operational watch state — active, paused, or archived. Topics are active from creation because scans start immediately; bootstrap run start, failure, and completion are recorded as topic events instead of changing topic.status."
    },
    {
      "date": "2026-06-18",
      "change": "Realtime topic cadence: POST /v1/topics cadence accepts \"realtime\" — the topic is scanned roughly every 10 minutes. hourly/daily/weekly are unchanged and daily stays the default. The feed starts as soon as watch targets exist instead of waiting for the bootstrap run to finish."
    },
    {
      "date": "2026-06-17",
      "change": "GitHub seeds and bigger X lists: POST /v1/topics seeds gain githubProfiles — GitHub usernames watched as standing github watch targets (releases, new repos, pushes, opened pull requests). The seeds.xAccounts cap is raised from 50 to 1000; large lists are scanned in batches across the topic's cadence. GET /v1/topics/:id watchTargets entries can now carry kind \"github\"."
    },
    {
      "date": "2026-06-16",
      "change": "Topic docs: topics now maintain two versioned markdown documents, returned on GET /v1/topics/:id as docs.thesis and docs.working (each { version, contentMarkdown, summary, createdAt }, or null before seeding). thesis holds the stable top-level beliefs seeded from the bootstrap research run — numbered claims with confidence and falsification conditions — and is never auto-edited; synthesis only flags tensions against it. working is the living idea layer: new scan items are continuously synthesized into it under pressure triggers (enough new items, staleness, first items after seeding), not on a fixed clock; each reorganization appends a new version. New topic event stages: docs_seeded and doc_synthesized (the doc_synthesized message is a what-changed summary; its metadata.tensions flags thesis claims under pressure, referencing claim ids), plus docs_seed_failed and doc_synthesis_failed on errors."
    },
    {
      "date": "2026-06-15",
      "change": "Standing research topics: POST /v1/topics creates a topic from a prompt (the subject plus the user's stake) with optional seeds — X accounts, RSS feeds, domains — that become standing watch targets, plus an automatic wide-search lane. Creating a topic fires a bootstrap deep-research run automatically; after it completes the topic goes active and scans on its cadence (hourly/daily/weekly, default daily), appending scan_digest events. GET /v1/topics lists topics; GET /v1/topics/:id returns the topic plus recent events and watch targets."
    },
    {
      "date": "2026-06-14",
      "change": "Agent inbox: every terminal run on the account lands as a pending export — including runs started on the web app, API, or Discord. Drain GET /v1/exports/pending at session start, surface watch URLs, acknowledge with POST /v1/exports/ack. Exposed as the research_inbox tool on MCP and the researcher-now plugin. Accounts with a provisioned Hermes agent runtime URL additionally receive the full run-export payload pushed on completion with no registration step."
    },
    {
      "date": "2026-06-13",
      "change": "Podcast episode URLs on POST /v1/analyze always answer fast with a deferred run (deferred=true, reason=podcast); transcript and analysis land on the run at watchUrl. Concurrent duplicate analyze calls coalesce onto one execution — after a gateway 502, check GET /v1/runs before retrying; the run very likely exists and is processing."
    },
    {
      "date": "2026-06-12",
      "change": "Contract versioning introduced: X-Researcher-Contract/X-Researcher-Notice/X-Researcher-Min-Plugin headers on every response, contractVersion + contractNotices on run-creating bodies."
    },
    {
      "date": "2026-06-11",
      "change": "/v1/analyze and runs are usage-settled: a budget session per analyze call settles to actual usage at close-out. costUsd in the response is what has been charged so far (0 for budget sessions); quote estimatedCostUsd and read the settled cost from the run. Flat-priced /v1/source-extractions, /v1/website-extractions, /v1/youtube-extractions removed (410)."
    }
  ],
  "baseUrl": "https://researcher.now",
  "description": "Article and video analysis, not scraping: any URL returns a cleanly formatted article — any YouTube video, X/Twitter video post, or Spotify / Apple Podcasts episode a full transcript with speaker-aware quotes — plus finished structured analysis (claims, facts, quantitative data, stakeholder positions, quotes), so you don't spend your own context window parsing raw pages. Deep multi-source research via one explicit deep_research call returns a synthesized, cited Research Report.",
  "agent_behavior": {
    "share_with_user": {
      "required": true,
      "instruction": "Always surface the viewerUrl from analyze and the watchUrl/viewerUrl from deep research back to the end user in your reply. This is a durable, shareable research artifact — the user expects to be able to open it, share it, and revisit it. Do not swallow these links. Relay these links exactly as returned by the API — never construct researcher.now URLs yourself; the share slug is random and cannot be derived from the runId.",
      "analyze": {
        "required_fields": [
          "viewerUrl"
        ],
        "reply_requirement": "Always include viewerUrl in the end-user reply after a successful analyze_article or analyze_video call."
      },
      "deep_research": {
        "required_fields": [
          "watchUrl",
          "viewerUrl"
        ],
        "reply_requirement": "Always include watchUrl and viewerUrl in the end-user reply after a successful deep_research or POST /v1/runs create call."
      },
      "research_status": {
        "required_fields": [
          "watchUrl",
          "viewerUrl"
        ],
        "reply_requirement": "Always include any returned watchUrl or viewerUrl in the end-user reply after a research_status call."
      }
    }
  },
  "artifacts": {
    "trade_ideas_v1": {
      "description": "Evidence-linked explicit positions and inferred market ideas when the completed run corpus supports them.",
      "advice": false,
      "execution": false,
      "notes": [
        "Explicit items cite source evidence for a stated market position.",
        "Inferred items are thesis-derived hypotheses and should be treated as lower-certainty research context.",
        "The artifact is produced by the named trade_ideas stage and includes validation counts for candidates retained or dropped.",
        "The read-only resolver may attach local equity/ETF references plus Polymarket, Hyperliquid, Robinhood Crypto, and 0x/Matcha-style token identity matches when confidence is high enough.",
        "Venue and price fields should be used only when present and resolver-verified; performance fields are not populated unless a separate performance resolver has run."
      ]
    }
  },
  "whenToChoose": {
    "researcher-now": [
      "Input is a bare YouTube URL (you want transcript + analysis + quotes, not metadata).",
      "You need structured claims, facts, quantitative data, stakeholder positions, contradictions.",
      "You're doing multi-source synthesis across web + video + RSS.",
      "You need durable Research Reports with retrieval-indexed chat and compounding research memory."
    ],
    "firecrawl": "You only need raw markdown of a known URL.",
    "tavily": "You only need a fast general web search.",
    "exa": "You need neural/semantic search over the open web."
  },
  "install": {
    "hermes": "pip install researcher-now && hermes plugins enable researcher-now (or add researcher-now to plugins.enabled in ~/.hermes/config.yaml if the CLI says not installed)",
    "hermesNote": "The hermes plugins enable step is REQUIRED — pip plugins are opt-in in Hermes.",
    "skill": "npx skills add researcher-now/skill",
    "codexMcp": "codex mcp add researcher-now --url https://researcher.now/mcp --bearer-token-env-var RESEARCHER_TOKEN"
  },
  "mcp": {
    "endpoint": "https://researcher.now/mcp",
    "serverName": "researcher-now",
    "transport": "streamable_http",
    "protocolVersion": "2025-06-18",
    "auth": {
      "header": "Authorization: Bearer $RESEARCHER_TOKEN",
      "customerKeysOnly": true,
      "adminTokensAccepted": false
    },
    "tools": [
      {
        "name": "analyze_article",
        "wraps": "POST /v1/analyze",
        "readOnly": false,
        "description": "Article Analysis: any article URL returns reader-grade formatted markdown plus finished structured analysis (claims, facts, quantitative data, stakeholder positions, quotes with attribution) and a durable runId/viewerUrl. Not a DOM dump; the analysis is included so it doesn't burn the calling agent's context window.",
        "share_with_user": {
          "required": true,
          "fields": [
            "viewerUrl"
          ],
          "instruction": "Always surface the viewerUrl back to the end user in your reply. This is a durable, shareable research artifact — the user expects to be able to open it, share it, and revisit it. Do not swallow this link. Relay this link exactly as returned by the API — never construct researcher.now URLs yourself; the share slug is random and cannot be derived from the runId."
        },
        "arguments": {
          "required": [
            "url"
          ]
        }
      },
      {
        "name": "analyze_video",
        "wraps": "POST /v1/analyze",
        "readOnly": false,
        "description": "Video & Podcast Analysis: a YouTube video, X/Twitter video post, or Spotify / Apple Podcasts episode URL returns the full transcript with speaker-aware quotes plus the same structured analysis and a durable runId/viewerUrl. Use for YouTube transcript, summarize this video, summarize this podcast episode, what does this video say.",
        "share_with_user": {
          "required": true,
          "fields": [
            "viewerUrl"
          ],
          "instruction": "Always surface the viewerUrl back to the end user in your reply. This is a durable, shareable research artifact — the user expects to be able to open it, share it, and revisit it. Do not swallow this link. Relay this link exactly as returned by the API — never construct researcher.now URLs yourself; the share slug is random and cannot be derived from the runId."
        },
        "arguments": {
          "required": [
            "url"
          ]
        }
      },
      {
        "name": "deep_research",
        "wraps": "POST /v1/runs",
        "readOnly": false,
        "description": "Returns a cited, synthesized Research Report. Async: returns runId + watchUrl/viewerUrl immediately; poll research_status until succeeded.",
        "share_with_user": {
          "required": true,
          "fields": [
            "watchUrl",
            "viewerUrl"
          ],
          "instruction": "Always surface the watchUrl and viewerUrl back to the end user in your reply. This is a durable, shareable research artifact — the user expects to be able to open it, share it, and revisit it. Do not swallow these links. Relay these links exactly as returned by the API — never construct researcher.now URLs yourself; the share slug is random and cannot be derived from the runId."
        },
        "sourceTypes": [
          "topic"
        ],
        "arguments": {
          "required": [
            "topic"
          ],
          "optional": [
            "idempotencyKey",
            "title",
            "mode",
            "depth",
            "instructions",
            "sourcePolicy",
            "limits.maxCostUsd",
            "outputSchemas"
          ],
          "rejected": [
            "source",
            "sources",
            "requestedBy",
            "preflightPlan",
            "message",
            "conversationalKickoff",
            "skipSynthesis"
          ]
        },
        "idempotencyArgument": "idempotencyKey"
      },
      {
        "name": "research_status",
        "wraps": "GET /v1/runs/:id",
        "readOnly": true,
        "description": "Progress and results for a Research Report by runId.",
        "share_with_user": {
          "required": true,
          "fields": [
            "watchUrl",
            "viewerUrl"
          ],
          "instruction": "Always surface any returned watchUrl or viewerUrl back to the end user in your reply. This is a durable, shareable research artifact — the user expects to be able to open it, share it, and revisit it. Do not swallow these links. Relay these links exactly as returned by the API — never construct researcher.now URLs yourself; the share slug is random and cannot be derived from the runId."
        }
      },
      {
        "name": "research_inbox",
        "wraps": "GET /v1/exports/pending + POST /v1/exports/ack",
        "readOnly": false,
        "description": "Completed runs this account's agents have not yet seen, including runs started on the web app, API, or Discord. Check at session start and before new research. Returned runs are acknowledged automatically; pass acknowledge:false to peek.",
        "share_with_user": {
          "required": true,
          "fields": [
            "urls.watch"
          ],
          "instruction": "Surface each returned run's watch URL back to the end user. Relay links exactly as returned by the API — never construct researcher.now URLs yourself."
        }
      },
      {
        "name": "recall_research",
        "wraps": "GET /v1/library/recall",
        "readOnly": true,
        "description": "Research memory: instantly recall anything the org has already researched, with citations. Check before paying for new work."
      },
      {
        "name": "list_personas",
        "wraps": "GET /public/entities",
        "readOnly": true,
        "description": "List the durable expert personas you can consult, with slug, name, what they are known for, and corpus size. Free.",
        "arguments": {}
      },
      {
        "name": "ask_persona",
        "wraps": "POST /v1/entities/:slug/chat",
        "readOnly": true,
        "description": "Ask a persona (by slug from list_personas) a question; returns an answer in their voice grounded in their actual writing and talks, with citations, plus a sessionId. Pass an optional sessionId to continue a prior conversation (the persona remembers the thread) and an optional depth ('quick' default, or 'deep' for a wider reranked retrieval on hard questions). Costs a small per-question fee.",
        "arguments": { "persona": "required persona slug, e.g. warren-buffett", "question": "required question string", "sessionId": "optional session id from a prior ask_persona/list_persona_sessions to continue the conversation", "depth": "optional 'quick' (default) or 'deep'" }
      },
      {
        "name": "list_persona_sessions",
        "wraps": "GET /v1/entities/:slug/chat/sessions",
        "readOnly": true,
        "description": "List your resumable chat sessions for one persona (by slug), so you can pick a sessionId to continue a conversation with ask_persona. Free.",
        "arguments": { "persona": "required persona slug, e.g. warren-buffett" }
      },
      {
        "name": "list_topics",
        "wraps": "GET /v1/topics",
        "readOnly": true,
        "description": "List your standing research topics — durable watches that track a subject and keep a living brief current. Returns id, title, status, cadence, last updated. Free.",
        "arguments": {}
      },
      {
        "name": "topic_brief",
        "wraps": "GET /v1/topics/:id",
        "readOnly": true,
        "description": "Read the latest synthesized brief for one of your standing topics (by id from list_topics). Free.",
        "arguments": { "topic": "required topic id" }
      }
    ],
    "deferred": [
      "Run Analyst chat until customer billing attribution is fixed end to end.",
      "URL, feed, and video deep-research ingestion until the MCP URL-safety gate validates redirects, public IP resolution, timeouts, and private-network denial; analyze_article/analyze_video cover single-URL work.",
      "Source add/prune/redo, run delete/stop, steering, webhooks, account key creation, deposits, collections, entity/topic management (create, edit, rebuild), Discord, admin/operator routes, and arbitrary REST passthrough. Persona consultation and topic reads ARE exposed (list_personas, ask_persona, list_persona_sessions, list_topics, topic_brief)."
    ],
    "clientSetup": {
      "codex": "codex mcp add researcher-now --url https://researcher.now/mcp --bearer-token-env-var RESEARCHER_TOKEN"
    }
  },
  "purpose": "Analyze articles and videos in seconds — with a full synthesized report following on the same run — and create durable cited Research Reports with reusable sources, structured evidence, live URLs, usage audit data, and run-scoped chat.",
  "installableSkill": {
    "command": "npx skills add researcher-now/skill",
    "source": "https://github.com/researcher-now/skill",
    "note": "The skill is public and contains only the agent contract; researcher-now credentials are customer rk_ keys from the account setup URL."
  },
  "analyze": {
    "endpoint": "POST /v1/analyze",
    "synchronous": true,
    "request": {
      "url": "https://example.com/article-or-youtube-url"
    },
    "response": {
      "runId": "durable run id",
      "watchUrl": "public run viewer URL",
      "viewerUrl": "public run viewer URL",
      "streamUrl": "public run event stream URL",
      "kind": "article | video",
      "title": "string",
      "markdown": "reader-grade formatted article markdown (kind=article)",
      "transcript": "full transcript with speaker-aware quotes (kind=video)",
      "analysis": "structured analysis: claims, facts, quantitativeFacts, stakeholderPositions, quotes with attribution",
      "status": "running (full report being written to viewerUrl) | awaiting_funding (free preview stored; fund from the run page or POST /v1/runs/:id/start to finish the report)"
    },
    "share_with_user": {
      "required": true,
      "fields": [
        "viewerUrl"
      ],
      "instruction": "Always surface the viewerUrl back to the end user in your reply. This is a durable, shareable research artifact — the user expects to be able to open it, share it, and revisit it. Do not swallow this link. Relay this link exactly as returned by the API — never construct researcher.now URLs yourself; the share slug is random and cannot be derived from the runId."
    },
    "notes": [
      "Every successful analyze call stores a durable single-source run that appears in /v1/runs, library search/recall, run chat, and iterate surfaces.",
      "Responds in seconds; the run then continues to a full synthesized report on the same viewerUrl.",
      "Billed like any run: a budget session settled to actual usage at close-out (unused budget returned). The response's costUsd is only what was charged at response time (0 for budget sessions) — never report it as the run's price; quote estimatedCostUsd instead, and read the settled cost from the run after completion. Unfunded keys still get the analysis as a free preview that parks as awaiting_funding — relay the viewerUrl so the user can fund and finish the report.",
      "The analysis is included — it does not burn the calling agent's context window.",
      "YouTube videos, X/Twitter video posts, and Spotify / Apple Podcasts episodes are detected by host and routed to media analysis (transcript-first); X posts without extractable video fall back to article analysis."
    ]
  },
  "auth": {
    "header": "Authorization: Bearer $RESEARCHER_TOKEN",
    "keySetupUrl": "https://researcher.now/account/?setup=agent",
    "introspection": "GET /v1/me",
    "rules": [
      "Use customer rk_ keys for customer workflows.",
      "Do not use admin tokens or payment bypasses.",
      "Send Idempotency-Key on POST /v1/runs and paid POST /v1/runs/:id/iterate calls."
    ]
  },
  "createRun": {
    "endpoint": "POST /v1/runs",
    "share_with_user": {
      "required": true,
      "fields": [
        "watchUrl",
        "viewerUrl"
      ],
      "instruction": "Always surface the watchUrl and viewerUrl back to the end user in your reply. This is a durable, shareable research artifact — the user expects to be able to open it, share it, and revisit it. Do not swallow these links. Relay these links exactly as returned by the API — never construct researcher.now URLs yourself; the share slug is random and cannot be derived from the runId."
    },
    "idempotency": {
      "header": "Idempotency-Key",
      "scope": "account+route",
      "ttlDays": 7
    },
    "sourceTypes": {
      "topic": {
        "required": [
          "type",
          "topic"
        ]
      },
      "url": {
        "required": [
          "type",
          "url"
        ],
        "optional": [
          "scope",
          "title"
        ]
      },
      "feed": {
        "required": [
          "type",
          "url"
        ],
        "optional": [
          "limit",
          "since",
          "title"
        ]
      },
      "video": {
        "required": [
          "type",
          "url"
        ],
        "optional": [
          "transcriptMode",
          "title"
        ]
      }
    },
    "optionalFields": [
      "requestedBy",
      "title",
      "mode",
      "depth",
      "instructions",
      "sourcePolicy",
      "limits",
      "outputSchemas",
      "preflightPlan",
      "conversationalKickoff",
      "message",
      "sourcesOnly",
      "skipSynthesis",
      "attachments"
    ],
    "sourceOnlySemantics": [
      "sourcesOnly:true uses provided sources without broad web search.",
      "skipSynthesis:true skips the full final-report writer; source-only runs may still publish a lightweight source-summary brief from the processed corpus."
    ],
    "videoTranscriptBehavior": [
      "Video runs are transcript-first.",
      "If native/generated captions are unavailable, empty, clearly partial, or cannot prove duration coverage, researcher-now escalates to paid Deepgram media transcription, including muxed video/audio fallback, before failing the required transcript lane.",
      "If media ASR succeeds but detects no spoken words, researcher-now stores an explicit no-spoken-content transcript source, skips substitute-source expansion, completes the run with a short no-transcript report, and exposes a video_transcript_no_speech warning/status instead of inventing dialogue or failing silently."
    ],
    "urlPromptRouting": {
      "conceptExpansion": "Composer-style prompts that are a single non-YouTube URL, or one URL plus fewer than 8 steering words, silently create a concept_expansion research run. researcher-now treats the source content as the prompt, analyzes its concepts, claims, references, methods, and named entities, searches adjacent work, and writes a source-centered concept-map report.",
      "exceptions": [
        "URL plus summarize, TLDR, digest, recap, or similar routes to single-source analysis.",
        "URL plus a clear question routes to a normal research run with the URL as source context.",
        "YouTube URLs keep the transcript-first video analysis flow."
      ]
    },
    "funding": {
      "status": "awaiting_funding",
      "rules": [
        "POST /v1/runs without funding returns runId, status:'queued', preview:true, and fundingUrl; the run executes a free preview pass and then parks as awaiting_funding.",
        "Relay fundingUrl to the customer; the report page shows real partial results with a funding CTA.",
        "On funding, POST /v1/runs/:id/start finishes the full report — preview sources are reused, no lost work."
      ]
    },
    "budget": {
      "preferred": "limits.maxCostUsd",
      "acceptedAliases": [
        "limits.max_cost_usd",
        "maxCostUsd",
        "max_cost_usd",
        "budgetUsd",
        "budget_usd"
      ],
      "rules": [
        "limits.maxCostUsd is the canonical budget ceiling.",
        "If depth is omitted, Researcher derives the planning tier from the budget ceiling.",
        "If depth is provided, the explicit depth wins.",
        "Researcher may stop early when quality gates are satisfied.",
        "Create responses include priceUsd as the planner estimate and budgetUsd/budgetPolicy as the authorized budget ceiling.",
        "Do not describe maxSources, query counts, loop counts, or other internal safety controls as customer-facing caps."
      ]
    },
    "examples": [
      {
        "requestedBy": "customer-agent",
        "source": {
          "type": "topic",
          "topic": "recent advances in battery recycling"
        },
        "limits": {
          "maxResearchLoops": 3,
          "maxCostUsd": 25
        }
      },
      {
        "requestedBy": "customer-agent",
        "source": {
          "type": "url",
          "url": "https://example.com/",
          "scope": "domain"
        },
        "limits": {
          "maxCostUsd": 25
        }
      },
      {
        "requestedBy": "customer-agent",
        "source": {
          "type": "feed",
          "url": "https://example.com/feed.xml",
          "limit": 50
        },
        "limits": {
          "maxCostUsd": 10
        }
      },
      {
        "requestedBy": "customer-agent",
        "source": {
          "type": "video",
          "url": "https://www.youtube.com/watch?v=...",
          "transcriptMode": "native"
        }
      }
    ]
  },
  "readRun": {
    "endpoints": [
      "GET /v1/runs",
      "GET /v1/runs/:id",
      "GET /v1/runs/:id/job",
      "GET /v1/runs/:id/stream",
      "GET /v1/runs/:id/results",
      "GET /v1/runs/:id/markdown",
      "GET /v1/runs/:id/sources",
      "GET /v1/runs/:id/extractions",
      "GET /v1/runs/:id/transcript",
      "GET /v1/runs/:id/usage",
      "GET /v1/runs/:id/diff",
      "GET /v1/runs/:id/tags"
    ],
    "warnings": {
      "report_contract_residual_issues": "The final edit still has writing, structure, citation, or output-shape defects.",
      "report_evidence_gaps": "Some requested requirements need more evidence than the current corpus contains.",
      "report_unavailable_evidence": "Some requested data appears paid, private, blocked, registry-only, or otherwise unavailable to an automated public run; the report should state those limitations instead of inventing values.",
      "video_transcript_no_speech": "The requested video transcript path completed, but media ASR detected no spoken words. Treat the run as completed with no transcript text, quotes, or claims."
    },
    "terminalStatuses": [
      "succeeded",
      "failed",
      "cancelled"
    ],
    "rules": [
      "For unattended or multi-run agents, register an account webhook before starting work.",
      "If no webhook receiver is available, stream or poll until a terminal status; the agent inbox (GET /v1/exports/pending) catches anything that finished while you were not watching.",
      "Remove terminal runs from queued or in-flight lists immediately.",
      "For failed or cancelled runs, surface status, error, watchUrl, and usage instead of waiting for a report."
    ]
  },
  "webhooks": {
    "endpoints": [
      "GET /v1/webhooks",
      "POST /v1/webhooks",
      "DELETE /v1/webhooks/:id"
    ],
    "create": {
      "endpoint": "POST /v1/webhooks",
      "body": {
        "event": "run.complete",
        "url": "https://example.com/researcher",
        "secret": "at-least-8-chars"
      }
    },
    "events": [
      "run.complete",
      "run.succeeded",
      "run.failed",
      "run.cancelled"
    ],
    "rules": [
      "Use run.complete for terminal-run notification across succeeded, failed, and cancelled runs.",
      "The x-researcher-event header and payload event name the actual terminal event such as run.succeeded or run.failed.",
      "When secret is set, x-researcher-signature is sha256= plus the HMAC-SHA256 of the raw JSON body.",
      "Webhooks are account-scoped; per-run webhookUrl fields on POST /v1/runs are not supported."
    ]
  },
  "agentInbox": {
    "endpoints": [
      "GET /v1/exports/pending",
      "POST /v1/exports/ack"
    ],
    "default": true,
    "rules": [
      "Every terminal run on the account (succeeded, failed, cancelled) lands in the inbox as a pending export, regardless of which surface started it — web app, API, MCP, plugin, or Discord. No registration is required.",
      "GET /v1/exports/pending lists unseen terminal runs (limit default 20, max 100). Entries carry id, runId, event, title, topic, and urls (watch, markdown, api).",
      "POST /v1/exports/ack with {\"ids\":[\"...\"]} marks entries as seen. Acknowledging is per-account: any agent on the account drains the same inbox.",
      "Check the inbox at session start and before commissioning new research; surface each run's watch URL to the user.",
      "The inbox holds one entry per run with its latest terminal event; a run that fails and later succeeds reappears as pending.",
      "Accounts with a provisioned Hermes agent runtime URL also receive the full researcher_run_export_v1 payload pushed to that URL on completion, signed like a webhook delivery when a secret is configured, with no registration step."
    ]
  },
  "topics": {
    "endpoints": [
      "POST /v1/topics",
      "GET /v1/topics",
      "GET /v1/topics/:idOrSlug",
      "PATCH /v1/topics/:id",
      "POST /v1/topics/:id/briefs/seen"
    ],
    "create": {
      "endpoint": "POST /v1/topics",
      "body": {
        "prompt": "required — the subject plus the user's stake in it (what they care about, what decisions it feeds)",
        "title": "optional display title (derived from the prompt when omitted)",
        "seeds": {
          "xAccounts": ["optional X/Twitter account handles to watch — up to 1000; large lists are scanned in batches"],
          "githubProfiles": ["optional GitHub usernames to watch via the public events API, up to 100 — releases, new repos, pushes, opened pull requests"],
          "feeds": ["optional RSS/Atom feed URLs to watch"],
          "domains": ["optional website domains to watch"]
        },
        "cadence": "realtime | hourly | daily | weekly (default daily; realtime scans roughly every 10 minutes)",
        "monthlyBudgetUsd": "optional monthly spend ceiling for the topic"
      },
      "auth": "Customer rk_ key, same as POST /v1/runs and POST /v1/analyze.",
      "behavior": [
        "Creating a topic fires a bootstrap deep-research run automatically — no separate POST /v1/runs call.",
        "Seeds become standing watch targets (X accounts, GitHub profiles, RSS feeds, websites) plus an automatic wide-search lane that looks beyond the seeds. GitHub watch targets appear on GET /v1/topics/:id with kind \"github\".",
        "Returns 201 with an active topic. Bootstrap run start, failure, and completion are recorded as topic events instead of changing topic.status."
      ]
    },
    "read": {
      "list": "GET /v1/topics lists your topics. Every topic object carries a slug (a URL-friendly form of its title, unique within your account).",
      "detail": "GET /v1/topics/:idOrSlug returns the topic plus recent events (including scan_digest items), its watch targets, its docs (docs.thesis and docs.working), its dailyBriefs, and briefsSeenAt. The path segment accepts either the topic's slug or its uuid."
    },
    "dailyBriefs": {
      "shape": "GET /v1/topics/:id returns dailyBriefs, newest first (up to 30). Each is { id, briefDate, windowStart, windowEnd, contentMarkdown, summary, itemCount, thesisDeltas, sourceItems, createdAt }: an immutable, dated, once-per-day narrative of what happened in [windowStart, windowEnd] that matters to the topic, thesis-relative when a thesis doc exists. thesisDeltas is an array of { claimId, direction, note } where direction is reinforced | contradicted | raised and claimId references a thesis claim id (like C1) or is null. sourceItems is the day's notable scanned items (deduped by url, ranked by engagement/recency, capped at 12), each carrying the same optional visual fields as scan_digest items. Quiet days produce no brief.",
      "visualFields": "scan_digest items and brief sourceItems carry optional visual fields so clients can render tweets-as-tweets and og thumbnails, each present only when the provider supplied it: text (full post text), authorName, authorAvatarUrl, media (an array of { type: photo|video|animated_gif, url, previewUrl? }), image (thumbnail/og image for non-tweet items), and domain. All existing item fields are unchanged.",
      "seen": "The detail response also carries briefsSeenAt (when you last marked the briefs seen, or null). POST /v1/topics/:id/briefs/seen marks the topic's briefs seen (now) and returns { briefsSeenAt }; 403 without a customer key, 404 when the topic is not yours.",
      "events": "A daily_brief_generated topic event is appended each time a brief is written; its message is the brief's one-line summary and its metadata carries briefId, itemCount, and thesisDeltaCount. daily_brief_failed surfaces a generation error (no retry until the next day)."
    },
    "rename": {
      "endpoint": "PATCH /v1/topics/:id",
      "body": { "title": "required — 1-200 chars after trimming" },
      "behavior": "Renames the topic and regenerates its slug. Returns { topic } with the new title and slug; a title_updated event is appended."
    },
    "docs": {
      "shape": "GET /v1/topics/:id returns docs.thesis and docs.working — two versioned markdown documents. Each is { version, contentMarkdown, summary, createdAt }, or null before seeding.",
      "thesis": "Stable top-level beliefs seeded from the bootstrap research run: numbered claims with confidence and falsification conditions. Never auto-edited — synthesis only flags tensions against it.",
      "working": "The living idea layer: new scan items are continuously synthesized into it under pressure triggers (enough new items, staleness, first items after seeding), not on a fixed clock. Each reorganization appends a new version.",
      "events": "docs_seeded marks the initial seeding of both docs from the bootstrap report; doc_synthesized marks each working-doc reorganization — its message is the what-changed summary and its metadata.tensions flags thesis claims under pressure, referencing claim ids (C1, C2, ...). Errors surface as docs_seed_failed and doc_synthesis_failed; docs_seed_skipped means seeding was skipped (for example, no bootstrap report markdown was found)."
    },
    "lifecycle": {
      "statuses": [
        "active",
        "paused",
        "archived"
      ],
      "rules": [
        "topic.status is the operational watch state.",
        "A new topic is active immediately because scans start as soon as watch targets exist.",
        "The topic scans on its cadence (appending scan_digest events with what changed) until paused or archived.",
        "Bootstrap run start, failure, and completion are recorded as topic events; they do not change the active watch status."
      ]
    }
  },
  "iterateRun": {
    "endpoint": "POST /v1/runs/:id/iterate",
    "actions": [
      "start",
      "pause",
      "continue",
      "deepen",
      "focus",
      "steer",
      "report",
      "fork",
      "evaluate",
      "cancel",
      "stop"
    ],
    "idempotency": {
      "header": "Idempotency-Key",
      "scope": "account+route+run",
      "ttlDays": 7
    },
    "sourceControls": [
      "POST /v1/runs/:id/sources",
      "PATCH /v1/runs/:id/sources/:sourceId",
      "DELETE /v1/runs/:id/sources",
      "DELETE /v1/runs/:id/sources/:sourceId",
      "POST /v1/runs/:id/sources/:sourceId/redo"
    ],
    "organization": [
      "GET /v1/runs/:id/tags",
      "POST /v1/runs/:id/tags",
      "DELETE /v1/runs/:id/tags",
      "DELETE /v1/runs/:id/tags/:tag",
      "POST /v1/runs/:id/export",
      "DELETE /v1/runs/:id"
    ]
  },
  "chat": {
    "runScoped": [
      "POST /v1/runs/:id/chat",
      "GET /v1/runs/:id/chat/sessions",
      "POST /v1/runs/:id/chat/sessions",
      "GET /v1/runs/:id/chat/sessions/:sessionId",
      "PATCH /v1/runs/:id/chat/sessions/:sessionId",
      "DELETE /v1/runs/:id/chat/sessions/:sessionId",
      "POST /v1/runs/:id/chat/sessions/:sessionId/messages",
      "GET /v1/runs/:id/chat/turns/:turnId/stream",
      "DELETE /v1/runs/:id/chat/turns/:turnId",
      "GET /v1/runs/:id/chat/index",
      "POST /v1/runs/:id/chat/index/rebuild"
    ],
    "global": [
      "GET /v1/account/chats"
    ]
  },
  "account": [
    "GET /v1/account/balance",
    "GET /v1/account/deposit-addresses",
    "POST /v1/account/deposit-addresses",
    "GET /v1/account/keys",
    "POST /v1/account/keys",
    "PATCH /v1/account/keys/:id",
    "DELETE /v1/account/keys/:id",
    "GET /v1/account/integrations/:platform",
    "POST /v1/account/integrations/:platform",
    "DELETE /v1/account/integrations/:platform/:id",
    "GET /v1/webhooks",
    "POST /v1/webhooks",
    "DELETE /v1/webhooks/:id",
    "GET /v1/exports/pending",
    "POST /v1/exports/ack"
  ],
  "library": [
    "GET /v1/library",
    "GET /v1/library/search",
    "GET /v1/library/evidence",
    "GET /v1/library/claims",
    "GET /v1/library/contradictions",
    "GET /v1/library/contradictions/:id",
    "PATCH /v1/library/contradictions/:id",
    "POST /v1/library/contradictions/:id/verify"
  ],
  "entities": {
    "publicPages": [
      "GET /e/:slug"
    ],
    "publicJson": [
      "GET /public/entities",
      "GET /public/entities/by-slug/:slug",
      "POST /public/entities/by-slug/:slug/retrieve"
    ],
    "authenticatedManagement": [
      "GET /v1/entities",
      "POST /v1/entities",
      "GET /v1/entities/:id",
      "PATCH /v1/entities/:id",
      "GET /v1/entities/:id/sources",
      "POST /v1/entities/:id/sources/from-run",
      "GET /v1/entities/:id/runs",
      "POST /v1/entities/:id/runs",
      "GET /v1/entities/:id/records",
      "GET /v1/entities/:id/profile",
      "POST /v1/entities/:id/profile/rebuild",
      "GET /v1/entities/:id/report",
      "POST /v1/entities/:id/report/rebuild",
      "POST /v1/entities/:id/index/rebuild",
      "POST /v1/entities/:id/retrieve",
      "POST /v1/entities/:id/chat",
      "GET /v1/entities/:id/chat/sessions",
      "POST /v1/entities/:id/chat/sessions",
      "GET /v1/entities/:id/chat/sessions/:sessionId",
      "PATCH /v1/entities/:id/chat/sessions/:sessionId",
      "DELETE /v1/entities/:id/chat/sessions/:sessionId",
      "POST /v1/entities/:id/chat/sessions/:sessionId/messages",
      "GET /v1/entities/:id/chat/turns/:turnId/events"
    ],
    "xPersonaIngestion": {
      "availability": "operator_internal_not_customer_api",
      "customerKeys": "Not available to customer rk_ keys."
    },
    "durabilityRules": [
      "Entity sources are durable snapshots owned by entity tables.",
      "Entity chat sessions, messages, turns, and events are durable entity-owned rows.",
      "Entity profiles are versioned source-cited artifacts; active profile ids are activation pointers.",
      "Entity About reports are separate versioned source-cited markdown artifacts; active report ids are activation pointers.",
      "Public entity About reports use the canonical sections Recurring interests and themes, Beliefs and principles, Future narratives and predictions, Recurring questions, Tensions and contradictions, and Unifying threads.",
      "Original run source ids and pack ids are provenance only.",
      "Linked entity runs snapshot sources and rebuild entity indexes, profiles, and About reports after run completion by default.",
      "Public entity JSON only returns publicly shareable linked runs.",
      "Public entity JSON may include subtitle for compact entity headers.",
      "Public entity JSON may include avatarUrl for a safe public image path or HTTPS image.",
      "Public entity JSON with sources included returns sourceOrigins, accepted source counts grouped into web, youtube, x, and other.",
      "Entity record counts are derived evidence records, not source counts.",
      "Entity chat uses the active profile for voice, worldview, and boundaries plus private style-fuel packets for cadence and diction.",
      "Public entity pages use the active About report for the About tab when one is ready.",
      "Factual entity-chat citations come only from retrieved answer packets for the turn; style packets are trace metadata, not citations."
    ]
  },
  "collections": [
    "GET /v1/collections",
    "POST /v1/collections",
    "POST /v1/collections/:id/runs",
    "DELETE /v1/collections/:id/runs/:runId"
  ]
}
