Tibeb

API · version 2026-11-04

Tibeb's API

The same research records the site shows, for your own code and AI tools: a REST API under /v1 and an MCP server at /mcp. Research only — no orders and no buy or sell calls. The routes read your records; the one thing that changes them is an agent run you start, which does what the same buttons on the site would. An agent run and the IC memo are the only things here that make a model call.

Authentication

Send the key as a bearer token on every request:

Authorization: Bearer tbk_…

Keys start with tbk_. Getting a key: a signed-in account makes and revokes its own keys in the terminal under Tools → API keys, each with a label — at most three live keys an account. A paid account adds its card first (a key can't be made while billing would refuse the account's calls), and a key stops working while its account is paused, can no longer sign in, or is out of credit. Each key's calls today and this month are shown beside it. A key is shown once, when it is made — copy it then — and the server keeps only its SHA-256 hash and its first characters, so a lost key can't be shown again: revoke it and make another. A signed-in browser session is not a key, and a key is not a sign-in to the site.

curl https://www.tibeb.trade/v1/theses \
  -H "Authorization: Bearer $TIBEB_KEY"

No account yet? Apply on the front page; a firm that wants a pilot writes to hello@tibeb.trade.

Rate limits

At most 120 calls a minute a key. Past that the API answers 429 with Retry-After: 60; the refused call is written to the audit log too. Agent runs share a queue on the server (two steps at a time across everyone), and every model call a run makes is booked to the key's login and paid from its account like the same work on the site.

Errors

REST errors are JSON with a detail in plain words:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer

{"detail": "An API key is required: Authorization: Bearer tbk_…"}
StatusWhen
400A body that can't be used: no thesis, a start_at that isn't a number, a webhook address that can't take webhooks
401No key, or not a key Tibeb made (or one that was revoked)
402The key's account can't spend (its plan's allowance used and no credit left), its plan allows fewer live keys than it holds (the oldest keep working: revoke down to the plan's count), or agent runs don't come with its plan (Pro+, Founding member and Team have them); the words say which
403/v1/plans or /v1/journal with a key that isn't the owner's; the key's account is paused or can no longer sign in
404No thesis, run or webhook of this key's login with that id; an unknown agent; not a ticker symbol
409The thesis isn't in the engine yet (run full-review first); the run isn't waiting, or has ended; too many webhooks
429Past 120 calls a minute (Retry-After: 60)
503The research store or the SEC reader isn't open on the server, or the SEC couldn't be read; or the server is starting and hasn't read this month's usage yet (try again in a moment)

MCP errors follow JSON-RPC 2.0: see MCP.

Theses

GET/v1/theses

The key's login's theses (up to 200): the question, the resolve-by date, the status, how many of the eight checks pass, and the committee's number when there is one.

{
  "theses": [
    {
      "id": "w1",
      "name": "AAOI",
      "question": "Will AAOI report $300M revenue for a quarter by 2027-03-31?",
      "resolve_by": "2027-05-31",
      "status": "open",
      "updated": "2026-11-08T14:02:11Z",
      "gate": {"passed": 3, "total": 8, "open": false, "whose": "engine"},
      "verdict": null
    }
  ],
  "version": "2026-11-04"
}

gate.whose is engine once the thesis is in the research engine, else web (the provisional checks). verdict, when present, is {"p_yes": 62.0, "lean": "yes"} — the committee's pooled probability, a research estimate, not a forecast of any price.

GET/v1/theses/{id}

One thesis in full: both scenarios, each of the eight checks, every source with the side it argues and the page's own words, the dated events — and the exposed companies only once all eight checks pass.

{
  "id": "w1",
  "question": "Will AAOI report $300M revenue for a quarter by 2027-03-31?",
  "resolve_by": "2027-05-31",
  "resolve_by_text": "a 10-Q, by 2027-05-31",
  "status": "open",
  "gate": {"passed": 3, "total": 8, "open": false, "whose": "engine"},
  "scenarios": {"yes": "InP supply loosens.", "no": "Supply stays tight."},
  "checks": [
    {"id": "1", "label": "Market expectation", "pass": false, "detail": "…"}
  ],
  "evidence": [
    {"id": "s1", "title": "AAOI 10-Q", "url": "https://www.sec.gov/…", "publisher": "",
     "date": "", "category": "filing", "side": "for", "link_checked": true,
     "passage": "Revenue was $192M."}
  ],
  "catalysts": [{"date": "2026-11-05", "title": "Q3 report", "url": ""}],
  "exposure": [],
  "exposure_note": "Companies are named once all eight checks pass."
}

