# Billing (/docs/billing)

Tiers, allowance, overage, and usage alerts.



Billing has two parts: a flat monthly fee that includes a token allowance,
and per-token overage when you exceed it. Manage everything in
[dashboard/billing](/dashboard/billing).

## Tiers [#tiers]

| Tier       | Notes                                                         |
| ---------- | ------------------------------------------------------------- |
| Tier 1     | Flat monthly + included allowance. Self-serve.                |
| Tier 2     | Higher flat monthly + higher allowance + contractual privacy. |
| Enterprise | Custom; data is contractually excluded from training.         |

## Overage [#overage]

Once your included allowance is consumed, additional tokens bill at the
metered price for your tier. There is no implicit pause — usage continues
to accrue. Pay-as-you-go billing happens in the next invoice cycle.

## Usage alerts [#usage-alerts]

Org admins set a percentage threshold in
[dashboard/limits](/dashboard/limits). When usage crosses the threshold,
the org admin gets an email. The alert fires once per billing period.

Refunds are credited net of usage on the invoice they applied to.

---

# Quick Start (/docs)

Get started with Orakle via the SDK, an MCP client, or the REST API.



You can use Orakle through the [dashboard](/dashboard) for ad-hoc pricing, or
programmatically over the API. The **SDK** is the fastest path for application
code, **MCP** drops pricing into any AI client, and the **REST API** is the
raw surface for everything else.

## 1. Get an API key [#1-get-an-api-key]

Create a key in your [dashboard](/dashboard). The dashboard is also where you
see past runs, track usage, and manage billing. Keys are shown once — store it
like a password.

## 2. SDK quick start [#2-sdk-quick-start]

Install the TypeScript SDK:

<Tabs items="[&#x22;pnpm&#x22;, &#x22;npm&#x22;]">
  <Tab value="pnpm">
    ```bash
    pnpm add orakle
    ```
  </Tab>

  <Tab value="npm">
    ```bash
    npm install orakle
    ```
  </Tab>
</Tabs>

One call submits the job, streams it, and resolves the final result. Use a
natural-language `query`, or pass `request_type: "structured"` with typed
product fields for the most accurate output (see the
[TypeScript SDK](/docs/sdks/typescript) for the structured shape):

```ts
import { OrakleClient } from "orakle";

const orakle = new OrakleClient({ apiKey: process.env.ORAKLE_API_KEY! });

const result = await orakle.priceCheck({
  request_type: "nl",
  query: "iPhone 15 Pro 128GB Grade B unlocked",
  geography: "US",
});

console.log(result.fused_distribution.quantiles);
// { "0.1": 780, "0.5": 870, "0.9": 960 }
```

See [TypeScript](/docs/sdks/typescript) for the TypeScript client.

## 3. MCP quick start [#3-mcp-quick-start]

Connect any MCP-capable AI app and let the model price products in chat. The
server URL is:

```text
https://api.orakle.xyz/v1/mcp
```

In an AI web app, add it as a custom connector under **Settings →
Connectors**. On first use your browser opens to authorize. Then ask the
model to price something — it calls the `get_pricing` tool directly. Full
setup in [MCP](/docs/mcp).

## What you get [#what-you-get]

Every completed pricing job returns:

* **`product_profile`** — the normalized product Orakle priced.
* **`fused_distribution`** — the calibrated price distribution: `quantiles`
  (e.g. `"0.1"`, `"0.5"`, `"0.9"`) and the market `currency`. Use the median
  for a point estimate, the spread for confidence.
* **`scenario_forecast`** — a scenario-weighted forward view (may be `null`).

<Callout title="The spread is the signal">
  A wide distribution means the market is genuinely uncertain about this item —
  that is information, not noise. Don't collapse it to a single number too early.
</Callout>

## Next [#next]

<Cards>
  <Card title="TypeScript SDK" href="/docs/sdks/typescript" description="Typed client that wraps submit, stream, and result into one call." />

  <Card title="MCP Server" href="/docs/mcp" description="Connect any MCP-capable AI app and price products in chat." />

  <Card title="REST API" href="/docs/api" description="The raw surface — submit a job, stream progress over SSE, read the result." />
</Cards>

## Built for LLMs too [#built-for-llms-too]

