Headless Oracle

Integration Guide

Headless Oracle provides a cryptographically verifiable defensive execution layer for autonomous agents operating across global markets.

Chirindo Witness quickstart

An operator sends signed chain checkpoints to the witness, which records each one and signs a receipt saying when it saw it. The witness attests only “at time T, I received this checkpoint, validly signed by the key with this thumbprint”. It does not attest who owns the key, nor that the records are true.

What it adds: a cut-off tail, or a rewrite by the key holder (edit, delete or reorder, then re-sign and re-link), of any history up to the last witnessed checkpoint, made after that checkpoint was witnessed, is detected when the chain is compared with the witness's receipts. Records after the last witnessed checkpoint, and a history rewritten before it was first witnessed, are not. The full list is in the Witness spec, under honest_limits.

The free pool needs no account and no key: anonymous checkpoints share one daily pool (figure in the plan table below). Because anyone can submit without an account, anyone can use up the rate limit or the daily cap until it resets; paid plans get their own key and quota.

Send your first checkpoint

Witness accepts only a checkpoint signed with an Ed25519 key, so the request reads its body from a file. This script makes a key and a one-entry checkpoint and writes checkpoint.json. It needs Node 20 or later and nothing else.

checkpoint.mjs

import { generateKeyPairSync, sign, createHash } from "node:crypto"; import { writeFileSync } from "node:fs";
const { privateKey, publicKey } = generateKeyPairSync("ed25519"), x = publicKey.export({ format: "jwk" }).x;
const sha = (s, enc) => createHash("sha256").update(s).digest(enc), kid = sha(`{"crv":"Ed25519","kty":"OKP","x":"${x}"}`, "base64url");
const cp = { count: 1, kid, last_entry_hash: "sha256:" + sha("first entry", "hex"), session_id: "try-" + Date.now(), ts: new Date().toISOString(), type: "checkpoint", v: "evidence.action/1" };
cp.sig = sign(null, Buffer.from(JSON.stringify(cp)), privateKey).toString("base64url");
writeFileSync("checkpoint.json", JSON.stringify({ checkpoint: cp, public_key_jwk: { kty: "OKP", crv: "Ed25519", x } }));
node checkpoint.mjs
curl -sS https://api.headlessoracle.com/v1/witness/checkpoints \
  -H 'Content-Type: application/json' \
  --data-binary @checkpoint.json

Witness answers 201 with a receipt signed by Headless Oracle. The checkpoint's keys are written in sorted order, so JSON.stringify gives the exact bytes the spec signs. With the gate, the count and the head hash come from your log; here they stand in for one placeholder entry.

With the Chirindo gate in front of your MCP server, checkpoints come from your real log: see /witness for npx -y @headlessoracle/chirindo init, proxy and verify.

Verify a receipt

  1. Fetch the receipts from Witness yourself. Public, no auth: GET https://api.headlessoracle.com/v1/witness/checkpoints?kid=<thumbprint>&session_id=<id>. Verifying against a sidecar file trusts the operator who supplied it. Only querying the witness is independent of the operator.
  2. Take the Witness public key. Use public_key (hex, 32 raw bytes) of the keys[] entry in GET https://api.headlessoracle.com/v5/keys whose key_id equals the receipt's public_key_id, and pin it.
  3. Check the signature. The signed bytes are all receipt fields except signature, keys sorted, JSON.stringify with no whitespace, UTF-8; the signature is Ed25519, hex.
  4. Compare the log with the receipts. In your browser at /verify, or offline with @headlessoracle/receipt-verify and chirindo verify. A log shorter than a witnessed count, or different from what Witness saw at that count, fails, and the check names the count where it fails.

Plans

PlanPlan idPriceQuota
Free poolwitness_free$02,000 new checkpoints per UTC day, shared by every anonymous caller
Evidence Startercustody_90d$49/monthUp to 1,000 new checkpoints per UTC day, your own key
Evidencecustody_1y$199/monthUp to 3,000 new checkpoints per UTC day, your own key

Buy from /pricing, or start the checkout yourself. POST /v5/checkout returns a transaction_id for Paddle checkout and a claim_token; after payment, POST /v5/claim with the claim_token answers {"state":"ready","key":"…"} once the payment is recorded. Until then it can answer 404 or carry retry_after_seconds, so poll.

