world-model · research API

Give it a company website → researchers fetch & verify public evidence → a publish-safe brief of grounded facts (currently-valid, source-cited, confidence-scored). Base URL https://api.customer1.bucktee.org

Auth

Every endpoint except /health needs a bearer token:

Authorization: Bearer <WM_API_KEY>

The flow

Research is async — POST a company → get a job_id → poll until done → read the brief. The group (graph key) is derived from the website and returned in the response — read the company back with it. The first scrape of a company requires a company name; re-POSTing the same website later just refreshes it (dedup skips unchanged evidence). List everything with /companies.

Endpoints

GET/health
Liveness, no auth.
curl https://api.customer1.bucktee.org/health
# {"ok": true}
POST/research
Kick off research for a company. Returns immediately with a job id. The graph key (group) is derived from the website — the same site always maps to the same graph.
websiterequired — domain or URL, e.g. plausible.io. Determines the graph.
companyrequired for a new company (first scrape); optional when refreshing an existing one.
depthoptional — shallow | standard (default) | deep. Deeper = more research rounds, more queries, and the dedicated deep-research model. The job loops (gather → read the graph → research the gaps) until it stops finding new entities.
curl -H "Authorization: Bearer $KEY" -X POST https://api.customer1.bucktee.org/research \
  -d '{"website":"plausible.io","company":"Plausible"}'
# 202 {"job_id":"a1b2c3","status":"queued","group":"plausible-io","new_company":true,"poll":"/research/a1b2c3"}
#
# a NEW company with no name is rejected:
# 400 {"error":"company name is required for a new company (first scrape)","group":"plausible-io"}
GET/research/<job_id>
Job status. status goes queued → running → done (or error). While running, agents is the list of researchers spawned and their live status (planner / site collector / web researcher / extractor / grounding, each running|done with a detail), activity is a one-line "what it's doing right now", and events is the raw step log. Poll any of them for live progress. When done, the brief is in result.
curl -H "Authorization: Bearer $KEY" https://api.customer1.bucktee.org/research/a1b2c3
# running: {"status":"running","activity":"Web researcher · round 1 — searching the web",
#           "agents":[{"id":"plan-0","label":"Planner · round 0","status":"done","detail":"12 queries"},
#                     {"id":"web-0","label":"Web researcher · round 0","status":"done","detail":"14 pages"},
#                     {"id":"extract-0","label":"Extractor · round 0","status":"running","detail":"building the graph"}]}
# done:    {"status":"done","group":"plausible-io","stats":{...},"result":{...brief...}}
GET/company/<group>
The publish-safe brief any time after research. Facts are currently-valid, grounded, non-contested.
curl -H "Authorization: Bearer $KEY" https://api.customer1.bucktee.org/company/plausible-io
GET/company/<group>/dossier
The marketing dossier — narrative profile (every sentence [n]-cited into the fact list), brand voice learned from the company's own copy, competitors, audience/ICP, and a strategy scaffold. Built automatically at the end of research; served from cache. Add ?build=1 to force a rebuild (takes ~a minute). fact_index maps every cite [n] to its fact, source URL, quote, and uuid (the feedback handle).
curl -H "Authorization: Bearer $KEY" https://api.customer1.bucktee.org/company/plausible-io/dossier
POST/company/<group>/ads
Generate voice-locked, claims-checked ad/content drafts from the dossier. Formats: search_rsa, meta_ad, x_post, linkedin_post, landing_hero, email_subjects. Every variant carries an audit: each factual claim mapped to a fact uuid (source URL + quote) or flagged unsupported, plus platform length checks. Drafts only — nothing publishes.
curl -H "Authorization: Bearer $KEY" -X POST https://api.customer1.bucktee.org/company/plausible-io/ads \
  -d '{"formats":["x_post","landing_hero"],"variants":3}'