Every page is available as raw Markdown — append `.md` to any docs URL, or use
[`/llms.txt`](/llms.txt) and [`/llms-full.txt`](/llms-full.txt) to feed the
entire documentation set to an agent.

---

# Authentication (/docs/api/authentication)

Pass a key as a Bearer token (MCP) or X-Orakle-API-Key header (REST).



Two kinds of keys: **personal** (`pk_user_…`, you mint in
[dashboard/api-keys](/dashboard/api-keys)) and **organization** (`sk_…`,
org admins mint). Both authenticate as the same user/org identity for
billing and read access.

## REST [#rest]

```
X-Orakle-API-Key: <your-key>
```

```bash
curl -s https://api.orakle.xyz/v1/jobs \
  -H "X-Orakle-API-Key: $ORAKLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"request_type":"nl","query":"iPhone 15 Pro 128GB"}'
```

## MCP [#mcp]

The MCP server (`/v1/mcp`) takes either an OAuth bearer token (handshake
runs in your MCP client; nothing to configure here) or your API key as
a Bearer token:

```
Authorization: Bearer pk_user_...
```

## Failure [#failure]

Missing or invalid credentials → `401 auth_required`. Reads of jobs you
don't own → `404 job_not_found` (leak-safe). See [Errors](/docs/api/errors).

---

# Errors (/docs/api/errors)

Error codes, HTTP statuses, the error response shape, and rate limiting.



## Response shape [#response-shape]

Every error — REST or `failed` [stream event](/docs/api/events) — uses one
public code. REST errors return:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "human-readable summary",
    "status": 422
  }
}
```

A `failed` SSE frame carries the same `code` (no `status`).

## Codes [#codes]

| Code                  | HTTP | Cause    | What to do                                                                         |
| --------------------- | ---- | -------- | ---------------------------------------------------------------------------------- |
| `invalid_request`     | 422  | You      | Bad body — wrong shape, missing `query`, bad `geography`, non-HTTPS `webhook_url`. |
| `auth_required`       | 401  | You      | Send valid `X-Orakle-API-Key`.                                                     |
| `job_not_found`       | 404  | You      | Wrong or unowned job id.                                                           |
| `rate_limited`        | 429  | You      | Honor `Retry-After`.                                                               |
| `insufficient_data`   | 422  | External | Not enough market data. Retry or refine.                                           |
| `service_unavailable` | 503  | Us       | Dispatch failure. Retry with backoff.                                              |
| `internal_error`      | 500  | Us       | Retry; contact support if persistent.                                              |

**You** — fix request. **Us** — transient. **External** — upstream limit.

## Rate limiting [#rate-limiting]

Capped per minute and per hour, by API key. Health and webhook endpoints
exempt. Exceeding returns:

```
HTTP/1.1 429 Too Many Requests
Retry-After: 30