side is for, against or neutral. Once the gate is open, each exposure row is {symbol, company, role, direction, scenario, exposure, reason}.

Filings

GET/v1/tickers/{symbol}/filings

The company's ten latest filings, from the SEC's own list for it. A symbol that isn't an SEC filer answers "filer": false with no filings.

{
  "symbol": "AAPL",
  "filer": true,
  "cik": 320193,
  "filings": [
    {"date": "2026-10-31", "form": "10-K", "company": "Apple Inc.", "cik": 320193,
     "url": "https://www.sec.gov/Archives/edgar/data/320193/…/aapl-20260927.htm",
     "path": "edgar/data/320193/0000320193-26-000105.txt", "description": "10-K"}
  ],
  "source": "SEC EDGAR"
}

PE helpers

An LBO model worked out by code, and an investment committee memo drafted from it. Research only: every input is sourced or marked as yours, and the memo makes no recommendation — the decision is your committee's.

GET/v1/pe/lbo/prefill

?symbol=MU or ?cik=723125. A US SEC filer's latest fiscal year from its XBRL — revenue, operating income, D&A, capex, debt, cash and shares outstanding — each with its tag, period, form, filing date, accession number and the filing's link. EBITDA is operating income + D&A, and says so. A figure the filing doesn't give is listed in missing, never estimated. Market cap and price are licensed market data and are not served until Tibeb's display licence is in place (market.licensed: false).

POST/v1/pe/lbo

The model: sources and uses, the yearly projection, the debt schedule with mandatory amortization and the cash sweep (senior first), leverage and interest coverage each year, exit equity, IRR and MOIC, and two sensitivity grids (entry × exit multiple, senior leverage × exit multiple). No model call. A target is a filer ({"kind": "filer", "symbol": "MU"}, its figures from the SEC; a figure in figures stands over the filing's and is marked as yours) or a private company ({"kind": "private", "name": …} with figures.revenue and figures.ebitda). Percentages are in percent. Inputs that can't make a model answer 422 with each problem in words.

curl https://www.tibeb.trade/v1/pe/lbo \
  -H "Authorization: Bearer $TIBEB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target": {"kind": "private", "name": "Acme Packaging"},
       "figures": {"revenue": 500000000, "ebitda": 100000000},
       "assumptions": {"entry": {"mode": "multiple", "multiple": 9},
         "senior": {"multiple": 3.5, "rate": 8, "amort": 5}, "sub": {"multiple": 1.5, "rate": 11, "amort": 0},
         "sweep_pct": 75, "transaction_fee_pct": 2, "financing_fee_pct": 2.5, "min_cash": 20000000,
         "years": 5, "growth_pct": [6, 6, 5, 5, 4], "margin_pct": 20,
         "da_pct": 3, "capex_pct": 3.5, "nwc_pct": 10, "tax_pct": 25, "exit_multiple": 9}}'
{
  "sources_uses": {"uses": {"enterprise_value": 900000000.0, "total": …}, "sources": {"sponsor_equity": …}},
  "years": [{"year": 1, "revenue": …, "ebitda": …, "fcf": …, "senior_end": …, "leverage": …, "interest_coverage": …}, …],
  "returns": {"equity_invested": …, "equity_at_exit": …, "moic": …, "irr": …, "years": 5},
  "sensitivity": {"entry_exit": {"rows": [7, 8, 9, 10, 11], "cols": [7, 8, 9, 10, 11], "irr": [[…]], "moic": [[…]]}, …},
  "provenance": {"revenue": {"kind": "user", …}, …},
  "memo_ceiling_usd": …
}

POST/v1/pe/lbo.xlsx

The same body; the model as an Excel workbook — sheets Inputs, Model, Debt, Returns, Sensitivity and Sources. Every cell on Model, Debt and Returns is a live formula over Inputs, so changing an input recalculates the model. Each figure from the SEC carries a comment with its tag, period and filing, and a row with the link on Sources; your assumptions are marked as yours. The sensitivity grids are values (a grid of whole models needs a data table).