curl -sS -X POST https://headlessoracle.com/v5/checkout \
  -H 'Content-Type: application/json' \
  -d '{"plan":"custody_90d"}'

curl -sS -X POST https://headlessoracle.com/v5/claim \
  -H 'Content-Type: application/json' \
  -d '{"claim_token":"<claim_token from the checkout response>"}'

Machine-readable: the Witness spec (request format, every check in order, receipt format, signing and its limits) and the prices at /v5/pricing.

28 exchanges. Global 24/7 coverage.

The scheduling complexity a timezone library was not built to solve.

67

exchange-specific holidays

different per country, non-overlapping

18

early close days

NYSE closes at 1:00 PM, not 4:00 PM

8

DST transitions

incl. 3-week US/EU phantom window

490

lunch break sessions

Tokyo 11:30–12:30, HK 12:00–13:00

728

weekend days across 28 venues

per calendar year

A timezone library handles zero of these. Headless Oracle handles all of them.

Think it's simple?

Common assumptions about market schedules, and what the data actually shows.

What you assume What actually happens
NYSE closes at 4pm ET Except Good Friday (1pm), day before July 4th (1pm), Christmas Eve (1pm), and emergency halts at any time
Tokyo is open 9am–3pm JST There's a lunch break 11:30–12:30 every trading day. Your bot thinks it's open. It's not.
"Just use a timezone library" pytz handles DST. It doesn't handle that NYSE closes for Juneteenth but LSE doesn't, or that Hong Kong closes for Lunar New Year but Singapore doesn't.
"I'll hardcode the holidays" Which year? 2026 holidays differ from 2027. When a holiday falls on a weekend, some exchanges shift to Friday, others Monday, others don't shift.
"I'll check once at market open" Circuit breaker at 2:47pm. Your cached "OPEN" is now wrong. Your bot trades into a halted market.

Quick Start

Using Claude Code? Add Headless Oracle to Claude Code in 3 steps → Copy one .mcp.json file and your agent has pre-trade verification instantly.

Try the public demo endpoint instantly — no API key needed:

curl https://headlessoracle.com/v5/demo

# Any supported exchange:
curl "https://headlessoracle.com/v5/demo?mic=XLON"
curl "https://headlessoracle.com/v5/demo?mic=XJPX"

For production use, fetch a signed attestation with your API key:

curl -X GET "https://headlessoracle.com/v5/status?mic=XNYS" \
     -H "X-Oracle-Key: YOUR_API_KEY"

Check when the next session opens (no auth required):

curl "https://headlessoracle.com/v5/schedule?mic=XNYS"

Verify Oracle's signing infrastructure is live:

curl "https://headlessoracle.com/v5/health"

Supported Exchanges

28 exchanges across 7 regions (including derivatives and 24/7 crypto). All DST transitions are handled server-side using IANA timezone data — no hardcoded UTC offsets.

MIC Exchange Local Hours Timezone DST
XNYS New York Stock Exchange 09:30 – 16:00 America/New_York Mar 8
XNAS NASDAQ 09:30 – 16:00 America/New_York Mar 8
XBSP B3 (São Paulo Stock Exchange) 10:00 – 17:00 America/Sao_Paulo Brazil (Nov)
XLON London Stock Exchange 08:00 – 16:30 Europe/London Mar 29
XPAR Euronext Paris 09:00 – 17:30 Europe/Paris Mar 29
XSWX SIX Swiss Exchange 09:00 – 17:30 Europe/Zurich Mar 29
XMIL Borsa Italiana (Milan) 09:00 – 17:30 Europe/Rome Mar 29
XHEL Nasdaq Helsinki 10:00 – 18:30 Europe/Helsinki Mar 29
XSTO Nasdaq Stockholm 09:00 – 17:30 Europe/Stockholm Mar 29
XIST Borsa Istanbul 10:00 – 18:00 Europe/Istanbul Mar 29
XSAU Saudi Exchange (Tadawul) Sun–Thu 10:00 – 15:00 Asia/Riyadh None
XDFM Dubai Financial Market Sun–Thu 10:00 – 14:00 Asia/Dubai None
XJSE Johannesburg Stock Exchange 09:00 – 17:00 Africa/Johannesburg None
XSHG Shanghai Stock Exchange 09:30 – 15:00 (lunch 11:30–13:00) Asia/Shanghai None
XSHE Shenzhen Stock Exchange 09:30 – 15:00 (lunch 11:30–13:00) Asia/Shanghai None
XHKG Hong Kong Exchanges and Clearing 09:30 – 16:00 (lunch 12:00–13:00) Asia/Hong_Kong None
XJPX Japan Exchange Group (Tokyo) 09:00 – 15:30 (lunch 11:30–12:30) Asia/Tokyo None
XKRX Korea Exchange (Seoul) 09:00 – 15:30 Asia/Seoul None
XBOM BSE Ltd (Bombay Stock Exchange) 09:15 – 15:30 Asia/Kolkata None
XNSE National Stock Exchange of India 09:15 – 15:30 Asia/Kolkata None
XSES Singapore Exchange 09:00 – 17:00 Asia/Singapore None
XASX Australian Securities Exchange (Sydney) 10:00 – 16:00 Australia/Sydney Oct (AU)
XNZE New Zealand Exchange (Auckland) 10:00 – 16:45 Pacific/Auckland Oct (NZ)