{ "error": { "code": "rate_limited", "message": "Rate limit exceeded. Max N requests per minute.", "status": 429 } }
```

Wait at least `Retry-After` seconds before retrying. SDKs back off
automatically.

---

# Events (/docs/api/events)

Every customer SSE event type, its payload, and terminal semantics.



The [stream](/docs/api/streaming) emits four event types with a common
envelope.

| `event_type` | Meaning                 | Terminal |
| ------------ | ----------------------- | -------- |
| `stage`      | Progress advanced.      | No       |
| `partial`    | Incremental result.     | No       |
| `completed`  | Job done; final result. | Yes      |
| `failed`     | Job failed; error code. | Yes      |

Common fields:

<TypeTable
  type="{
  event_type: { description: &#x22;stage | partial | completed | failed.&#x22;, type: &#x22;string&#x22; },
  job_id: { description: &#x22;The job id.&#x22;, type: &#x22;string&#x22; },
  stage: { description: &#x22;Opaque progress label. Do not branch on its value.&#x22;, type: &#x22;string | null&#x22; },
  message: { description: &#x22;Human-readable progress.&#x22;, type: &#x22;string&#x22; },
  timestamp: { description: &#x22;ISO 8601 timestamp.&#x22;, type: &#x22;string&#x22; },
}"
/>

## stage [#stage]

Drive a progress indicator from `message`; treat `stage` as opaque.

```
event: stage
id: 2
data: {"event_type":"stage","job_id":"b7c1...","stage":"…","message":"Pricing in progress…","timestamp":"2026-05-15T12:00:02Z"}
```

## partial [#partial]

`result` is a subset of the final `JobResult`.

```
event: partial
id: 5
data: {"event_type":"partial","job_id":"b7c1...","stage":"…","message":"...","timestamp":"2026-05-15T12:00:05Z","result":{...}}
```

## completed [#completed]

Terminal. `result` carries the full `JobResult` (`product_profile`,
`fused_distribution`, `scenario_forecast`, …).

```
event: completed
id: 9
data: {"event_type":"completed","job_id":"b7c1...","stage":"…","message":"...","timestamp":"2026-05-15T12:00:42Z","result":{"product_profile":{...},"fused_distribution":{"quantiles":{"0.5":812.0},"currency":"USD"},"scenario_forecast":null}}
```

## failed [#failed]

Terminal. `code` is a public [error code](/docs/api/errors).

```
event: failed
id: 4
data: {"event_type":"failed","job_id":"b7c1...","stage":null,"message":"Not enough comparable listings to price this item.","timestamp":"2026-05-15T12:00:11Z","code":"insufficient_data"}
```

<Callout type="info" title="Terminal closes the stream">
  After `completed` or `failed`, the server closes the connection. Do not
  reconnect.
</Callout>

---

# API Reference (/docs/api)

The Orakle HTTP API — submit a pricing job, stream progress over SSE, read the calibrated result.



Base URL: `https://api.orakle.xyz`. Authenticate every request with the
`X-Orakle-API-Key` header. All responses are JSON unless you open a stream.

Pricing is asynchronous. You `POST` a job and immediately get back a `job_id`
plus a `poll_url` and `stream_url`. The job moves through
`pending → processing → completed | failed`; you either stream Server-Sent
Events for live progress and the final result, or poll the status endpoint
until it is terminal.

<Cards>
  <Card title="Authentication" href="/docs/api/authentication" description="The X-Orakle-API-Key header and 401 behavior." />

  <Card title="Jobs" href="/docs/api/jobs" description="Submit a pricing job and read its status and result." />

  <Card title="Streaming" href="/docs/api/streaming" description="Open the SSE stream, replay with Last-Event-Id, keepalive." />

  <Card title="Events" href="/docs/api/events" description="Every customer event type and its payload shape." />

  <Card title="Errors" href="/docs/api/errors" description="Error codes, HTTP statuses, and rate limiting." />

  <Card title="Quick reference" href="/docs/api/reference" description="One-screen table of every public endpoint." />
</Cards>

## End to end [#end-to-end]

```bash
# 1. Submit a job → 202 with job_id, poll_url, stream_url
curl -s https://api.orakle.xyz/v1/jobs \
  -H "X-Orakle-API-Key: $ORAKLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"request_type":"nl","query":"iPhone 15 Pro 128GB Grade B unlocked","geography":"US"}'

# 2. Stream progress and the final result (job_id from step 1)
curl -N https://api.orakle.xyz/v1/jobs/<job_id>/stream \
  -H "X-Orakle-API-Key: $ORAKLE_API_KEY"

# 3. Or poll until status is completed | failed
curl -s https://api.orakle.xyz/v1/jobs/<job_id> \
  -H "X-Orakle-API-Key: $ORAKLE_API_KEY"
```

<Callout title="Prefer an SDK">
  The [TypeScript SDK](/docs/sdks/typescript) wraps submit → stream → result
  into one call. Use the raw API for other stacks or full wire-level control.
  Working inside an AI client? See the [MCP server](/docs/mcp).
</Callout>

---

# Jobs (/docs/api/jobs)

Submit a pricing job and read your history.



Jobs are scoped per user. Every read enforces ownership — your key only
sees jobs you submitted.

## Submit [#submit]

`POST /v1/jobs` returns immediately with `job_id`, a `poll_url`, and a
`stream_url`.

```bash
curl -s https://api.orakle.xyz/v1/jobs \
  -H "X-Orakle-API-Key: $ORAKLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"request_type":"nl","query":"iPhone 15 Pro 128GB"}'
```