POST/v1/pe/ic-memo

{"lbo": <the body above>, "thesis_id": "w1", "with_10k": true}. One model call, booked to your key's login. The memo's sections — situation, business, thesis and value creation, financing and returns, key risks and mitigants, diligence questions, what would change the view — from the LBO's numbers, the SEC's figures, your thesis's evidence and passages of the latest 10-K. Code then checks it: a sentence with a figure that isn't in that material, a quote that isn't in the filing or a source's own words, or any recommendation wording is removed and listed under removed. ?format=docx or ?format=md returns the file; the JSON carries markdown and docx_base64.

Valuation models

The models a bank or buy-side analyst is trained on, beside the LBO — a three-statement model, a DCF, trading comparables, precedent transactions and a merger model — all code-only arithmetic over sourced inputs: no model call. A US filer's three statements come from its XBRL (GET /v1/pe/prefill), each line with its tag, period, form, filing date, accession number and link; a line the filing doesn't give is listed in missing, never estimated. Every driver and assumption is yours. A market cap or a current price is licensed market data: it reaches only a caller who may see it (market.licensed); anyone else may type one, and the answer says "as typed". Each model has a .xlsx variant: a workbook of live formulas with every sourced input commented and listed on a Sources sheet. Inputs that can't make a model answer 422 with each problem in words. Research, not advice: an implied price is not a target.

GET/v1/pe/prefill

?symbol=MU or ?cik=723125 (&what=statements, the default; what=lbo is the LBO's shorter read). The income statement, balance sheet and cash-flow statement for the latest three fiscal years (annual) and the latest quarters the filings report (quarterly), the cover's shares outstanding, a base case the filings' averages suggest for the three-statement model (suggested_drivers, each with how it was averaged — an offer, used only when you send it back), the 10-year Treasury par yield on file for the DCF's risk-free rate (risk_free_on_file), and the market cap and price for a licensed caller.

POST/v1/pe/three-statement

The base year from the filing (target: {"kind": "filer", "symbol": "MU"}; a line in figures stands over it) or typed ({"kind": "private", "name": …} with the base-year lines), years (3–10, five by default) and scenarios — a set of drivers per case (base, bull, bear, any names): growth_pct (one, or one a year), gross_margin_pct, sga_pct, rnd_pct, da_pct (da_basis revenue or ppe), capex_pct, dso, dio, dpo, tax_pct, interest_pct, revolver_pct, payout_pct, min_cash, and optionally debt_change and buybacks by year. Interest is on opening balances (nothing circular), the revolver is the plug and assets equal liabilities plus equity every year. Each case's statements, ratios and a side-by-side summary.

curl https://www.tibeb.trade/v1/pe/three-statement \
  -H "Authorization: Bearer $TIBEB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target": {"kind": "filer", "symbol": "MU"}, "years": 5,
       "scenarios": {"base": {"growth_pct": 8, "gross_margin_pct": 40, "sga_pct": 12, "rnd_pct": 10,
         "da_pct": 9, "capex_pct": 15, "dso": 45, "dio": 120, "dpo": 50, "tax_pct": 15,
         "interest_pct": 5, "revolver_pct": 7, "payout_pct": 10, "min_cash": 2000000000}}}'

POST/v1/pe/dcf