Also covered, not listed in the table: XCBT, XNYM, XCBO, XCOI and XBIN.

All times are local to the exchange. Retrieve the full exchange directory programmatically at /v5/exchanges.

2026 DST Risk Events

Any bot using hardcoded UTC offsets will compute incorrect hours after these dates:

  • Mar 8 — US clocks spring forward (EST → EDT). Affects XNYS, XNAS.
  • Mar 29 — UK/EU clocks spring forward (GMT/CET → BST/CEST). Affects XLON, XPAR.
  • Oct 25 — UK/EU clocks fall back. Affects XLON, XPAR.
  • Nov 1 — US clocks fall back. Affects XNYS, XNAS.

Headless Oracle handles all transitions automatically.

Available Endpoints

GET /v5/status Authenticated

Primary endpoint for production market status checks.

Header: X-Oracle-Key: YOUR_API_KEY
Query:  mic=XNYS  (or any of the 28 supported MIC codes — see /v5/exchanges)

GET /v5/demo Public

Returns a signed receipt for any exchange. No auth required. Use this for integration testing and verification development.

Query: mic=XNYS  (optional, defaults to XNYS)

GET /v5/schedule Public

Returns the next scheduled open and close times for any exchange. Includes lunch_break for exchanges with midday breaks (XJPX, XHKG). Does not reflect real-time halts or manual overrides.

{
  "mic": "XJPX",
  "name": "Japan Exchange Group (Tokyo)",
  "timezone": "Asia/Tokyo",
  "queried_at": "2026-03-10T01:00:00.000Z",
  "current_status": "OPEN",
  "next_open":  "2026-03-10T03:30:00.000Z",
  "next_close": "2026-03-10T06:30:00.000Z",
  "lunch_break": { "start": "11:30", "end": "12:30" },
  "note": "Times are UTC. lunch_break times are local exchange time (see timezone field)."
}

// Exchanges without a lunch break return lunch_break: null
// { ..., "lunch_break": null, ... }

GET /v5/exchanges Public

Returns the full directory of supported exchanges with MIC codes, names, and timezones.

GET /v5/keys Public

Returns the current Ed25519 public key registry and the canonical payload specification for independent signature verification.

{
  "keys": [{
    "key_id":      "key_2026_v1",
    "algorithm":   "Ed25519",
    "format":      "hex",
    "public_key":  "03dc27993a2c90856cdeb45e228ac065f18f69f0933c917b2336c1e75712f178",
    "valid_from":  "2026-01-01T00:00:00Z",
    "valid_until": null
  }],
  "canonical_payload_spec": { ... }
}

// valid_until is null when no key rotation is scheduled.
// It will be set in advance of any planned rotation.

GET /v5/batch Authenticated

Fetch signed status receipts for multiple exchanges in one request. Each receipt is independently signed and verifiable in isolation.

Header: X-Oracle-Key: YOUR_API_KEY
Query:  mics=XNYS,XNAS,XLON  (comma-separated, deduplicated)