Poll `GET /v1/jobs/{id}` until `status` is `completed` or `failed`, or
stream events from `GET /v1/jobs/{id}/stream` (Server-Sent Events).

## List [#list]

`GET /v1/jobs` returns your jobs newest-first with cursor pagination.

| Param       | Notes                                                |
| ----------- | ---------------------------------------------------- |
| `q`         | Case-insensitive substring on the query field.       |
| `date_from` | ISO timestamp; inclusive lower bound.                |
| `date_to`   | ISO timestamp; inclusive upper bound.                |
| `limit`     | Default 50, max 200.                                 |
| `cursor`    | Opaque token from `next_cursor`. Omit on first page. |

Responses include `items[]` and `next_cursor` (`null` on the last page).

---

# Quick reference (/docs/api/reference)

Every public Orakle endpoint on one screen.



Base URL: `https://api.orakle.xyz`. Auth header: `X-Orakle-API-Key`.

## Endpoints [#endpoints]

| Method | Path                                    | Auth | Purpose                                                                      |
| ------ | --------------------------------------- | ---- | ---------------------------------------------------------------------------- |
| `POST` | `/v1/jobs`                              | Yes  | Submit a pricing job. Returns `202` with `job_id`, `poll_url`, `stream_url`. |
| `GET`  | `/v1/jobs/{id}`                         | Yes  | Read job status and result. `404` if not found or not yours.                 |
| `GET`  | `/v1/jobs/{id}/stream`                  | Yes  | SSE stream of progress and the final result.                                 |
| `GET`  | `/v1/health`                            | No   | Service health: `{ "status": "healthy", "version": "..." }`.                 |
| `GET`  | `/.well-known/oauth-protected-resource` | No   | RFC 9728 metadata for MCP OAuth.                                             |

There is no public OpenAPI document. Details: [Jobs](/docs/api/jobs),
[Streaming](/docs/api/streaming), [Events](/docs/api/events),
[Errors](/docs/api/errors).

---

# Streaming (/docs/api/streaming)

Stream live job progress and the final result over Server-Sent Events.



## `GET /v1/jobs/{id}/stream` [#get-v1jobsidstream]

Opens a `text/event-stream` SSE connection for one job. The stream replays
prior events, then tails live until terminal (`completed` or `failed`), then
closes.

```bash
curl -N https://api.orakle.xyz/v1/jobs/<job_id>/stream \
  -H "X-Orakle-API-Key: $ORAKLE_API_KEY"
```

`-N` disables curl buffering. Unknown or unowned jobs return `404
job_not_found`.

## Wire format [#wire-format]

Standard SSE frames: `event:`, `id:` (monotonic integer), `data:` JSON, blank
line.

```
event: stage
id: 3
data: {"event_type":"stage","job_id":"b7c1...","stage":"planning","message":"Planning strategy...","timestamp":"2026-05-15T12:00:03Z"}

```

See [Events](/docs/api/events) for every event type and payload.

## Replay with Last-Event-Id [#replay-with-last-event-id]

Send `Last-Event-Id` with the last `id` you processed. The server replays
only higher sequences then continues live — so a dropped connection resumes
without gaps or duplicates.

```bash
curl -N https://api.orakle.xyz/v1/jobs/<job_id>/stream \
  -H "X-Orakle-API-Key: $ORAKLE_API_KEY" \
  -H "Last-Event-Id: 3"
```

## Keepalive [#keepalive]

When idle, the server emits SSE comments to hold the connection open. Skip
lines starting with `:`.

```
: keepalive

```

## Consuming in Node [#consuming-in-node]

```ts
const res = await fetch(
  `https://api.orakle.xyz/v1/jobs/${jobId}/stream`,
  { headers: { "X-Orakle-API-Key": process.env.ORAKLE_API_KEY! } },
);

const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buf = "";