flows: {"source": "three_statement", "three_statement": <the body above>, "case": "base"} or typed lists (ebit, da, capex, nwc_change, optionally ebitda, and tax_pct); wacc: risk_free_pct (left out, the Treasury yield on file is used and said so), beta, erp_pct, cost_of_debt_pct, tax_pct, debt_weight_pct; terminal: growth_pct and optionally exit_multiple; mid_year; bridge (debt, cash, minority interest, preferred stock, diluted shares — the filing's unless typed); price (typed; a licensed caller's is Massive's). Unlevered free cash flow by year, both terminal values, EV and equity, the implied price, sensitivity grids over WACC × growth and WACC × exit multiple, and — with a price — the reverse DCF: the terminal growth, and the uniform growth of free cash flow, the price implies.

POST/v1/pe/comps

peers (tickers, up to 15), an optional target, market_caps by symbol (typed; a licensed caller's come from Massive) and figures that stand over the filings'. Each company's latest fiscal year — revenue, EBITDA (operating income + D&A), net income, debt, cash, diluted shares — EV / revenue, EV / EBITDA and P / E, the count, mean, median and quartiles (QUARTILE.INC), outliers by 1.5 × IQR, the target's implied range, and suggested: filers on the target's SIC line, suggested, never applied on their own.

POST/v1/pe/deal-comps

deals: each with target, acquirer, date, ev, revenue, ebitda, premium_pct and your source note (no free feed carries deal terms); a deal with no ev may name its 8-K (filing: {"cik", "accession"}) and the value is read from the filing's own words, kept only when a sentence states the figure, and quoted. figures for the target (or a filer's from the SEC). The multiples, the statistics, the premiums and the implied range.

POST/v1/pe/merger

acquirer and target (filers from the SEC, or private with figures), deal: offer_price or premium_pct (over target_price), stock_pct, new_debt_pct, acquirer_price, new_debt_rate_pct, cash_yield_pct, synergies (pre-tax, by year), fees, tax_pct, years (1–5) and optional net income growth for each side. Pro forma EPS and accretion / dilution by year, breakeven synergies, a stock-mix sensitivity, contribution analysis and a pro forma balance sheet with goodwill (purchase price − book equity; no write-ups, stated).

POST/v1/pe/three-statement.xlsx

POST/v1/pe/dcf.xlsx

POST/v1/pe/comps.xlsx

POST/v1/pe/deal-comps.xlsx

POST/v1/pe/merger.xlsx

The same bodies; each model as an Excel workbook of live formulas (Inputs and the model's sheets, the sensitivity grids as the engine's values, and Sources). A figure from the SEC is commented with its tag, period and filing; a figure a licence withholds is an empty cell with a comment saying why.

Rates

The U.S. Treasury's daily par yield curve (public domain), bootstrapped by code to zero rates, discount factors and forwards, and two calculators. Arithmetic only: no model call, nothing estimated. Every answer states its conventions in words, and a figure that can't be worked out is null with the reason in missing.

GET/v1/rates/curve

?date=2026-11-06 (optional): the newest day on file on or before it. The par points (each tenor in years), the same a week, a month and a year before, the zero curve (compounded semiannually) with each node's discount factor, the one-year forward from every half year, the named forwards (1y1y, 2y1y, 3y2y, 5y5y, 10y10y, 20y10y), the measures — 3m10y, 2s10s, 5s30s and the 2s5s10s butterfly in basis points from the Treasury's own points, the 5- and 10-year breakevens in percent (nominal less real par) — the real curve and the bills. Bills out to a year are read on the Treasury's bond-equivalent basis; from a year out the par bonds are bootstrapped on a half-year grid, between the Treasury's points by a monotone cubic (PCHIP).

curl "https://www.tibeb.trade/v1/rates/curve?date=2026-11-06" \
  -H "Authorization: Bearer $TIBEB_KEY"
{
  "day": "2026-11-06",
  "par": [{"label": "3M", "t": 0.25, "rate": 3.88}, …, {"label": "10Y", "t": 10.0, "rate": 4.08}, …],
  "compare": {"1w": {"day": "2026-10-30", "par": […]}, "1m": {"day": "2026-10-06", "par": […]}, "1y": …},
  "zero": [{"t": 0.083333, "label": "1M", "zero": 3.9725, "df": 0.99672741}, …],
  "forwards": [{"key": "1y1y", "start": 1.0, "end": 2.0, "forward": 3.3739}, …],
  "measures": {"3m10y": 20.0, "2s10s": 56.0, "5s30s": 105.0, "2s5s10s": -38.0, "be5": 2.19, "be10": 2.22},
  "conventions": {"interpolation": "…", "bootstrap": "…", "zero": "…", "forward": "…"},
  "source": "U.S. Department of the Treasury, daily Treasury par yield curve rates (public domain)"
}

POST/v1/rates/bond

A fixed-coupon bond: settlement, maturity, coupon (percent of face) and either the clean price per 100 or the yield (percent); optionally frequency (1, 2, 4 or 12; 2 by default), day_count (30/360, US, by default, or ACT/ACT, ICMA), redemption, face and calls ([{"date": "2031-11-15", "price": 100}]). The clean and dirty price, accrued interest, the yield to maturity (compounded at the coupon frequency, simple interest in the final period), Macaulay and modified duration, convexity, DV01, the yield to each call and to worst, and the G-spread and Z-spread over the Treasury curve on file on the settlement date, with the cash flows.

curl https://www.tibeb.trade/v1/rates/bond \
  -H "Authorization: Bearer $TIBEB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"settlement": "2026-11-09", "maturity": "2036-11-15", "coupon": 4.25, "price": 99.5,
       "calls": [{"date": "2031-11-15", "price": 100}]}'
{
  "clean": 99.5, "dirty": 101.554167, "accrued": 2.054167, "ytm": 4.311907,
  "macaulay": 8.088608, "modified": 7.917901, "convexity": 76.075, "dv01": 0.08041,
  "calls": [{"date": "2031-11-15", "price": 100.0, "ytc": 4.361882}], "ytw": 4.311907, "ytw_is": "maturity",
  "g_spread_bp": 23.06, "z_spread_bp": 23.35, "curve_day": "2026-11-06",
  "cash_flows": [{"date": "2026-11-15", "t": 0.016667, "amount": 2.125}, …],
  "conventions": {"day_count": "…", "yield": "…", "dv01": "…", "g_spread": "…", "z_spread": "…"}, "missing": []
}