# {"formats":{"x_post":{"variants":[{"text":"...","audit":{"claims":[...],"unsupported":0,...}}]}}}
GET/company/<group>/assets
The latest generated asset batch (variants + audits + asset_ids + UTM params).
GET/company/<group>/roi?window=14
Trailing-window performance: ROAS/CAC/CTR by channel, campaign, and asset (joined via utm_content=asset_id), winners & losers, and the proven angles feeding the next generation.
POST/company/<group>/roi/rows
Push normalized metric rows ({"rows":[{"date","channel","campaign","asset_id","spend", "impressions","clicks","conversions","revenue","source"}]}) — the ingest seam for kimchi or custom scripts.
POST/company/<group>/roi/sync
Pull fresh rows from every configured ad source (GA4, Google Ads, Meta, Search Console, Shopify, Klaviyo). Body: {"since":"YYYY-MM-DD","write_back":true} — write_back also lands campaign/channel performance facts in the graph (structured, no LLM).
POST/company/<group>/feedback
Human-in-the-loop correction of a fact (take uuid from the brief or the dossier's fact_index). Authoritative: it outranks collected evidence and persists across re-research.
fact_uuidrequired — the fact's uuid
actionrequiredconfirm (pin, confidence 1.0) | reject (invalidate, kept as history) | correct (invalidate + write the replacement)
correctionrequired for correct — the corrected statement
authoroptional — who corrected it
curl -H "Authorization: Bearer $KEY" -X POST https://api.customer1.bucktee.org/company/plausible-io/feedback \
  -d '{"fact_uuid":"...","action":"correct","correction":"Plausible has 12 employees."}'
# {"action":"correct","fact":"...old claim...","replacement_uuid":"...","dossier_invalidated":true}
GET/search?group=<g>&q=<query>
Live semantic search over a company's grounded facts.
curl -H "Authorization: Bearer $KEY" "https://api.customer1.bucktee.org/search?group=plausible-io&q=who+are+the+customers"
DELETE/company/<group>
Wipe one company's graph (every node + edge) and drop its tenant entry — to re-research from a clean slate. Other companies are untouched.
curl -X DELETE -H "Authorization: Bearer $KEY" https://api.customer1.bucktee.org/company/plausible-io
# {"deleted":"plausible-io","nodes_deleted":214,"tenant_removed":true}
GET/companies
Every registered company (tenant): graph key, name, website, and created / last-research times.
curl -H "Authorization: Bearer $KEY" https://api.customer1.bucktee.org/companies
# {"companies":[{"group_id":"plausible-io","company":"Plausible","website":"plausible.io",
#                "created_at":"2026-06-26T15:00:00+00:00","last_research":"2026-06-26T15:02:00+00:00"}]}
GET/jobs
All in-flight + recent research jobs with their live agents and status — so a reloaded UI (or any monitor) can re-discover running work without holding the job id.
curl -H "Authorization: Bearer $KEY" https://api.customer1.bucktee.org/jobs
# {"jobs":[{"job_id":"a1b2c3","company":"Plausible","status":"running",
#           "activity":"Web researcher · round 1 — searching the web","agents":[...]}]}

Brief shape (/company/<group> and job result)

{
  "group_id": "plausible-io",
  "as_of": "2026-06-26",
  "entities": [{"name":"Plausible","type":"Organization","summary":"..."}],
  "facts": [{
    "fact": "Uku is a co-founder of Plausible.",
    "relation": "CO_FOUNDER", "subject": "Uku", "object": "Plausible",
    "valid_from": "2000-01-01",
    "grounded": true, "confidence": 1.0, "confidence_band": "high",
    "source_url": "https://plausible.io/about",
    "quote": "Uku is a co-founder of Plausible."
  }],
  "dimensions": { "people": [/* facts */], "products": [/* facts */] },
  "stats": {"entities": 17, "facts_published": 8,
            "excluded": {"stale": 0, "contested": 2, "unsupported": 0}}
}

Each fact carries its source URL, a verbatim supporting quote, a confidence band, and a validity window — so you can drop facts (or a dimension) straight into a generation prompt and trust it's current and cited. excluded = what the publish filter held back.

JavaScript

const BASE = "https://api.customer1.bucktee.org";
const KEY  = "";
const h = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" };

// 1) start research
const { job_id } = await fetch(`${BASE}/research`, {
  method: "POST", headers: h,
  body: JSON.stringify({ website: "plausible.io", company: "Plausible" })  // company required on first scrape
}).then(r => r.json());                                                    // -> { job_id, group: "plausible-io", ... }

// 2) poll until done (job.activity is a live one-liner you can show meanwhile)
let job;
do {
  await new Promise(r => setTimeout(r, 5000));
  job = await fetch(`${BASE}/research/${job_id}`, { headers: h }).then(r => r.json());
  console.log(job.status, job.activity);   // e.g. "running" "round 2 · extracted — 14 entities, 31 facts"
} while (job.status === "queued" || job.status === "running");

// 3) use the brief
const brief = job.result;                 // or GET ${BASE}/company/plausible-io later
console.log(brief.facts);