for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  buf += decoder.decode(value, { stream: true });

  let i: number;
  while ((i = buf.indexOf("\n\n")) !== -1) {
    const frame = buf.slice(0, i);
    buf = buf.slice(i + 2);
    if (frame.startsWith(":")) continue; // keepalive

    const lines = frame.split("\n");
    const event = lines.find((l) => l.startsWith("event: "))?.slice(7);
    const data = lines.find((l) => l.startsWith("data: "))?.slice(6);
    if (data) handle(event, JSON.parse(data));
  }
}
```

<Callout title="Prefer the SDK">
  The [SDKs](/docs/sdks/typescript) handle streaming, reconnection, and
  `Last-Event-Id` replay, resolving a single typed result. Hand-roll SSE only
  for raw progress events or non-JS/Python clients.
</Callout>

---

# Connect (/docs/mcp/connect)

Add the Orakle MCP server to any MCP-capable AI app or agent.



Interactive clients handle the OAuth handshake on first use. For headless
agents, use an API key from [dashboard/api-keys](/dashboard/api-keys) as a
Bearer token.

The server URL is:

```text
https://api.orakle.xyz/v1/mcp
```

## AI web apps (custom connectors) [#ai-web-apps-custom-connectors]

Most AI web apps connect to remote MCP servers through a **custom connector**
configured in their settings. The flow is the same across apps:

1. Open the app's **Settings → Connectors** (or equivalent).
2. Choose **Add custom connector**.
3. Enter a name (e.g. `Orakle`) and the server URL above.
4. Save. On the first call, the app opens a browser window for OAuth
   sign-in and stores the resulting token.

No config file or API key is needed for this path — the app drives OAuth
for you.

## Config-based clients [#config-based-clients]

Clients that read a JSON config file should add Orakle under `mcpServers`
with the HTTP transport. Use an API key from
[dashboard/api-keys](/dashboard/api-keys) for headless use:

```json
{
  "mcpServers": {
    "orakle": {
      "url": "https://api.orakle.xyz/v1/mcp",
      "headers": { "Authorization": "Bearer pk_user_..." }
    }
  }
}
```

## Raw HTTP [#raw-http]

To talk to the server directly — for scripts, testing, or custom agents —
POST JSON-RPC over the [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports):

```bash
curl -s https://api.orakle.xyz/v1/mcp \
  -H "Authorization: Bearer pk_user_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

## Verify [#verify]

`tools/list` should return `get_pricing`, `submit_pricing_job`, and
`get_job_result`. The [dashboard MCP page](/dashboard/mcp) shows the
timestamp of your most recent call so you can confirm the connection is
live.

---

# MCP Server (/docs/mcp)

Orakle's pricing tools as a Model Context Protocol server for AI clients and agents.



The Orakle MCP server exposes pricing intelligence as Model Context Protocol
tools, so any MCP-capable AI client or headless agent can submit and read
pricing jobs directly.

Endpoint:

```
https://api.orakle.xyz/v1/mcp
```

It speaks the [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports)
(MCP spec `2025-11-25`) and is stateless — no session is kept between
requests. The endpoint accepts `POST`, `GET`, and `DELETE`.

## Auth modes [#auth-modes]

* **OAuth 2.1 + PKCE** — for interactive clients. The client discovers the
  authorization server, opens a browser for sign-in, and obtains a bearer
  token automatically. No manual token handling.
* **API key** — for headless agents. Send your key as
  `Authorization: Bearer sk_...`. Create keys in your
  [dashboard](/dashboard).

<Cards>
  <Card title="Connect" href="/docs/mcp/connect" description="Add the server to any MCP-capable AI app or agent." />

  <Card title="Tools" href="/docs/mcp/tools" description="get_pricing, submit_pricing_job, get_job_result — inputs, outputs, examples." />
</Cards>

## When to use MCP [#when-to-use-mcp]

MCP wraps the same job system as the [REST API](/docs/api) and
[SDKs](/docs/sdks/typescript); read access is strictly per-user. Pick by where
you work:

* **Inside an AI client or agent** → MCP. The model calls the tools directly.
* **Application code (TypeScript/Python)** → [SDK](/docs/sdks/typescript). One
  call wraps submit → stream → result.
* **Any other stack or wire-level control** → [REST API](/docs/api).

---

# Tools (/docs/mcp/tools)

The three Orakle MCP tools — get_pricing, submit_pricing_job, get_job_result.



Three tools wrap the [REST API](/docs/api) job system; read access is
per-user. `geography` accepts `US`, `GB`, `CA`, `AU`, `AE`; default `US`.