POST/v1/rates/mm

A bill on each basis: days to maturity (or settlement and maturity) and exactly one of price (per 100), discount, bey or mmy (percent). The price, the bank-discount yield and the money-market yield (both ACT/360) and the bond-equivalent yield — the Treasury's investment rate: simple interest out to half a year, its quadratic beyond, on a 366-day year when the twelve months after settlement hold a 29 February.

curl https://www.tibeb.trade/v1/rates/mm \
  -H "Authorization: Bearer $TIBEB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"days": 91, "discount": 3.79}'
{"days": 91, "given": "discount", "price": 99.041972, "discount": 3.79, "mmy": 3.82666, "bey": 3.879809,
 "year_days": 365, "conventions": {…}}

Agent runs

An agent is the research engine's own stages, run in order on one of your theses — the same work the site does when you press its buttons. Each step keeps its status, how long it took, what it cost and the gate after it. A step that needs a person (the lock, which pre-registers what the thesis expects) waits for an approval.

GET/v1/agents

The agents there are, their steps, and which steps wait for approval.

AgentSteps
evidence-builderdiligence: builds the evidence ledger and checks every link
committeecommittee: six seats, their answers pooled by code
exposure-mapperexposure: once all eight checks pass, reads the companies' newest annual reports
full-reviewadopt, edge, lock (waits for approval), catalysts, diligence, committee, exposure
unlockworks a thesis through the eight checks, asking you where a person decides

POST/v1/agents/{agent}/runs

Start an agent on a thesis. start_at (optional) is the step to start from.

curl -X POST https://www.tibeb.trade/v1/agents/committee/runs \
  -H "Authorization: Bearer $TIBEB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"thesis": "w1"}'
{
  "id": "run_3f9a0c1b2d4e5f60",
  "agent": "committee",
  "thesis": "w1",
  "status": "queued",
  "steps": [
    {"n": 0, "name": "committee", "status": "queued", "started": null, "finished": null,
     "seconds": null, "cost_usd": null, "words": "", "error": "", "gate": null,
     "needs_approval": false}
  ],
  "created": "2026-11-09T09:00:00+00:00",
  "updated": "2026-11-09T09:00:00+00:00",
  "cost_usd": 0,
  "input": {"thesis": "w1", "engine_id": "t_…", "start_at": 0, "auto": false}
}

A run's status is queued, running, waiting, completed, failed, cancelled or interrupted (the server restarted under it). A failed step says why in plain words and skips the steps after it. A run never carries a login id or an email.

GET/v1/runs

Your runs, newest first.

GET/v1/runs/{id}

One run, as above.

GET/v1/runs/{id}/events

Server-sent events: an event: run with the whole run each time it changes, then event: end when it has ended.

curl -N https://www.tibeb.trade/v1/runs/run_3f9a0c1b2d4e5f60/events \
  -H "Authorization: Bearer $TIBEB_KEY"

POST/v1/runs/{id}/approve

Approve a waiting run. A step that asked a question takes an answer: {"answer": {"choice": "…", "fields": {…}, "url": "…"}}; no body approves. 409 when the run isn't waiting.