All MICs are validated up front — one invalid MIC returns 400 for the whole request. Tier 3 signing failure fails the whole batch.

GET /v5/health Public

Signed liveness probe. Use this to distinguish Oracle is down from market is genuinely UNKNOWN. A valid signed response means the signing infrastructure is alive.

{
  "receipt_id":    "uuid-v4",
  "issued_at":     "2026-03-10T13:00:00.000Z",
  "expires_at":    "2026-03-10T13:01:00.000Z",
  "status":        "OK",
  "source":        "SYSTEM",
  "public_key_id": "key_2026_v1",
  "signature":     "hex_string"
}

// 200 + valid signature = Oracle is alive
// 500 CRITICAL_FAILURE = signing system offline

Response Schema

{
  "receipt_id":     "uuid-v4",
  "issued_at":      "ISO-8601-Timestamp (UTC)",
  "expires_at":     "ISO-8601-Timestamp (UTC, issued_at + 60s)",
  "mic":            "XNYS",
  "status":         "OPEN",       // OPEN | CLOSED | HALTED | UNKNOWN
  "source":         "SCHEDULE",   // SCHEDULE | OVERRIDE | SYSTEM
  "schema_version": "v5.0",
  "public_key_id":  "key_2026_v1",
  "signature":      "hex_string"
}

// Do not act on a receipt whose expires_at has passed.

Status Values

  • OPEN: The venue is in an active trading session. Single-name halts do not change venue status; see /halt-gate.
  • CLOSED: Market is outside trading hours — weekend, holiday, or after close.
  • HALTED: Market has been manually halted (circuit breaker or emergency override). Treat identically to CLOSED.
  • UNKNOWN: Oracle cannot determine status. Treat as CLOSED. Halt all execution.

Source Values

  • SCHEDULE: Status derived from the exchange holiday/hours calendar.
  • OVERRIDE: Status set manually. Active during circuit breakers or emergency halts. Includes a reason field.
  • SYSTEM: Internal fallback — returned when an error occurred in the primary logic. Always paired with UNKNOWN.

Verification Logic

To confirm a receipt is authentic, reconstruct the signed payload and verify the Ed25519 signature against the published public key.

1. Reconstruct the Payload

The signature covers a fixed allowlist of receipt fields enumerated in canonical_payload_spec at /v5/keys (union of receipt_fields, override_fields, health_fields). Filter the response to that allowlist, sort the keys alphabetically, then minify with no whitespace. Decoration the worker added to the response after signing — extensions, a duplicated receipt wrapper, discovery_url — must be excluded so the message bytes match what was signed.

// 1. Fetch the canonical field spec from /v5/keys
const { keys, canonical_payload_spec: spec } =
  await fetch("https://headlessoracle.com/v5/keys").then(r => r.json());

// 2. Build the allowlist (union of all receipt-type field lists)
const canonicalFields = [
  ...spec.receipt_fields,
  ...(spec.override_fields || []),
  ...(spec.health_fields  || []),
].filter((v, i, a) => a.indexOf(v) === i).sort();

// 3. Filter the receipt to the allowlist (drops signature + decoration)
const filtered = {};
for (const key of canonicalFields) {
  if (key !== "signature" && key in receipt) filtered[key] = receipt[key];
}
const canonical = JSON.stringify(filtered); // no spaces, alphabetical keys

2. Implementation Samples

Python (PyNaCl)

import json, requests
from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError

ORACLE_BASE = "https://headlessoracle.com"

def verify_and_check(mic="XNYS"):
    # Fetch the receipt and the public key + canonical field spec
    receipt = requests.get(
        f"{ORACLE_BASE}/v5/status?mic={mic}",
        headers={"X-Oracle-Key": "YOUR_KEY"},
        timeout=4,
    ).json()
    keys_doc = requests.get(f"{ORACLE_BASE}/v5/keys", timeout=4).json()
    public_key_hex = keys_doc["keys"][0]["public_key"]
    spec           = keys_doc["canonical_payload_spec"]

    # Build the allowlist (union of receipt / override / health fields)
    canonical_fields = sorted(set(
        spec["receipt_fields"]
        + spec.get("override_fields", [])
        + spec.get("health_fields",  [])
    ))

    # Filter the receipt to the allowlist — drops signature + decoration
    # (extensions, discovery_url, duplicate `receipt` wrapper)
    filtered = {k: receipt[k] for k in canonical_fields
                if k != "signature" and k in receipt}

    # Canonical message: alphabetical sort, no whitespace, UTF-8
    canonical = json.dumps(filtered, sort_keys=True, separators=(",", ":"))
    sig       = bytes.fromhex(receipt["signature"])

    try:
        VerifyKey(bytes.fromhex(public_key_hex)).verify(canonical.encode(), sig)
    except BadSignatureError:
        return False  # fail closed
    return receipt["status"] == "OPEN"