<Callout type="info" title="Sync vs async">
  `get_pricing` blocks. `submit_pricing_job` + `get_job_result` are the async
  pair. Failures return as `isError: true` tool results, not JSON-RPC errors.
</Callout>

## get\_pricing [#get_pricing]

Submits a job and waits. Not read-only; open-world.

**Input**

<TypeTable
  type="{
  query: {
    type: &#x22;string&#x22;,
    description: &#x22;e.g. 'iPhone 15 Pro 128GB Grade B unlocked US'.&#x22;,
    required: true,
  },
  geography: {
    type: &#x22;string&#x22;,
    description: &#x22;ISO 3166-1 alpha-2.&#x22;,
    default: &#x22;US&#x22;,
  },
}"
/>

**Output** — pricing envelope:

```ts
{
  status: string;
  job_id?: string;
  result?: Record<string, unknown>;
  error?: string;
}
```

**Example**

```json
// call
{ "name": "get_pricing", "arguments": {
  "query": "iPhone 15 Pro 128GB Grade B unlocked", "geography": "US" } }

// result (structuredContent)
{
  "status": "completed",
  "job_id": "job_01Hh...",
  "result": { "fused_distribution": { "quantiles": { "p50": 612.0 } } }
}
```

## submit\_pricing\_job [#submit_pricing_job]

Async submit. Returns a `job_id` to poll via `get_job_result`. Not read-only;
open-world.

**Input**

<TypeTable
  type="{
  query: {
    type: &#x22;string&#x22;,
    description: &#x22;Natural-language pricing query.&#x22;,
    required: true,
  },
  geography: {
    type: &#x22;string&#x22;,
    description: &#x22;ISO 3166-1 alpha-2.&#x22;,
    default: &#x22;US&#x22;,
  },
}"
/>

**Output**

```ts
{ job_id: string }
```

**Example**

```json
// call
{ "name": "submit_pricing_job", "arguments": {
  "query": "Galaxy S24 256GB Grade A unlocked", "geography": "GB" } }

// result (structuredContent)
{ "job_id": "job_01HJ..." }
```

## get\_job\_result [#get_job_result]

Status and result of a submitted job. Read-only.

**Input**

<TypeTable
  type="{
  job_id: {
    type: &#x22;string&#x22;,
    description: &#x22;From submit_pricing_job.&#x22;,
    required: true,
  },
}"
/>

**Output** — same envelope as `get_pricing`:

```ts
{
  status: string;
  job_id?: string;
  result?: Record<string, unknown>;
  error?: string;
}
```

**Example**

```json
// call
{ "name": "get_job_result", "arguments": { "job_id": "job_01HJ..." } }

// result while running (structuredContent)
{ "status": "processing", "job_id": "job_01HJ..." }

// result once terminal
{
  "status": "completed",
  "job_id": "job_01HJ...",
  "result": { "fused_distribution": { "quantiles": { "p50": 498.0 } } }
}
```

<Callout type="warn" title="Errors are tool results">
  Failures return `isError: true`, not JSON-RPC errors. Check `isError` (or a
  non-`completed` `status` with an `error` field) before trusting `result`.
</Callout>

---

# Authentication (/docs/sdks)

How Orakle SDKs authenticate against the API.



## API key [#api-key]

`apiKey` is the key you create in your [dashboard](/dashboard). It is sent as
the `X-Orakle-API-Key` header on every request. Treat it like a password.

---

# TypeScript SDK (/docs/sdks/typescript)

The orakle npm package — typed pricing jobs over SSE, with streaming callbacks and AbortSignal support.



Published as `orakle` on npm. Ships ESM and CJS. Requires Node.js 22+ (uses
the global `fetch` and `AbortController`) and works in modern browsers.

## Install [#install]

<Tabs items="[&#x22;pnpm&#x22;, &#x22;npm&#x22;]">
  <Tab value="pnpm">
    ```bash
    pnpm add orakle
    ```
  </Tab>

  <Tab value="npm">
    ```bash
    npm install orakle
    ```
  </Tab>
</Tabs>

## Instantiate [#instantiate]

```ts
import { OrakleClient } from "orakle";

const orakle = new OrakleClient({ apiKey: process.env.ORAKLE_API_KEY! });
```

