JAWZDocs · from code

Jawz from code

The read tools over plain HTTP — from a script, a server, a notebook, a cron job.

Jawz is usually described as something an AI assistant connects to. That is one caller, not the only one. The same endpoint answers anything that can make a web request: no AI, no account, no key, no SDK. A macro regime read, its history, financial conditions, global liquidity, and the Loop chapters themselves are all a single POST away. It is the same door the agents use — there is no separate developer API, and therefore nothing that can drift out of sync with what the agents see.

The call

One endpoint, one method, JSON-RPC in the body. Three things are required and the third is the one people miss:

  • POST https://jawz.ai/api/mcp
  • Content-Type: application/json
  • Accept: application/json, text/event-stream both types, or the request is refused with 406 Not Acceptable.

The body names a tool and its arguments:

{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"<tool>","arguments":{ ... }}}

Reading the response

Tool results come back SSE-framed. The body is not bare JSON — it is an event stream, so you get an event: message line, then a data: line holding the JSON object. Take the data: line and strip its six-character prefix. Transport-level errors (a malformed body, a missing Accept header) skip the framing and arrive as plain JSON, so parse defensively: use the data: line if there is one, otherwise the body itself. Every snippet on this page does exactly that.

Inside, prefer result.structuredContent. The response also carries result.content[0].text, which is the same object serialised as a string for AI clients to read — identical content, but it costs you a second JSON.parse. Code should read structuredContent and ignore the text.

Failures return HTTP 200. A bad argument is not a 4xx — it is a normal result with result.isError: true and a readable explanation in the text field. Check that flag; checking the status code alone will make errors look like successes.

curl

Twelve weeks is the default lookback; this asks for four, weekly:

curl -s https://jawz.ai/api/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"get_regime_history",
                 "arguments":{"lookback_weeks":4,"interval":"weekly"}}}' \
  | grep '^data: ' | sed 's/^data: //' \
  | jq '.result.structuredContent.data.rows[]
        | {as_of, regime, cycle, provenance}'
{ "as_of": "2026-08-02", "regime": "RED",    "cycle": "FALL",   "provenance": "observed" }
{ "as_of": "2026-08-09", "regime": "YELLOW", "cycle": "FALL",   "provenance": "observed" }
{ "as_of": "2026-08-16", "regime": "YELLOW", "cycle": "FALL",   "provenance": "observed" }
{ "as_of": "2026-08-23", "regime": "YELLOW", "cycle": "SUMMER", "provenance": "observed" }
{ "as_of": "2026-08-30", "regime": "YELLOW", "cycle": "SUMMER", "provenance": "observed" }

provenance is worth reading, not skipping. observed means the row was written the day it was read and never recomputed. reconstructed means it was rebuilt from the data as it stood on that date — vintage-correct, without look-ahead, but produced by today’s engine.

Python

import json, requests

def jawz(tool, **args):
    r = requests.post(
        "https://jawz.ai/api/mcp",
        headers={
            "Content-Type": "application/json",
            "Accept": "application/json, text/event-stream",
        },
        json={"jsonrpc": "2.0", "id": 1, "method": "tools/call",
              "params": {"name": tool, "arguments": args}},
        timeout=30,
    )
    # Tool results are SSE-framed; transport errors arrive as plain JSON.
    body = r.text
    line = next((l[6:] for l in body.splitlines() if l.startswith("data: ")), body)
    msg = json.loads(line)
    if "error" in msg:
        raise RuntimeError(msg["error"]["message"])
    result = msg["result"]
    if result.get("isError"):
        raise RuntimeError(result["content"][0]["text"])
    return result["structuredContent"]

read = jawz("get_regime_history", lookback_weeks=4, interval="weekly")
for row in read["data"]["rows"]:
    print(row["as_of"], row["regime"], row["cycle"], row["provenance"])

Node

async function jawz(tool, args = {}) {
  const res = await fetch("https://jawz.ai/api/mcp", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Accept: "application/json, text/event-stream",
    },
    body: JSON.stringify({
      jsonrpc: "2.0", id: 1, method: "tools/call",
      params: { name: tool, arguments: args },
    }),
  });
  // Tool results are SSE-framed; transport errors arrive as plain JSON.
  const body = await res.text();
  const line = body.split("\n").find((l) => l.startsWith("data: "));
  const msg = JSON.parse(line ? line.slice(6) : body);
  if (msg.error) throw new Error(msg.error.message);
  if (msg.result.isError) throw new Error(msg.result.content[0].text);
  return msg.result.structuredContent;
}

const read = await jawz("get_conditions_history", { lookback_weeks: 4 });
for (const row of read.data.rows) {
  console.log(row.as_of, row.composite, row.direction, row.real_yield_10y_pct);
}