JavaScript (Web Crypto API)

const ORACLE_BASE = "https://headlessoracle.com";

async function verifyReceipt(receipt) {
  // Fetch the public key + canonical field spec
  const { keys, canonical_payload_spec: spec } =
    await fetch(`${ORACLE_BASE}/v5/keys`).then(r => r.json());

  // Build the allowlist (union of receipt / override / health fields)
  const canonicalFields = [
    ...spec.receipt_fields,
    ...(spec.override_fields || []),
    ...(spec.health_fields  || []),
  ].filter((v, i, a) => a.indexOf(v) === i).sort();

  // Filter the receipt to the allowlist — drops signature + decoration
  const filtered = {};
  for (const key of canonicalFields) {
    if (key !== "signature" && key in receipt) filtered[key] = receipt[key];
  }
  const canonical = JSON.stringify(filtered);

  const keyBytes  = hexToBytes(keys[0].public_key);
  const sigBytes  = hexToBytes(receipt.signature);
  const msgBytes  = new TextEncoder().encode(canonical);
  const cryptoKey = await crypto.subtle.importKey(
    "raw", keyBytes, { name: "Ed25519" }, false, ["verify"]
  );
  return crypto.subtle.verify({ name: "Ed25519" }, cryptoKey, sigBytes, msgBytes);
}

function hexToBytes(hex) {
  return new Uint8Array(hex.match(/.{2}/g).map(b => parseInt(b, 16)));
}

Or use the browser-based verifier — paste any receipt and verify the Ed25519 signature instantly with zero server calls. An independent client-side verifier on a separate trust boundary is also live at verify.headlessoracle.com.

Reference implementations: github.com/headlessoracle/demo-agent — ~100-line Node.js reference verifier (MIT). Architectural context: github.com/headlessoracle/essays — including The Trust Primitive: Why Agent Commerce Needs Environment-State Attestation (v1.6.4, April 2026, CC BY 4.0).

Fail-Closed Architecture

The oracle uses a three-tier safety cascade. If anything fails at any tier, the response always defaults to UNKNOWN — never a false OPEN.

TIER 0Manual override active — returns HALTED or CLOSED with signed reason.
TIER 1Schedule-based calculation — checks holidays, weekends, trading hours, half-days, lunch breaks.
TIER 2Fail-closed safety net — any Tier 1 error returns a signed UNKNOWN receipt.
TIER 3Catastrophic — signing system offline. Returns unsigned CRITICAL_FAILURE with HTTP 500.

Integrator Rule (Binding)

Execute only if status === 'OPEN' AND the Ed25519 signature is valid. Any other result — CLOSED, HALTED, UNKNOWN, timeout, network error, or invalid signature — must halt execution. This is a contractual obligation under the Terms of Service.

MCP Integration

Headless Oracle exposes an MCP (Model Context Protocol) server at POST /mcp. Any agent framework that supports MCP tool calling — Claude Desktop, Cursor, or custom agents built on the MCP spec — can call Oracle directly without writing HTTP client code.

POST /mcp Public · JSON-RPC 2.0

Protocol version: 2024-11-05 (MCP Streamable HTTP transport). No authentication required for MCP tool calls.

Available MCP Tools

get_market_status

Check whether a stock exchange is currently open or closed. Returns a cryptographically signed receipt. MANDATORY: treat UNKNOWN or HALTED as CLOSED and halt execution.

Input: { "mic": "XNYS" } — any of the 28 supported MIC codes.

get_market_schedule

Get next open and close times for an exchange. Includes lunch break windows for XJPX and XHKG. Not cryptographically signed — does not reflect real-time halts.