<TypeTable
  type="{
  apiKey: {
    description: &#x22;Your key from the dashboard. Sent as X-Orakle-API-Key.&#x22;,
    type: &#x22;string&#x22;,
  },
  baseUrl: {
    description: &#x22;API base URL. Must be https://.&#x22;,
    type: &#x22;string&#x22;,
    default: '&#x22;https://api.orakle.xyz&#x22;',
  },
  timeoutSec: {
    description: &#x22;Per-job timeout in seconds. Minimum 240.&#x22;,
    type: &#x22;number&#x22;,
    default: &#x22;240&#x22;,
  },
  maxLineBytes: {
    description: &#x22;Maximum bytes per SSE line accepted by the parser.&#x22;,
    type: &#x22;number&#x22;,
    default: &#x22;5 MiB&#x22;,
  },
  maxEventBytes: {
    description: &#x22;Byte cap per SSE event (summed across multi-line data fields).&#x22;,
    type: &#x22;number&#x22;,
    default: &#x22;10 MiB&#x22;,
  },
}"
/>

## priceCheck [#pricecheck]

Submits a job, opens the SSE stream, and resolves with the final result.
Rejects with an [`OrakleError`](#errors) of `kind: "job_failed"` on a
`failed` event.

<CodeBlockTabs defaultValue="Natural language">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="Natural language">
      Natural language
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Structured">
      Structured
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="Natural language">
    ```ts
    const result = await orakle.priceCheck({
      request_type: "nl",
      query: "iPhone 15 Pro 256GB unlocked mint",
      geography: "US",
    });

    console.log(result.fused_distribution.quantiles);
    // { "0.05": 740, "0.1": 780, "0.25": 820, "0.5": 870, "0.75": 920, "0.9": 960, "0.95": 1010 }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Structured">
    ```ts
    const result = await orakle.priceCheck({
      request_type: "structured",
      product: {
        category: "smartphone",
        make: "Apple",
        model: "iPhone 15 Pro",
        condition: "mint",
        attributes: { storage: "256GB", carrier_lock: "unlocked" },
      },
      geography: "US",
    });
    ```
  </CodeBlockTab>
</CodeBlockTabs>

`geography` is one of `"US" | "GB" | "CA" | "AU" | "AE"` and is optional.
`webhook_url` is also accepted on both request shapes.

## priceCheckStream [#pricecheckstream]

Yields every [`JobEvent`](/docs/api/events) as it arrives — `stage`, `partial`,
then a terminal `completed` or `failed` — and ends after the terminal event.

```ts
for await (const event of orakle.priceCheckStream({
  request_type: "nl",
  query: "PS5 Disc Edition",
})) {
  if (event.event_type === "stage") {
    console.log(event.stage, event.message);
  }
  if (event.event_type === "completed") {
    console.log(event.result.fused_distribution.quantiles);
  }
  if (event.event_type === "failed") {
    console.error(event.code, event.message);
  }
}
```

`priceCheckStream` does not throw on a `failed` event — it yields it and
returns. It still throws on transport, timeout, and HTTP errors.

## Options [#options]

Both methods take an optional second argument:

<TypeTable
  type="{
  signal: {
    description: &#x22;AbortSignal to cancel the job. Aborting rejects with the signal's reason.&#x22;,
    type: &#x22;AbortSignal&#x22;,
  },
  timeoutSec: {
    description: &#x22;Override the client timeout for this call, in seconds. Minimum 240.&#x22;,
    type: &#x22;number&#x22;,
  },
  onProgress: {
    description: &#x22;Called for each stage event as the pipeline advances.&#x22;,
    type: &#x22;(event: StageEvent) => void&#x22;,
  },
  onPartial: {
    description: &#x22;Fires once with the analyzed product profile right after the Analyzing stage, before the full result.&#x22;,
    type: &#x22;(event: PartialEvent) => void&#x22;,
  },
}"
/>

```ts
const result = await orakle.priceCheck(
  { request_type: "nl", query: "MacBook Air M3 16GB" },
  {
    timeoutSec: 300,
    onProgress: (e) => console.log(`[${e.stage}] ${e.message}`),
    onPartial: (e) => renderPlaceholder(e.result.product_profile),
  },
);
```

## Cancellation [#cancellation]

Pass an `AbortSignal` to cancel an in-flight job. The promise rejects with the
signal's abort reason — a plain `DOMException`, distinct from the SDK's own
`OrakleError` of `kind: "timeout"`.

```ts
const controller = new AbortController();
setTimeout(() => controller.abort(), 10_000);

try {
  await orakle.priceCheck(
    { request_type: "nl", query: "..." },
    { signal: controller.signal },
  );
} catch (err) {
  if (err instanceof DOMException && err.name === "AbortError") {
    console.log("Cancelled by caller");
  }
}
```

## Errors [#errors]

Every SDK failure is a single `OrakleError` class carrying a `kind` literal
and optional per-kind fields. Branch with the exported predicates — they
narrow `status`, `code`, `timeoutSec`, and `cause` without casts.

| `kind`       | Predicate                | When                                                           | Extra fields     |
| ------------ | ------------------------ | -------------------------------------------------------------- | ---------------- |
| `http`       | `isHttpError(err)`       | Non-2xx HTTP response — the job never started.                 | `status`, `code` |
| `job_failed` | `isJobFailedError(err)`  | Job was accepted but failed in the pipeline (`failed` SSE).    | `code`           |
| `timeout`    | `isTimeoutError(err)`    | The job exceeded the effective timeout.                        | `timeoutSec`     |
| `connection` | `isConnectionError(err)` | Network, DNS, or transport failure.                            | `cause`          |
| `config`     | `isConfigError(err)`     | Caller misconfigured the client (bad `baseUrl`, bad byte cap). | —                |
| `protocol`   | `isProtocolError(err)`   | Server/transport violated the SSE contract.                    | —                |

```ts
import {
  OrakleError,
  isHttpError,
  isJobFailedError,
  isTimeoutError,
  isConnectionError,
  isConfigError,
  isProtocolError,
} from "orakle";

try {
  await orakle.priceCheck({ request_type: "nl", query: "..." });
} catch (err) {
  if (!(err instanceof OrakleError)) throw err;
  // Predicates (not `switch (err.kind)`) so `status`, `code`, `timeoutSec`, and
  // `cause` narrow to non-optional inside each branch without `!` or `as`.
  if (isHttpError(err)) console.error(err.code, err.status);
  else if (isJobFailedError(err)) console.error(err.code);
  else if (isTimeoutError(err)) console.error(err.timeoutSec);
  else if (isConnectionError(err)) console.error(err.cause);
  else if (isConfigError(err) || isProtocolError(err)) console.error(err.message);
}
```

The `http` and `job_failed` kinds share the same `ErrorCode` taxonomy:
`invalid_request`, `auth_required`, `rate_limited`, `job_not_found`,
`insufficient_data`, `internal_error`, `service_unavailable`.

The raw API and MCP surfaces use the same public error-code taxonomy.

## Result shape [#result-shape]

`priceCheck` resolves with a `PriceCheckResult`:

<TypeTable
  type="{
  product_profile: {
    description: &#x22;The normalized product Orakle priced.&#x22;,
    type: &#x22;ProductProfile&#x22;,
  },
  fused_distribution: {
    description: &#x22;The calibrated price distribution: quantiles + the market currency.&#x22;,
    type: &#x22;PriceDistribution&#x22;,
  },
  scenario_forecast: {
    description: &#x22;Scenario-weighted forward view. May be null.&#x22;,
    type: &#x22;ScenarioForecast | null&#x22;,
  },
  forecast_series: {
    description: &#x22;Historical + forecast price points for charting.&#x22;,
    type: &#x22;ForecastPoint[]&#x22;,
  },
  forecast_narrative: {
    description: &#x22;Prose summary of the forward view.&#x22;,
    type: &#x22;string&#x22;,
  },
  events: {
    description: &#x22;Dated market catalysts relevant to the price.&#x22;,
    type: &#x22;PricingEvent[]&#x22;,
  },
  token_usage: {
    description: &#x22;Usage for the request. Drives metered billing; may be absent.&#x22;,
    type: &#x22;number&#x22;,
  },
}"
/>

The SSE primitive `createSSEStream` is also exported for server-side proxies
that re-emit a sanitized stream to the browser.