Finding the tools

There is no tool table on this page, deliberately — a hand-written list drifts, and a drifted list is worse than none. Ask the server instead. tools/list returns every tool you can call, each with its name, description, and a JSON Schema for its arguments:

curl -s https://jawz.ai/api/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
  | grep '^data: ' | sed 's/^data: //' \
  | jq -r '.result.tools[]
           | "\(.name)\t\(.inputSchema.properties | keys | join(", "))"'
get_started
get_world_brief          list_only, slug
get_prices               base_currency, holdings
get_macro_regime
get_regime_history       interval, lookback_weeks
get_liquidity_history    interval, lookback_weeks
...

Twenty-two tools, and what comes back is exactly the set that answers you — the list is the contract, not a menu with locked items on it.

Arguments are typed strictly

The schema from tools/list is enforced, literally. {"lookback_weeks": 4} works; {"lookback_weeks": "4"} is rejected — numbers are not coerced from strings, and enums accept only their listed values. The rejection tells you precisely what was wrong (expected number, received string, with the offending path), so read the error rather than guessing. Omit an optional argument entirely and you get its documented default.

Prices show their work

get_prices takes holdings (symbol + quantity pairs) and, since 2026-09-14, answers per holding with four blocks a program can check instead of trust: asset (what the symbol resolved to: type, exchange, ISIN, provider id), resolution (how surely: matched_by is isin, qualified_symbol or bare_symbol, with the candidate count and any mismatch against your hints), quote (the native price with its own timestamp and source) and fx (the rate actually applied, dated and sourced). position carries the value native and in the base currency. Every field that existed before keeps its meaning and value.

Three optional hints narrow a resolution: isin (equities, ETFs, funds), exchange (NASDAQ, NYSE, XETRA, OSL, MIL, LSE, …) and asset_class. A top-level base_currency converts every line; "native" leaves each line in its quote currency. A hint that contradicts the result is reported in resolution.mismatch, not silently obeyed.

Two rules you can rely on. A bare symbol that matches both a listed instrument and a crypto token (Robinhood, Backpack and xStocks list tokenized stock proxies under the equity's own ticker) prices the listed instrument; if no exactly-named listing exists the line is refused with a suggestion, never priced as the token. And a failed lookup is not a finding: when a data provider is rate-limited or errors, the line comes back unpriced with a retry note — never a refusal, never a substitute value, never cached.

{"raw":"ARKG.DE","price":8.47,"source":"yahoo",
 "warning":""ARKG.DE" priced as ARKG.MI — the Xetra line returns no data; the Milan listing (EUR) prices.",
 "asset":{"type":"equity","symbol":"ARKG","exchange":"MIL","id":"yahoo:ARKG.MI"},
 "resolution":{"matched_by":"qualified_symbol","candidates":1,"mismatch":null},
 "quote":{"price":7.325,"currency":"EUR","datetime":"2026-09-14T14:29:59Z","source":"yahoo"},
 "fx":{"pair":"EURUSD","rate":1.1558,"datetime":"2026-09-14T21:07:57Z","source":"yahoo"},
 "position":{"quantity":1,"value_native":7.325,"value_base":8.47}}

The collision list lives in code, one row per symbol that has gone wrong, each with the date and the evidence, so a reader can re-run the check rather than trust the row.

Limits

  • 300 calls per day per caller, and 25 per day for get_prices and get_etf_profile, which spend third-party data quota. Counted through a daily-rotating hash — never identified, never stored as an address.
  • Read-only. Everything reachable this way reads; nothing writes, and nothing is kept about the caller.
  • Data cadence. Most series refresh once daily after the underlying publishers post. Polling faster than that spends your ceiling to receive the same numbers; get_data_health reports every source’s freshness and is the honest way to know whether anything has actually moved.

Will the numbers change under you?

Only loudly. Jawz publishes a regime engine changelog, and the commitment on it is the one that matters if you are building on this: no change to classification semantics ships without a dated entry there. Thresholds, input sets, confirmation windows, fallback rules, basis changes — anything that could move a regime colour or cycle quadrant for the same underlying data. Formatting and field-naming changes are excluded on purpose, so the page stays worth reading.

If your model is pre-registered, record that page’s latest entry date next to your configuration hash. The comparison then stays auditable from both ends.

If you would rather not write the code

Connecting an AI assistant to the same endpoint takes one paste and no scripting — that path is at /docs/mcp. What the four chapters actually do with these reads is in the Loop guide.

Something missing?

A field you need, a shape that fights your pipeline, a limit that is in your way: hello@jawz.ai. This page exists because an integrator asked a question we had never answered in writing — most of the roadmap arrives the same way.