Input: { "mic": "XJPX" }

list_exchanges

Returns all 28 supported exchanges with MIC codes, names, and timezones.

No input required.

Setup: Claude Desktop ~30 seconds

Add this to your Claude Desktop config file, then restart Claude Desktop:

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

Windows: %APPDATA%\Claude\claude_desktop_config.json

{
  "mcpServers": {
    "headless-oracle": {
      "url": "https://headlessoracle.com/mcp"
    }
  }
}

After restarting Claude Desktop, Headless Oracle tools will appear automatically. No API key required for the demo tools.

Example Prompts

Try these after setup:

  • › "Is the NYSE open right now?"
  • › "Check if any Asian markets are currently in their lunch break"
  • › "What time does the London Stock Exchange close today?"
  • › "Are there any market holidays this week across all exchanges?"
  • › "Before I execute this trade — verify XNYS is open and give me the signed receipt"

Key Provenance

The Ed25519 public key for verifying receipts returned by MCP tools is published at /.well-known/oracle-keys.json (RFC 8615 standard).

API Keys & Billing

Production access to /v5/status, /v5/batch, and /v5/account requires an API key passed in the X-Oracle-Key header. All other endpoints are public.

Get an API Key

Subscribe via anonymous Paddle checkout — no account creation required. After payment your key appears on the pricing page.

curl -X POST https://headlessoracle.com/v5/checkout
# Returns: { "url": "https://checkout.paddle.com/..." }
# Redirect the user (or open the URL) to complete payment.

Keys are prefixed ho_live_ for easy identification in logs and environment variables.

Check Your Account

curl https://headlessoracle.com/v5/account \
     -H "X-Oracle-Key: YOUR_API_KEY"
# Returns: { "plan": "pro", "status": "active", "key_prefix": "ho_live_..." }

Error Codes

  • 401 API_KEY_REQUIRED: No key in header.
  • 403 INVALID_API_KEY: Key not found. Check for typos or use /v5/account to confirm it's active.
  • 402 PAYMENT_REQUIRED: Subscription is suspended or cancelled. Update billing to restore access.

Open Standards

Headless Oracle publishes the protocols it implements as Apache 2.0 open standards. Any operator can implement these independently — the value is in the interoperability, not the lock-in.

SMA Protocol

Signed Market Attestation

GitHub →

Defines the canonical JSON schema for a cryptographically signed market-state receipt: fields, Ed25519 signing algorithm, canonical payload serialisation (alphabetical key sort), 60-second TTL, and fail-closed UNKNOWN semantics.

/v5/conformance-vectors →

MPAS-1.0

Multi-Party Attestation Aggregation

GitHub →

Defines how N independent oracle operators' SMA receipts are aggregated into a single AggregatedAttestation with quorum consensus. A 2-of-3 quorum means no single compromised operator can produce a forged result. Includes consumer verification algorithm, operator registration conventions, and on-chain verification sketch.

IMPLEMENTATIONS.md →

APTS-1.0

Agent Pre-Trade Safety Standard

GitHub →

A vendor-neutral 6-step checklist every autonomous trading agent must complete before executing: fetch signed attestation → verify circuit breakers → verify settlement window → verify TTL → verify Ed25519 signature → halt on any failure. Includes CI badge, CHECKLIST.yaml, and conformance table.

Circuit Breaker Overrides

The OVERRIDE source tier allows manual status injection — used to broadcast HALTED during exchange circuit breakers, emergency closures, or scheduled maintenance windows.

When an override is active, the receipt includes a reason field explaining the override:

{
  "receipt_id":     "uuid-v4",
  "issued_at":      "2026-03-09T19:30:00.000Z",
  "expires_at":     "2026-03-09T19:31:00.000Z",
  "mic":            "XNYS",
  "status":         "HALTED",
  "source":         "OVERRIDE",
  "reason":         "NYSE circuit breaker Level 1 triggered",
  "schema_version": "v5.0",
  "public_key_id":  "key_2026_v1",
  "signature":      "hex_string"
}

Override receipts are signed with the same Ed25519 key as schedule-based receipts. Verification logic is identical. Your bot does not need to handle overrides differently — treat HALTED identically to CLOSED.