POST/v1/runs/{id}/cancel

Stop a run between steps. 409 once it has ended.

POST/v1/runs/{id}/steps/{n}/rerun

A new run of the same agent on the same thesis, from step n on.

Webhooks

Tibeb POSTs a run to your address when it completes, fails or waits for an approval (run.completed, run.failed, run.waiting). Up to five webhooks a login.

POST/v1/webhooks

{"url": "https://…", "events": ["run.completed"]} — events is optional (all three). The address must be https, with no credentials in it, and on the public internet: a private, loopback, link-local or carrier-grade NAT address is refused when it is registered and again each time a hook is sent, and the request goes to the very address that was checked.

{
  "id": "hook_8c1d2e3f4a5b",
  "url": "https://example.com/tibeb",
  "events": ["run.completed", "run.failed", "run.waiting"],
  "secret": "whsec_…",
  "note": "Keep the secret: it is not shown again."
}

GET/v1/webhooks

Your webhooks (without their secrets) and the events there are.

DELETE/v1/webhooks/{id}

Remove one.

What arrives

POST /tibeb HTTP/1.1
Content-Type: application/json
X-Tibeb-Event: run.completed
X-Tibeb-Signature: sha256=5d41402abc4b2a76b9719d911017c592…

{"event": "run.completed", "at": "2026-11-09T09:04:12+00:00", "run": {…the run, as above…}}

Verifying the signature

The signature is HMAC-SHA256 of the raw request body, keyed with the webhook's secret, as sha256=<hex>. Compute it over the bytes you received — before any JSON parsing — and compare in constant time.

Python

import hashlib
import hmac


def verify(secret: str, body: bytes, header: str) -> bool:
    want = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, header or "")

Node

const crypto = require('node:crypto');

function verify(secret, rawBody, header) {
  const want = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(want);
  const b = Buffer.from(header || '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

MCP

POST/mcp

The Model Context Protocol over HTTP (JSON-RPC 2.0, protocol 2025-06-18), with the same key. Methods: initialize, ping, tools/list and tools/call; a notification (no id) gets 202 and no body, and a batch (a JSON array) gets an array back. The tools are the REST routes under other names, so your code and your AI tools see one thing.

ToolArgumentsSame as
list_thesesnoneGET /v1/theses
get_thesisidGET /v1/theses/{id}
ticker_filingssymbolGET /v1/tickers/{symbol}/filings
start_runagent, thesisPOST /v1/agents/{agent}/runs
get_runidGET /v1/runs/{id}
build_lbotarget, figures, assumptionsPOST /v1/pe/lbo
draft_ic_memolbo, thesis_id, with_10kPOST /v1/pe/ic-memo
build_three_statementtarget, figures, years, scenariosPOST /v1/pe/three-statement
build_dcfflows, wacc, terminal, bridge, price, …POST /v1/pe/dcf
build_compspeers, target, market_caps, figuresPOST /v1/pe/comps
build_mergeracquirer, target, figures, dealPOST /v1/pe/merger
treasury_curvedate (optional)GET /v1/rates/curve
bond_analyticssettlement, maturity, coupon, price or yield, …POST /v1/rates/bond
curl https://www.tibeb.trade/mcp \
  -H "Authorization: Bearer $TIBEB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
       "params": {"name": "get_thesis", "arguments": {"id": "w1"}}}'
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [{"type": "text", "text": "{\"id\": \"w1\", \"question\": …}"}],
    "structuredContent": {"id": "w1", "question": "…"},
    "isError": false
  }
}

A tool that can't answer (no such thesis, say) returns "isError": true with the reason as text. Protocol errors use JSON-RPC codes:

CodeMeaning
-32600Invalid request (not a JSON-RPC 2.0 message)
-32601Method not found
-32602Unknown tool
-32603Internal error (said by its kind, never the server's text)

The owner's records

GET/v1/plans the owner's key only

The desk's plans.

GET/v1/journal the owner's key only

The desk's journal and its scorecard. Any other key gets 403.

OpenAPI

The /v1 routes and /mcp are described in OpenAPI 3 at /v1/openapi.json — public, the routes' shapes only, no records and no secrets. Point a client generator at it.

Questions, or a key for a pilot: hello@tibeb.trade. How the data is handled: Security.