# oculr > AI-powered EVM transaction analysis API across 50+ EVM mainnets. Submit a transaction hash from any supported chain, receive a structured explanation: what happened, which protocol, what risks, and the USD value. Designed for agent consumption - JSON by default, async-first, MPP/x402 autonomous payment. oculr analyses transactions on Ethereum, Base, Arbitrum, Optimism, Polygon, BNB Chain, Avalanche, and 40+ more EVM mainnets - chain is auto-detected from the tx hash, no `chain` field in the request needed. The pipeline fetches on-chain data, decodes calldata and events, resolves addresses and entities, and produces a structured report. Payment is per-request via MPP/x402 - no API key required, settlement via Tempo MPP sessions in USDC.e. > **Reading this file?** This is a full-text snapshot of the oculr docs. For the latest details (full schemas, error-code response bodies, SSE event vocabulary, the typed sub-agent contract), fetch the canonical pages at , (autonomous-agent entry point), and (Anthropic + OpenAI tool-use schemas for sub-agents). --- ## Docs ### [Introduction](https://oculr.xyz/docs/) # oculr > Paste a transaction hash from any of 50+ EVM mainnets. Get back what happened, who did it, what's risky, and how much USD moved - in plain English, as structured JSON. ## What oculr does You give oculr a transaction hash. It fetches the trace, decodes calldata and events, resolves the addresses against on-chain labels, walks the call graph with an AI agent, and returns a typed `ExplanationResult`: - A one-line **summary** ("Uniswap V3 swap: 1,000 USDC → 0.42 WETH"). - A **txType** classification (`swap`, `exploit`, `mev`, `liquidation`, …) you can route on. - **Risks** flagged for review (known bad actors, unverified contracts, anomalous gas). - The **protocol** involved, **addresses** with labels, and the **USD value** of the primary action. - A **confidence** rating (`high` / `medium` / `low`) so your code knows when to trust the summary verbatim and when to escalate. oculr is hosted at **[oculr.xyz](https://oculr.xyz)** (web app and docs) with the API at **[mpp.oculr.xyz](https://mpp.oculr.xyz)**. No accounts, no API keys. ## How you pay oculr is a [Machine Payments Protocol](https://mpp.dev) (MPP) service. Payment uses **MPP sessions** - your client opens a payment channel with a `maxDeposit` against the API, signs cumulative vouchers per request, and the server redeems the highest voucher on-chain. Like a bar tab - many requests, one settlement. Settlement happens on [Tempo](https://tempo.xyz) in USDC.e. You hold a wallet with a USDC.e balance; the MPP client handles the 402 challenge transparently. [`mppx`](https://www.npmjs.com/package/mppx) is the preferred client - install it once, point it at the wallet, and every `fetch()` your code makes against `mpp.oculr.xyz` settles automatically. Any MPP-compatible client also works. ### What it costs per request Pricing is **metered**: you pay the actual upstream cost of your analysis × 1.20, plus a $0.02 fixed fee, settled in $0.01 voucher increments as the run progresses. Typical analyses on the default model land between **$0.45 and $0.90**; complex incident investigations cost more, simple transfers less. Every response carries a `costs` object with the actual cost broken into category buckets - `llms`, `dataCollection`, `codeExecution`, `other` - plus a `totalUsd`. See **[Pricing](/pricing)** for real production examples and the full model breakdown. ## Pick how you'll use oculr oculr ships the same JSON contract through three surfaces. Pick the one that matches how *you* work today: ### Agent mode Ask an agent CLI (Claude Code, Amp, Codex CLI, …) to analyse a transaction in one line: :::code-group ```bash [Claude Code] claude -p "Read https://mpp.oculr.xyz/SKILL.md and analyse EVM transaction 0xYOUR_TX_HASH - what happened, any risks?" ``` ```bash [Amp] amp --execute "Read https://mpp.oculr.xyz/SKILL.md and analyse EVM transaction 0xYOUR_TX_HASH - what happened, any risks?" ``` ```bash [Codex CLI] codex exec "Read https://mpp.oculr.xyz/SKILL.md and analyse EVM transaction 0xYOUR_TX_HASH - what happened, any risks?" ``` ::: For sub-agents inside a parent tool-use loop, point the parent at `https://mpp.oculr.xyz/tool-spec.json` - typed Anthropic + OpenAI schemas, no markdown parsing. See **[Use as an agent](/quickstart/agent)**. ### Manual mode Call the oculr MPP from your own code. Synchronous analysis is a metered SSE stream - `mppx`'s session manager opens the channel and signs vouchers as cost accrues: ```typescript import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as `0x${string}`) const session = tempo.session.manager({ account, maxDeposit: '15' }) const stream = await session.sse('https://mpp.oculr.xyz/explain', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' }, body: JSON.stringify({ txHash: '0x4e4b8ed4…' }), }) for await (const payload of stream) { const msg = JSON.parse(payload) if (msg.type === 'result') console.log(msg.summary, msg.risks) } ``` Prefer a plain `fetch()` with no stream? `POST /explain/async` + poll is fixed-quote per request. See **[Call the oculr MPP](/quickstart/client)** for both walkthroughs. ### Web app Paste your hash into **[oculr.xyz/app](https://oculr.xyz/app)** - no setup, streaming workflow, flow diagram, JSON view. Right for one-off triage, sharing a link with a teammate, or eyeballing an incident. See **[Use the web app](/guides/web-app)**. ## Who uses oculr - **Security engineers** - triage abnormal transactions in real time and get a structured read fast. - **Security researchers** - understand complex transactions in detail when writing up an incident. - **Trading desks** - decode complex DeFi transactions to understand counterparty intent. ## Endpoints at a glance | Method | Path | What it does | |---|---|---| | `POST` | `/explain` | Synchronous analysis over metered SSE - the final event is the result | | `POST` | `/explain/async` | Non-blocking - returns a `jobId` immediately | | `GET` | `/result/:jobId` | Poll an async job for its result | | `GET` | `/health` | Service health probe (free) | | `GET` | `/openapi.json` | Full OpenAPI 3.1 spec (free) | | `GET` | `/tool-spec.json` | Typed Anthropic + OpenAI tool-use schemas (free) | | `GET` | `/SKILL.md` | Prose entry point for agents (free) | | `GET` | `/llms.txt` | Discovery index for LLM crawlers (free) | Full request/response schemas at **[Endpoints reference](/reference/endpoints)**. ## Next - **[Use as an agent](/quickstart/agent)** - skill mode, sub-agent tool-use, raw API tutorial. - **[Call the oculr MPP](/quickstart/client)** - `mppx` setup and your first request. - **[Analyze a transaction](/guides/analyzing-transactions)** - hash to insight, with worked examples. - **[Core concepts](/concepts/)** - pipeline, confidence levels, sync vs async. - **[FAQ](/faq)** - pricing, chains, supported transaction types, troubleshooting. --- ### [Pricing](https://oculr.xyz/docs/pricing) # Pricing > oculr is metered: you pay what the analysis actually costs, plus a fixed margin. No subscriptions, no tiers, no API keys - every request settles over [MPP](https://mpp.dev) in USDC.e on [Tempo](https://tempo.xyz). ## The formula Every request is priced from the real upstream cost of *your* transaction's analysis: ``` charge = upstream cost × 1.20 + $0.02 ``` - **Upstream cost** - what oculr pays to analyse your transaction: LLM tokens, trace fetches, address labels, token prices, web research, code execution. Simple transfers touch a handful of services; a deep exploit investigation touches many more. - **× 1.20** - a flat 20% margin. - **+ $0.02** - a fixed per-request fee. There is no minimum beyond the fixed fee and no rounding games - charges settle in $0.01 voucher increments as the analysis runs (see [How metering works](#how-metering-works)). The formula applies to both endpoints, at different granularity: - **Sync `POST /explain` (SSE) - metered exactly.** You pay your specific analysis's cost × 1.20 + $0.02, signed incrementally as it runs. - **Async `POST /explain/async` - fixed quote.** One upfront payment per request ([pay-as-you-go](https://mpp.dev/guides/pay-as-you-go) style): the rolling average cost of recent analyses × 1.20 + $0.02, stated in the `402` challenge before you commit. Cheap-run and expensive-run customers average out. ## What analyses actually cost Real charges from recent production runs on the default model (`claude-opus-4-8`): | Transaction | Upstream cost | You pay | Duration | |---|---|---|---| | Token transfer | $0.37 | **$0.47** | 74s | | Staking operation | $0.47 | **$0.58** | 61s | | Multi-step staking | $0.70 | **$0.86** | 98s | **Typical analyses land between $0.45 and $0.90.** Cost scales with transaction complexity - the agent runs more tool calls and burns more tokens on a novel exploit or a deep multi-protocol DeFi trace than on a transfer. Complex incident investigations can run into several dollars. ### Choosing a cheaper model The LLM is the dominant cost component (typically 70-90% of the total). Pass a `model` field on the request body to trade accuracy for cost: | Model | Relative cost | When to use | |---|---|---| | `claude-opus-4-8` *(default)* | 1× | Best accuracy - incident triage, exploits, anything you'll act on | | `claude-sonnet-4-6` | ~0.2× LLM cost | Routine DeFi decoding at volume | | `claude-haiku-4-5-20251001` | ~0.05× LLM cost | Bulk classification, simple transfers | Non-LLM costs (traces, labels, prices) are the same regardless of model, so the total doesn't scale down linearly - but on typical transactions Sonnet and Haiku cut the bill substantially. ## How metering works Sync analyses use **MPP metered sessions** - the [streamed-payments](https://mpp.dev/guides/streamed-payments) variant of the protocol's [session intent](https://mpp.dev/intents/session): 1. Your client opens a payment channel against the API with a `maxDeposit` cap. 2. As the analysis accrues upstream cost, the server requests $0.01 voucher increments so your cumulative payment tracks `cost × 1.20`. The session client signs them automatically mid-stream - no interaction. 3. The final increment adds the $0.02 fixed fee, and the result is released. Two properties fall out of this design: - **You never prepay for work that didn't happen.** If an analysis is cheap, you pay a cheap price. There is no flat quote to overshoot. - **Your hard spend cap is `maxDeposit`.** The server can never charge past the channel deposit you configured. Set it per session to whatever you're comfortable funding. ```typescript import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as `0x${string}`) // maxDeposit is your spend ceiling for the session - $15 covers ~17-25 typical analyses. const session = tempo.session.manager({ account, maxDeposit: '15' }) // session.sse('https://mpp.oculr.xyz/explain', …) - see the quickstart for the full call. ``` Because metering rides the stream, this applies to the SSE endpoint. The async endpoint instead pays its fixed quote through the classic one-shot flow (`Mppx.create()` + `fetch()`), settled from the same kind of session channel. Full recipes for both: [Call the oculr MPP](/quickstart/client). ## Cost transparency Every response tells you exactly what you paid for. The `costs` object breaks the upstream cost into category buckets: ```json { "summary": "Uniswap V3 swap: 1,000 USDC → 0.42 WETH", "costs": { "llms": 0.4655, "dataCollection": 0.2160, "codeExecution": 0.0138, "other": 0.0020, "totalUsd": 0.6973 } } ``` - `llms` - agent reasoning tokens. - `dataCollection` - traces, address labels, token prices, web research. - `codeExecution` - sandboxed calldata/bytecode analysis. - `other` - everything else. Watching the live stream? SSE mode emits `cost` events as spend accrues, so a UI can show the meter running. See [Core concepts → SSE streaming](/concepts/#sse-streaming-web-uis). ## What's free Discovery and health surfaces cost nothing - no payment challenge, no wallet needed: | Path | What it is | |---|---| | `GET /health` | Service health probe | | `GET /openapi.json` | Full OpenAPI 3.1 spec | | `GET /tool-spec.json` | Typed Anthropic + OpenAI tool schemas | | `GET /SKILL.md` | Prose entry point for agents | | `GET /llms.txt` | Discovery index for LLM crawlers | ## FAQ **Is there a rate limit?** No fixed limit - per-request payment is the throttle, and `maxDeposit` is your cap. **What if the analysis fails?** oculr returns partial results (HTTP 200, `degraded: true`) rather than failing outright - you're only metered for the work that ran. See [Partial results](/concepts/#partial-results). **Where does my payment go?** Settlement is USDC.e on Tempo, token contract `0x20C000000000000000000000b9537d11c60E8b50`. Funding instructions in the [FAQ](/faq#wallet-and-funding). ## Related - [Call the oculr MPP](/quickstart/client) - `mppx` setup and your first paid request - [Core concepts](/concepts/) - sessions, confidence levels, partial results - [FAQ](/faq) - wallet funding, supported chains, troubleshooting --- ### [FAQ](https://oculr.xyz/docs/faq) # FAQ > The questions people actually ask about oculr - pricing, supported chains, confidence levels, async usage, and what to do when a request fails. ## Pricing **How much does an analysis cost?** Pricing is metered - you pay the actual upstream cost of your analysis × 1.20, plus a $0.02 fixed fee. Typical analyses on the default model (`claude-opus-4-8`) land between **$0.45 and $0.90**; cost scales with transaction complexity, and you can pass a `model` field (`claude-sonnet-4-6`, `claude-haiku-4-5-20251001`) to cut the LLM component substantially. See **[Pricing](/pricing)** for real production examples and the full formula. The exact cost is dynamic - every response carries a `costs` object broken into category buckets (`llms`, `dataCollection`, `codeExecution`, `other`) plus a `costs.totalUsd` showing what that specific call actually cost. **Do I need an API key?** No. oculr uses [MPP/x402](https://mpp.dev) - you bring a wallet funded with USDC.e on [Tempo](https://tempo.xyz), use an MPP client like [`mppx`](https://www.npmjs.com/package/mppx), and every request settles a payment automatically. No signup, no API key, no rate limit beyond what your wallet funds. **oculr uses MPP sessions** (one of two MPP intents - the other is `charge`). The client opens a payment channel with a `maxDeposit`, signs cumulative vouchers per request, and the server redeems the highest voucher on-chain. Like a bar tab - many requests, one settlement. **Is there a rate limit?** No fixed rate limit. Per-request payment is the throttle. Your client's `maxDeposit` is your spend cap - set it to whatever ceiling you're comfortable with for the session. ## Wallet and funding **How do I fund the wallet?** You need a USDC.e balance on Tempo. The Tempo USDC.e contract is: ``` 0x20C000000000000000000000b9537d11c60E8b50 ``` If you already hold USDC.e on Tempo, top up that wallet. If you don't, transfer USDC.e to it however you normally move tokens on Tempo. oculr never asks you to use a particular bridge - anything that gets USDC.e into the wallet works. **Can I use a managed wallet instead of holding a private key?** Yes. [Tempo Wallet](https://wallet.tempo.xyz/welcome) is a managed MPP client with built-in spend controls and service discovery. The setup prompt fetches [tempo.xyz/SKILL.md](https://tempo.xyz/SKILL.md) - a public markdown file you can inspect before running - and walks the agent through wallet creation: ``` Read https://tempo.xyz/SKILL.md and set up tempo ``` The agent configures Tempo Wallet end-to-end. After setup, every paid call against `mpp.oculr.xyz` settles through the managed wallet. ## Chains oculr auto-detects the chain from the transaction hash - you don't pass a `chain` in the request. It covers **50+ EVM mainnets**. Analysis is richest on the chains below, which have full enrichment: address labels, USD values, and risk flags. | Slug | Network | |---|---| | `ethereum-mainnet` | Ethereum | | `base-mainnet` | Base | | `arbitrum-mainnet` | Arbitrum | | `optimism-mainnet` | Optimism | | `matic-mainnet` | Polygon | | `bsc-mainnet` | BNB Chain | | `avalanche-mainnet` | Avalanche | | `linea-mainnet` | Linea | | `zksync-mainnet` | zkSync Era | | `scroll-mainnet` | Scroll | | `blast-mainnet` | Blast | | `xdai-mainnet` | Gnosis | | `abstract-mainnet` | Abstract | | `bera-mainnet` | Berachain | | `celo-mainnet` | Celo | | `fantom-mainnet` | Fantom | | `fraxtal-mainnet` | Fraxtal | | `hype-mainnet` | Hyperliquid EVM | | `kaia-mainnet` | Kaia | | `mantle-mainnet` | Mantle | | `mode-mainnet` | Mode | | `nova-mainnet` | Arbitrum Nova | | `soneium-mainnet` | Soneium | | `sonic-mainnet` | Sonic | | `story-mainnet` | Story | | `unichain-mainnet` | Unichain | | `worldchain-mainnet` | World Chain | | `zkevm-mainnet` | Polygon zkEVM | | `sei-pacific` | Sei | The remaining mainnets are auto-detected with trace decoding - and USD values where price data exists - including Monad, Flare, Ink, Lisk, Morph, X Layer, Sophon, Plasma, Vana, and others. The response includes `chain` (slug) and `chainName` (display name) so you know which one matched. If the transaction hash isn't found on any supported chain, you'll get an error - see [troubleshooting](#troubleshooting). ## Transactions **What about reverted transactions?** oculr analyses reverted transactions just like successful ones. The `status` field comes back as `"reverted"` and the summary explains *why* - slippage exceeded, out of gas, custom revert, etc. **The analysis came back with `confidence: 'low'` - what does that mean?** The agent couldn't fully identify the protocol or all the major actors. Common causes: unverified contracts, a brand-new protocol no labeller has tagged yet, or a sparse trace. Treat the summary as a hint and cross-check the `risks` array. **When should I use `POST /explain/async`?** Three cases: - Your HTTP client has a short timeout. - You're calling from inside a parent agent's tool-use loop and don't want each turn to block. - You're processing many transactions concurrently. The async endpoint returns a `jobId` immediately - poll `GET /result/:jobId` every 2-3 seconds. **Can I stream progress to a UI?** Yes - SSE is how sync `POST /explain` works. Set `Accept: text/event-stream` (via `tempo.session.manager().sse()`, which carries the metered payment) and you'll get incremental events (`preflight_status`, `tool_call`, `tool_result`, `cost`, then `result`). Prefer plain fetch without a stream? Use `POST /explain/async` + polling. ## Agents **Should my agent fetch `SKILL.md` or `tool-spec.json`?** Both work for different shapes of agent. - **`/SKILL.md`** is prose - best for interactive CLIs (Claude Code, Amp, Codex CLI) where a human asks an agent to look at a transaction. The agent reads the file once, learns the call shape, and dispatches with an MPP client. - **`/tool-spec.json`** is typed Anthropic + OpenAI tool-use schemas - best for sub-agents inside a parent agent's tool-use loop. Drop the array straight into `client.messages.create({ tools: ... })` and you're done. Eliminates the "model parses markdown" class of integration bugs. See [Use as an agent](/quickstart/agent) for both setups end-to-end. **Does oculr produce a Mermaid diagram?** Yes, when the transaction has a clear call flow. The result includes a `mermaidDiagram` field. The hosted web app renders it under the **Flow** tab. ## Troubleshooting **I'm seeing HTTP 402 - what do I do?** Check the body first. If it carries `code: 'use_metered_sse'`, you sent a plain JSON `POST /explain` - sync analysis is a metered SSE stream, so switch to `tempo.session.manager().sse()`, or use `POST /explain/async` for plain fetch. Otherwise it's the standard MPP payment challenge: your `fetch()` wasn't intercepted by an MPP client. Install `mppx` and call `Mppx.create({ methods: [tempo({ account, maxDeposit: '5' })] })` once at startup. See [Call the oculr MPP](/quickstart/client) for both setups. **`mppx` is throwing `InsufficientBalance`.** Your wallet doesn't hold enough USDC.e on Tempo to open a payment channel. Top up the wallet at the USDC.e address listed in [Wallet and funding](#wallet-and-funding). Make sure `maxDeposit` is less than or equal to the wallet balance. **I'm getting `degraded: true` in the response.** The upstream stack (RPC, Anthropic, etc.) had a transient issue mid-analysis, and oculr returned a *partial* result rather than failing the request. `degradationReason` tells you which phase struggled. `trace-fetch-failure` is usually safe to retry after 30 seconds; `agent-loop-failure` is safe to retry once. **My transaction hash returns "not found on any supported chain".** The chain probably isn't one oculr supports yet. oculr checks every supported mainnet (50+) in parallel, so if *none* of them returned a hit, the chain you're on isn't covered. Verify the hash on the source chain's block explorer. ## Related - [Core concepts](/concepts/) - pipeline, confidence levels, sync vs async - [Use as an agent](/quickstart/agent) - skill mode and tool-use mode walkthroughs - [Endpoints reference](/reference/endpoints) - full request/response schemas --- ## Quickstart ### [Quickstart overview](https://oculr.xyz/docs/quickstart/) # Quickstart > Two integration paths. Pick the one that matches how you'll call oculr - then follow the dedicated guide. oculr is hosted at **[mpp.oculr.xyz](https://mpp.oculr.xyz)**. No server to run, no API key to manage. You bring a wallet funded with USDC.e on Tempo, and an MPP client like `mppx` handles the payment handshake on every request. ## Which path? | | Use as an agent | Call the oculr MPP | |---|---|---| | **For** | AI agents - coding CLIs, sub-agents, anything that already speaks LLM tool-use | Apps, scripts, services calling the API directly from code | | **Setup** | One-line prompt or one fetch of `/tool-spec.json` | `npm install mppx`, init once at boot | | **Time** | ~30 seconds | ~5 minutes | | **Guide** | **[Use as an agent →](/quickstart/agent)** | **[Call the oculr MPP →](/quickstart/client)** | Both paths hit the same endpoints and get back the same `ExplanationResult` JSON. ## What you need either way - An **EVM wallet** with USDC.e on [Tempo](https://tempo.xyz) (contract `0x20C000000000000000000000b9537d11c60E8b50`). Top up the wallet however you normally move tokens on Tempo. Typical analyses cost $0.45-$0.90 - see [Pricing](/pricing). - **[`mppx`](https://www.npmjs.com/package/mppx)**, the MPP/x402 client library. It intercepts the `402` payment challenge and pays it transparently. Install with `npm install mppx`; if you only want the CLI, `npm install -g mppx`. That's it. Pick a path above and follow the guide. ## Just want to eyeball one transaction? Skip the integration entirely. Paste the hash into **[oculr.xyz/app](https://oculr.xyz/app)** - the web app handles the streaming workflow, summary, trace, and flow diagram. No code, no install: just a browser wallet with USDC.e on Tempo. See **[Use the web app](/guides/web-app)**. ## Related - [Use as an agent](/quickstart/agent) - skill mode, sub-agent tool-use mode, raw API tutorial - [Call the oculr MPP](/quickstart/client) - `mppx` setup and first request from code - [Core concepts](/concepts/) - pipeline, confidence levels, sync vs async - [Pricing](/pricing) - the metered formula and real production costs - [FAQ](/faq) - wallet funding, supported chains, troubleshooting --- ### [Call the API](https://oculr.xyz/docs/quickstart/client) # Call the oculr MPP > Go from zero to your first paid transaction analysis in under 5 minutes - with `mppx` handling MPP/x402 payment transparently inside `fetch()`. The setup below is for apps, scripts, and backends calling the MPP from code. ## Run it from an agent No code needed - an agent CLI can make the same call for you in one line. Replace `0xYOUR_TX_HASH` with the real hash: :::code-group ```bash [Claude Code] claude -p "Read https://mpp.oculr.xyz/SKILL.md and analyse EVM transaction 0xYOUR_TX_HASH - what happened, any risks?" ``` ```bash [Amp] amp --execute "Read https://mpp.oculr.xyz/SKILL.md and analyse EVM transaction 0xYOUR_TX_HASH - what happened, any risks?" ``` ```bash [Codex CLI] codex exec "Read https://mpp.oculr.xyz/SKILL.md and analyse EVM transaction 0xYOUR_TX_HASH - what happened, any risks?" ``` ::: The agent fetches `SKILL.md`, sets up the MPP client, and runs the analysis. For deeper agent integration (persistent skill install, typed tool-use schemas, sub-agent patterns), see **[Use as an agent](/quickstart/agent)**. ## 1. Install `mppx` ```bash npm install mppx ``` `mppx` is the MPP/x402 client library. It intercepts `402` payment challenges, settles them on Tempo, and replays the request - your code sees a single round-trip. ## 2. Fund a wallet on Tempo Bring an EVM wallet holding **USDC.e on [Tempo](https://tempo.xyz)** (contract `0x20C000000000000000000000b9537d11c60E8b50`). Top up the wallet however you normally move tokens on Tempo. ```bash export WALLET_PRIVATE_KEY=0xYOUR_PRIVATE_KEY ``` > Prefer not to put a private key in an env var? `mppx account create` stores keys in your OS keychain - see the [agent quickstart](/quickstart/agent#set-up-your-wallet). ## 3. Analyse a transaction (sync, metered) Synchronous `POST /explain` uses [metered pricing](/pricing) - the payment is signed incrementally as the analysis accrues cost, which requires a **session client** and SSE. `tempo.session.manager()` handles the whole lifecycle: it opens the payment channel on first use, signs vouchers in the background as the stream charges, and reuses the channel across calls. ```typescript import { tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as `0x${string}`) // One manager per process. `maxDeposit` caps the channel total - your hard spend ceiling. const session = tempo.session.manager({ account, maxDeposit: '15' }) const stream = await session.sse('https://mpp.oculr.xyz/explain', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' }, body: JSON.stringify({ txHash: '0x4e4b8ed4…' }), }) for await (const payload of stream) { const msg = JSON.parse(payload) if (msg.type !== 'result') continue // progress events - ignore or log console.log(msg.summary) // → "Uniswap V3 swap: 1,000 USDC → 0.42 WETH via the 0.05% fee pool" console.log(msg.txType) // "swap" | "exploit" | "mev" | … console.log(msg.confidence) // "high" | "medium" | "low" console.log(msg.risks) // [] or ["High gas price: 3× base fee", …] } ``` The final `{ type: 'result', … }` message is the full `ExplanationResult` - shape at [Endpoints reference](/reference/endpoints). The other stream events (`preflight_status`, `agent_text`, `tool_call`, `cost`) are progress you can surface or ignore. :::info A plain JSON `POST /explain` (no SSE) returns `402` with `code: 'use_metered_sse'` - a one-shot payment can't meter as cost accrues. If you'd rather not consume a stream, use the async pattern below: plain `fetch()`, fixed quote per request. ::: ## 4. Or: plain-fetch async (fixed quote) `POST /explain/async` is priced as a **fixed quote per request** (the rolling average cost of recent analyses, same 20% margin + $0.02 fee - quoted upfront in the 402 challenge). That works with the classic `mppx` boot: call `Mppx.create()` once at startup and every `fetch()` auto-pays. ```typescript import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as `0x${string}`) await Mppx.create({ methods: [tempo({ account, maxDeposit: '5' })] }) const { jobId } = await fetch('https://mpp.oculr.xyz/explain/async', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ txHash: '0x4e4b8ed4…' }), }).then(r => r.json()) // Poll - GET /result/:jobId is free. while (true) { await new Promise(r => setTimeout(r, 3000)) const job = await fetch(`https://mpp.oculr.xyz/result/${jobId}`).then(r => r.json()) if (job.status === 'complete') { console.log(job.result.summary); break } if (job.status === 'error') throw new Error(job.error) } ``` ## 5. Pass a `context` hint to improve accuracy `context` is free-form natural language passed straight to the analysis agent. Use it whenever you already know something useful about the transaction - it works identically on both endpoints: ```typescript body: JSON.stringify({ txHash: '0x…', context: 'check if this is a reentrancy exploit', }) ``` ## Quick test from the shell If you have `mppx` installed globally (`npm install -g mppx`) and a funded account, you can kick off an async job from the command line - useful when prototyping: ```bash mppx mpp.oculr.xyz/explain/async \ -J '{"txHash":"0x4e4b8ed4de38be29e3a7a15e2b14b5d8262e3c5b3f1e9d6a7c8b9e0f1a2d3c4e"}' # → {"jobId":"…"} # Poll (free, plain curl works): curl -s https://mpp.oculr.xyz/result/YOUR_JOB_ID \ | jq '{status, result: {summary, txType, confidence}}' ``` `mppx` handles the `402` payment behind the scenes - bare `curl` against a paid endpoint will just return the payment challenge. ## Related - [Endpoints reference](/reference/endpoints) - full request/response schemas - [Use as an agent](/quickstart/agent) - agent-loop integration via `/tool-spec.json` or `/SKILL.md` - [Analyze a transaction](/guides/analyzing-transactions) - worked example with the result schema explained --- ### [Use as an agent](https://oculr.xyz/docs/quickstart/agent) # Use as an agent > Three integration paths for AI agents - pick by how tightly your agent loop needs to control the call. | Mode | Best for | Setup | |---|---|---| | [**Skill mode**](#skill-mode) | Interactive prompts - "ask Claude to look at this tx" | One-line prompt, or install a persistent skill | | [**Tool-use mode**](#tool-use-mode) | Sub-agents inside a parent tool-use loop | Fetch `/tool-spec.json`, drop into your LLM API call | | [**Raw API**](#raw-api) | Custom server pipelines, anything that wants full HTTP control | `mppx` + `fetch()` | All three hit the same `mpp.oculr.xyz` endpoints and get back the same `ExplanationResult`. --- ## Skill mode Best when a human asks an agent CLI to analyse a transaction *right now*. The agent reads `/SKILL.md` once, learns the call shape, and dispatches with an MPP client. ### One-shot prompt Paste this into your terminal - replace `0xYOUR_TX_HASH` with the real hash: :::code-group ```bash [Claude Code] claude -p "Read https://mpp.oculr.xyz/SKILL.md and analyse EVM transaction 0xYOUR_TX_HASH - what happened, which protocol, any risks, and the USD value?" ``` ```bash [Amp] amp --execute "Read https://mpp.oculr.xyz/SKILL.md and analyse EVM transaction 0xYOUR_TX_HASH - what happened, which protocol, any risks, and the USD value?" ``` ```bash [Codex CLI] codex exec "Read https://mpp.oculr.xyz/SKILL.md and analyse EVM transaction 0xYOUR_TX_HASH - what happened, which protocol, any risks, and the USD value?" ``` ::: The agent fetches `SKILL.md`, sets up an MPP client if it isn't already running, and runs the analysis (async start + poll by default). No persistent install - every session starts fresh. ### Install oculr as a persistent skill If you call oculr regularly, save `SKILL.md` into your agent's skills directory once so it doesn't refetch every session. For Claude Code that's `~/.claude/skills/oculr/`: ```bash mkdir -p ~/.claude/skills/oculr curl -s https://mpp.oculr.xyz/SKILL.md -o ~/.claude/skills/oculr/SKILL.md ``` Other agents follow their own convention - drop the file wherever that agent looks for skills. Once installed, prompts can reference the skill by name: ```bash claude -p "Use the oculr skill to analyse 0xYOUR_TX_HASH" ``` ### Set up your wallet You need a wallet holding USDC.e on Tempo. Two paths: **Option A - managed wallet via [Tempo Wallet](https://wallet.tempo.xyz/welcome).** Recommended if you don't want to handle a private key. Tempo Wallet is a managed MPP client with built-in spend controls and service discovery. The setup prompt below fetches [tempo.xyz/SKILL.md](https://tempo.xyz/SKILL.md) - a public markdown file you can inspect before running - and walks the agent through wallet creation: ``` Read https://tempo.xyz/SKILL.md and set up tempo ``` The agent handles the rest. **Option B - local key via `mppx`.** `mppx` ships an account manager that stores keys in your OS keychain (Keychain on macOS, Credential Manager on Windows, libsecret on Linux): ```bash # Create a new account (key written to the OS keychain - no plaintext on disk) mppx account create ``` Once created, transfer USDC.e to the account's address. The Tempo mainnet USDC.e contract is `0x20C000000000000000000000b9537d11c60E8b50`. --- ## Tool-use mode Best when oculr is **one tool among several** that a parent agent orchestrates. The parent gets typed schemas, latency hints, and a dispatch table - no markdown parsing. ### Step 1 - fetch the tool spec ```typescript const spec = await fetch('https://mpp.oculr.xyz/tool-spec.json').then(r => r.json()) ``` Returns: ```json { "version": 1, "baseUrl": "https://mpp.oculr.xyz", "auth": "mpp-x402", "anthropic": [ /* 3 tools - Anthropic Messages format */ ], "openai": [ /* 3 tools - OpenAI Chat Completions format */ ], "endpoints": { "explain_transaction": { "method": "POST", "path": "/explain" }, "start_explain_job": { "method": "POST", "path": "/explain/async" }, "get_job_result": { "method": "GET", "path": "/result/{jobId}" } }, "skillUrl": "https://mpp.oculr.xyz/SKILL.md", "openapiUrl": "https://mpp.oculr.xyz/openapi.json" } ``` The three exposed tools: | Tool | Purpose | Blocking? | |---|---|---| | `explain_transaction` | Analyse a tx, return result inline | Yes | | `start_explain_job` | Start async analysis, return `jobId` | No | | `get_job_result` | Poll an async job for its result | No | **For sub-agents, prefer the async flow** - `start_explain_job` + `get_job_result` keeps the parent agent's tool-call turn fast. ### Step 2 - register the tools with your LLM ::: code-group ```typescript [Anthropic Messages API] import Anthropic from '@anthropic-ai/sdk' const spec = await fetch('https://mpp.oculr.xyz/tool-spec.json').then(r => r.json()) const client = new Anthropic() const response = await client.messages.create({ model: 'claude-opus-4-8', tools: spec.anthropic, messages: [{ role: 'user', content: 'Analyse tx 0x4e4b8ed4…' }], }) ``` ```typescript [OpenAI Chat Completions] import OpenAI from 'openai' const spec = await fetch('https://mpp.oculr.xyz/tool-spec.json').then(r => r.json()) const client = new OpenAI() const response = await client.chat.completions.create({ model: 'gpt-4o', tools: spec.openai, messages: [{ role: 'user', content: 'Analyse tx 0x4e4b8ed4…' }], }) ``` ::: ### Step 3 - dispatch tool calls When the LLM emits a tool call, look up the HTTP route in `spec.endpoints` and dispatch. `mppx` handles the `402` payment automatically: ```typescript import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' // `maxDeposit` is required - caps the channel total this session funds. await Mppx.create({ methods: [tempo({ account: privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as `0x${string}`), maxDeposit: '5', })], }) async function dispatchToolCall(name: string, args: Record) { // Sync explain_transaction is a metered SSE stream - a plain JSON POST to // /explain returns 402 use_metered_sse. In a plain-fetch executor, serve it // through the async pair instead: same ExplanationResult, no stream to consume. if (name === 'explain_transaction') { const { jobId } = await dispatchToolCall('start_explain_job', args) while (true) { await new Promise(r => setTimeout(r, 3000)) const job = await dispatchToolCall('get_job_result', { jobId }) if (job.status === 'complete') return job.result if (job.status === 'error') throw new Error(job.error) } } const route = spec.endpoints[name] if (!route) throw new Error(`Unknown tool: ${name}`) // Substitute path params, e.g. /result/{jobId} const path = route.path.replace(/\{(\w+)\}/g, (_, k) => String(args[k])) const url = `${spec.baseUrl}${path}` const init: RequestInit = { method: route.method } if (route.method === 'POST') { init.headers = { 'Content-Type': 'application/json' } init.body = JSON.stringify(args) } const res = await fetch(url, init) if (!res.ok) throw new Error(`oculr ${name} ${res.status}: ${(await res.text()) || 'no body'}`) return res.json() } ``` ### Step 4 - async polling pattern ```typescript async function explainAsync(txHash: string, context?: string) { const { jobId } = await dispatchToolCall('start_explain_job', { txHash, context }) while (true) { await new Promise(r => setTimeout(r, 3000)) const job = await dispatchToolCall('get_job_result', { jobId }) if (job.status === 'complete') return job.result if (job.status === 'error') throw new Error(job.error) } } ``` --- ## Raw API For custom server pipelines, or any case where you want full control of the HTTP layer. ### Step 1 - fund a wallet oculr charges per request via MPP. There's no account, no API key. You bring an EVM wallet funded with **USDC.e on [Tempo](https://tempo.xyz)** - settlement runs over Tempo MPP sessions. The Tempo mainnet USDC.e contract is `0x20C000000000000000000000b9537d11c60E8b50`. ```bash export WALLET_PRIVATE_KEY=0xYOUR_PRIVATE_KEY ``` Or use `mppx account create` to store the key in your OS keychain rather than an env var. ### Step 2 - install `mppx` ```bash npm install mppx ``` `mppx` is the MPP/x402 client library. It intercepts `402` responses, pays the challenge, and replays the request - your `fetch()` sees a single round-trip. ### Step 3 - boot `mppx` and analyse a transaction Call `Mppx.create()` once at startup; every subsequent `fetch()` on `globalThis` auto-pays `402`s for the configured account. For agents, use the async path - `POST /explain/async` is a fixed quote per request and works with plain `fetch()`, and it returns a `jobId` immediately so your parent agent's tool-call turn stays fast. (Sync `POST /explain` is a metered SSE stream needing `tempo.session.manager().sse()` - see [Call the oculr MPP](/quickstart/client); a plain JSON `POST /explain` returns `402 use_metered_sse`.) ```typescript import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY! as `0x${string}`) // `maxDeposit` is required - '5' is $5 USDC.e (~6-10 analyses); caps the channel total funded this session. await Mppx.create({ methods: [tempo({ account, maxDeposit: '5' })] }) ``` ```typescript async function analyseTransaction(txHash: string, context?: string) { const startRes = await fetch('https://mpp.oculr.xyz/explain/async', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ txHash, context }), }) if (!startRes.ok) throw new Error(`oculr start: ${startRes.status}`) const { jobId } = await startRes.json() while (true) { await new Promise(r => setTimeout(r, 3000)) const pollRes = await fetch(`https://mpp.oculr.xyz/result/${jobId}`) if (!pollRes.ok) throw new Error(`oculr poll: ${pollRes.status}`) const job = await pollRes.json() if (job.status === 'complete') return job.result if (job.status === 'error') throw new Error(`oculr: ${job.error}`) } } ``` ### Step 4 - branch on the result A successful response is an `ExplanationResult`. The fields an agent typically routes on: ```typescript const { summary, txType, confidence, risks, degraded } = result // Always check the partial signal first. if (degraded) { return escalate(result) } // Confidence-gated handling. if (confidence === 'high') { return summary } else if (confidence === 'medium' && risks.length === 0) { return summary } else { return escalate(result) } // `txType` is a closed enum - dispatch to specialised follow-up. switch (txType) { case 'swap': return classifySwapPnL(result) case 'exploit': return triageExploit(result) case 'mev': return logMevPattern(result) case 'liquidation': return creditRiskUpdate(result) // … } ``` ### Quick test from the shell If you have `mppx` installed globally (`npm install -g mppx`) and a funded account, you can ping the API from the command line - useful when prototyping: ```bash mppx mpp.oculr.xyz/explain/async \ -J '{"txHash":"0x4e4b8ed4de38be29e3a7a15e2b14b5d8262e3c5b3f1e9d6a7c8b9e0f1a2d3c4e"}' # → {"jobId":"…"} - poll with plain curl (free): curl -s https://mpp.oculr.xyz/result/JOB_ID | jq '{status, result: {summary, txType, confidence}}' ``` `mppx` handles the `402` payment behind the scenes - bare `curl` against a paid endpoint will just return the payment challenge. --- ## Sub-agent design notes If you're embedding oculr into a parent agent, these are the integration points worth being deliberate about: - **Prefer `start_explain_job` + `get_job_result` over `explain_transaction`.** Async polling keeps each parent-agent turn fast. - **Branch on `result.confidence`.** `'high'` → use the summary verbatim; `'medium'` → cross-check `risks`; `'low'` → escalate. - **Branch on `result.degraded` first.** A partial result is still a `200` but with `confidence: 'low'` and limited fields - don't trust the body uncritically. - **Use `result.txType` for routing.** It's a closed enum (`swap | transfer | exploit | mev | …`) - dispatch to specialised follow-up logic per type. - **Pass `context` aggressively.** It's free, accepts natural language, and meaningfully improves accuracy. Examples: `"this address is suspected of front-running"`, `"verify if this is a sandwich attack"`. - **A non-empty `risks` array is a signal.** Even if `txType` is benign, populated `risks[]` warrants escalation. - **Cap your spend client-side.** `mppx`'s `maxDeposit` is your session ceiling - set it to whatever you're comfortable losing if the wallet key leaks. ## Related - [`/tool-spec.json`](https://mpp.oculr.xyz/tool-spec.json) - typed contract for tool-use mode - [`/SKILL.md`](https://mpp.oculr.xyz/SKILL.md) - prose entry point for skill mode - [`/openapi.json`](https://mpp.oculr.xyz/openapi.json) - full REST schema - [Endpoints reference](/reference/endpoints) - request/response details --- ## Guides ### [Analyze a transaction](https://oculr.xyz/docs/guides/analyzing-transactions) # Analyze a transaction > Submit a tx hash, get back a plain-English explanation with risk flags and USD value. This page covers all three call styles and walks through how to read the result. Before you start: install `mppx` and fund a Tempo wallet - see [Call the oculr MPP](/quickstart/client#1-install-mppx). If you'll only ever ask an agent CLI to analyse a tx, the agent will set this up for you - see [Use as an agent](/quickstart/agent#skill-mode). ## With an agent One-line prompt for a coding agent: :::code-group ```bash [Claude Code] claude -p "Read https://mpp.oculr.xyz/SKILL.md and analyse EVM transaction 0xYOUR_TX_HASH - what happened, which protocol, any risks, and the USD value?" ``` ```bash [Amp] amp --execute "Read https://mpp.oculr.xyz/SKILL.md and analyse EVM transaction 0xYOUR_TX_HASH - what happened, which protocol, any risks, and the USD value?" ``` ```bash [Codex CLI] codex exec "Read https://mpp.oculr.xyz/SKILL.md and analyse EVM transaction 0xYOUR_TX_HASH - what happened, which protocol, any risks, and the USD value?" ``` ::: The agent reads `SKILL.md` once, then calls `POST /explain` with `mppx` handling the `402` payment. Replace `0xYOUR_TX_HASH` with a real hash. ## From your code (TypeScript) Plain-fetch path - start an async job and poll (fixed quote per request; the sync SSE alternative is in [Call the oculr MPP](/quickstart/client)): ```typescript import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as `0x${string}`) await Mppx.create({ methods: [tempo({ account, maxDeposit: '5' })] }) const { jobId } = await fetch('https://mpp.oculr.xyz/explain/async', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ txHash: '0x4e4b8ed4de38be29e3a7a15e2b14b5d8262e3c5b3f1e9d6a7c8b9e0f1a2d3c4e', // Optional - improves accuracy on ambiguous transactions: context: 'check if this is a reentrancy exploit', }), }).then(r => r.json()) let analysis while (!analysis) { await new Promise(r => setTimeout(r, 3000)) const job = await fetch(`https://mpp.oculr.xyz/result/${jobId}`).then(r => r.json()) if (job.status === 'complete') analysis = job.result if (job.status === 'error') throw new Error(job.error) } ``` ## From the shell (`mppx` CLI) ```bash mppx mpp.oculr.xyz/explain/async \ -J '{"txHash":"0x4e4b8ed4de38be29e3a7a15e2b14b5d8262e3c5b3f1e9d6a7c8b9e0f1a2d3c4e"}' # → {"jobId":"…"} - poll with plain curl (free): curl -s https://mpp.oculr.xyz/result/JOB_ID | jq '{status, result: {summary, txType, confidence, risks}}' ``` `mppx` handles the `402` payment challenge automatically. Bare `curl` against a paid endpoint will return the challenge body and stop. ## Async pattern for batch processing ```bash JOB=$(mppx mpp.oculr.xyz/explain/async \ -J '{"txHash":"0x…"}' | jq -r '.jobId') # Poll until done. while true; do S=$(mppx mpp.oculr.xyz/result/$JOB | jq -r '.status') [ "$S" = "complete" ] && break [ "$S" = "error" ] && { echo "Job failed"; exit 1; } sleep 3 done mppx mpp.oculr.xyz/result/$JOB | jq -r '.result.summary' ``` ## In the web app Paste your hash into [oculr.xyz/app](https://oculr.xyz/app) - the **Workflow** tab streams live progress; the **Summary** tab shows the final result; the **Flow** tab renders the Mermaid sequence diagram. --- ## Example result A Uniswap V3 swap on Ethereum mainnet: ```json { "txHash": "0x4e4b…", "chain": "ethereum-mainnet", "chainName": "Ethereum", "status": "success", "summary": "Uniswap V3 swap: 1,000 USDC → 0.42 WETH via the 0.05% fee pool", "steps": [ "Sender called exactInputSingle on Uniswap V3 Router", "Router called swap on the USDC/WETH 0.05% pool", "Pool transferred 0.42 WETH to sender" ], "risks": [], "protocol": "uniswap_v3", "txType": "swap", "confidence": "high", "usdValue": 1000.00, "addresses": [ { "address": "0x…", "label": "Uniswap V3 Router", "role": "router" } ], "contracts": [ { "address": "0x…", "name": "UniswapV3Pool", "purpose": "0.05% fee USDC/WETH pool" } ], "costs": { "llms": 0.33, "dataCollection": 0.03, "codeExecution": 0.01, "other": 0.00, "totalUsd": 0.37 } } ``` Optional fields you may also see, depending on the transaction: - `degraded` / `degradationReason` - set when the upstream stack hit a transient issue mid-analysis. Always check this *first*; a partial result is still HTTP 200 but with `confidence: 'low'` and limited fields. - `mermaidDiagram` - Mermaid sequence-diagram source, when the call flow is non-trivial. - `balanceChanges` - per-address signed balance flow with USD values. - `tokenTransfers` - every ERC-20/721/1155 transfer touched in the trace. - `prettyTrace` / `traceAnnotations` - annotated call tree, used by the web app's **Trace** tab. - `findings` - structured exploit findings (one entry per distinct vulnerability), populated when `txType === 'exploit'`. Full schema in [`POST /explain` → Response](/reference/endpoints/explain#response). ## Enum values The fields with closed enums (your code can switch on these safely): **`status`** | Value | Meaning | |---|---| | `success` | Transaction executed and state was committed. | | `reverted` | Transaction reverted; the summary explains why (slippage, OOG, custom revert, …). | **`txType`** | Value | Meaning | |---|---| | `swap` | Token swap on a DEX router or aggregator. | | `transfer` | Plain ERC-20 / ERC-721 / native transfer. | | `exploit` | Suspected protocol exploit. Populates `findings[]`. | | `liquidation` | Lending-protocol liquidation. | | `bridge` | Cross-chain bridge deposit, withdrawal, or message. | | `deployment` | Contract deployment. | | `mev` | MEV - sandwich, JIT liquidity, atomic arb, backrun. | | `governance` | DAO vote, proposal, or executor call. | | `routine_infra` | Keeper, multisig admin, sequencer maintenance. | | `approval` | ERC-20 `approve` or permit. | | `stake` | Staking deposit, withdrawal, restaking, or claim. | | `other` | Doesn't match the above; check `summary` and `risks`. | **`confidence`** | Value | Meaning | What to do | |---|---|---| | `high` | Protocol and all major actors identified. | Use the summary verbatim. | | `medium` | Some addresses or protocol unknown. | Treat as a hint; cross-check `risks`. | | `low` | Sparse trace or mostly unknown contracts. | Investigate further or escalate. | **`chain`** - oculr auto-detects from the tx hash (no `chain` field in the request) and returns the matched slug plus `chainName`. oculr covers 50+ EVM mainnets; the full list is in [FAQ → Chains](/faq#chains). **`costs`** (all keys present whenever `costs` is returned) | Key | What it covers | |---|---| | `llms` | LLM inference for the analysis agent loop | | `dataCollection` | On-chain data, analytics, prices, and labels (RPC, SQL analytics, token prices, address labels, metadata) | | `codeExecution` | Sandboxed code execution | | `other` | Fallthrough bucket | | `totalUsd` | Sum of all of the above | ## Interpreting the result **Branch on `degraded` first.** If `degraded: true`, you're looking at a partial result - the upstream stack hit a transient issue mid-analysis. Don't trust the body uncritically. **Then branch on `confidence`.** `high` → use the summary verbatim. `medium` → cross-check `risks`. `low` → investigate further or escalate. **Read `risks`.** Common flags: - `"High gas price: 3× base fee"` - possible MEV urgency or panic. - `"Known exploiter address detected"` - sender/recipient is a known bad actor. - `"Unverified contract handles user funds"` - no verified source on the matched chain's explorer. A non-empty `risks` array warrants attention even when `txType` is benign. **Use `txType` for routing.** It's a closed enum - your code can switch on it to dispatch to specialised follow-up (PnL classification, exploit triage, MEV pattern logging). ## Related - [Endpoints reference](/reference/endpoints) - full request/response schemas - [Core concepts](/concepts/) - how the analysis pipeline works - [Use as an agent](/quickstart/agent) - agent-loop integration --- ### [Use the web app](https://oculr.xyz/docs/guides/web-app) # Use the web app > Paste a hash at [oculr.xyz/app](https://oculr.xyz/app), watch the analysis stream live, and read the result in five views - no code, no setup beyond a wallet. The web app is the fastest way to triage a single transaction: one-off incident checks, eyeballing something suspicious, or walking a teammate through what a transaction did. For automation, use the [API](/quickstart/client) or [agent integration](/quickstart/agent) instead - same JSON contract, same pricing. ## What you need - A browser wallet (MetaMask, Rabby, or anything EIP-1193 compatible). - **USDC.e on [Tempo](https://tempo.xyz)** in that wallet. Funding instructions are in the [FAQ](/faq#wallet-and-funding). ## Connect and pay 1. Open [oculr.xyz/app](https://oculr.xyz/app) and click **Connect wallet**. 2. If your wallet is on another network, the app prompts **Switch to Tempo** - one click adds/switches the chain. 3. On your first analysis, the app opens an **MPP session** against the API: a payment channel with a deposit ceiling, authorised with a single wallet signature. No pop-up per request - as the analysis runs, the session signs metered vouchers automatically in the background. The payment bar shows your connected address and the session balance - your deposit minus what's been charged so far ([metered pricing](/pricing): actual cost × 1.20 + $0.02 per analysis). When the balance runs low, a **Top up** button adds deposit to the same channel with one more signature. Your session channel is **reused across analyses** - connect once, analyse many. Unused deposit is reclaimed when the session settles on disconnect. ## Run an analysis Paste a transaction hash from any of the 50+ supported EVM mainnets - the chain is auto-detected, no dropdown to pick. The analysis streams in real time, and the app switches to the **Summary** tab when it completes. Typical runs take 1-2 minutes; complex transactions take longer. ## The five views | Tab | What it shows | When to use it | |---|---|---| | **Workflow** | The live stream: pre-flight progress, the agent's reasoning as it thinks, and every tool call with its duration | Watching the analysis run; understanding *how* oculr reached its conclusion | | **Summary** | The human-readable result - what happened, who did it, risks, protocol, USD value, confidence | The answer, for humans | | **Trace** | The decoded call tree with resolved function signatures and address labels | Digging into a specific call frame yourself | | **Flow** | A rendered diagram of the transaction - who called what, where assets moved | Explaining the transaction to someone else; screenshots for write-ups | | **JSON** | The raw `ExplanationResult`, exactly as the [API](/reference/endpoints/explain) returns it | Copying structured data into a report or tool | The Workflow tab is worth watching at least once: each tool call you see is the agent [buying a resource under the hood](/concepts/under-the-hood) - a trace, a label lookup, a price - and it makes the metered cost of the run concrete. ## Reading the result The same rules apply as in the API: - **Check `confidence` first** (shown in the Summary). `high` means protocol and actors identified; `low` means sparse data - treat the summary as a lead, not a verdict. See [Confidence levels](/concepts/#confidence-levels). - **A partial result is not an error.** If an upstream source had a transient issue you still get a result, flagged as partial with the reason. Re-running after a moment usually completes it. ## Troubleshooting **"Switch to Tempo" keeps appearing** - your wallet rejected the chain switch. Add Tempo manually: chain ID `4217`, RPC `https://rpc.tempo.xyz`. **Connect succeeds but analysis won't start** - your wallet likely holds no USDC.e on Tempo, so the session channel can't open. Fund the wallet per the [FAQ](/faq#wallet-and-funding) and retry. **Balance shows $0.00 left mid-analysis** - the session deposit is spent. Click **Top up**; the running analysis resumes charging against the new deposit. ## Related - [Pricing](/pricing) - what an analysis costs and how metering works - [Analyze a transaction](/guides/analyzing-transactions) - interpreting results, worked examples - [Call the oculr MPP](/quickstart/client) - the same analysis from your own code --- ## Concepts ### [Core concepts](https://oculr.xyz/docs/concepts/) # Core concepts > Mental models for using oculr effectively - the analysis pipeline, MPP/x402 payments, confidence levels, and sync vs async. ## The analysis pipeline oculr runs a three-phase pipeline for every transaction: 1. **Pre-flight** - fetch the trace, detect the chain, resolve address labels and contract source. Runs in parallel before any LLM iteration so the agent starts with as much context as possible. 2. **Agent loop** - Claude orchestrates tool calls to resolve unknowns: token prices, wallet labels, contract behaviour, web context for novel protocols. The agent picks the tool set per transaction based on what it sees in the trace. 3. **Report** - assemble the structured `ExplanationResult` and return it. The depth of analysis scales with the transaction: a simple transfer resolves in one or two passes; a novel exploit may take several. ## MPP/x402 payments oculr charges per request - no API key, no signup. When you call the API without an active payment session, the server returns `402 Payment Required` with a payment challenge. The [`mppx`](https://www.npmjs.com/package/mppx) client handles the whole exchange on Tempo transparently. You can read the protocol spec at [mpp.dev](https://mpp.dev). For oculr specifically: - **Payment uses MPP sessions** (the protocol's [session intent](https://mpp.dev/intents/session)). Your client opens a payment channel against the API with `maxDeposit` (a per-channel cap), signs cumulative vouchers, and the server redeems the highest voucher on-chain. One settlement covers many requests. - **Sync `/explain` is metered.** Vouchers are signed incrementally *during* the analysis as cost accrues, so it requires a session client consuming SSE - `tempo.session.manager().sse()`. A plain JSON `POST /explain` returns `402` with `code: 'use_metered_sse'`. - **Async `/explain/async` is fixed-quote.** One upfront payment per request (the rolling average cost of recent analyses, same margin + fee), so the classic `Mppx.create()` + `fetch()` pattern works. See [Pricing](/pricing). - **Your spend cap is the client's `maxDeposit`.** Set it to whatever you're comfortable funding for the session. - **Settlement is in USDC.e on Tempo.** Token contract `0x20C000000000000000000000b9537d11c60E8b50`. ## Confidence levels Every result carries a `confidence` rating. Branch on it. | Level | Meaning | What to do | |---|---|---| | `high` | Protocol and all major actors identified. | Use the summary verbatim. | | `medium` | Some addresses or protocol unknown. Summary may be incomplete. | Treat as a hint; cross-check `risks`. | | `low` | Sparse trace or mostly unknown contracts. | Investigate further or escalate. | ```typescript const { confidence, summary, risks, txType } = result if (confidence === 'high') { // Use the summary verbatim in your output. } else if (confidence === 'medium') { // Cross-check risks before acting. } else { // Escalate or investigate. } ``` ## Partial results oculr never 5xx's mid-analysis. When an upstream service (RPC, Anthropic, etc.) has a transient issue, you get HTTP 200 with `degraded: true`, a populated `degradationReason`, and `confidence: 'low'`. The field name is `degraded` (kept for backwards compatibility) but the result is best understood as **partial** - real but incomplete. Always check `degraded` first. ```typescript const result = await res.json() if (result.degraded) { // Reason is one of: 'trace-fetch-failure' | 'agent-loop-failure' | 'sse-stream-fatal' // 'trace-fetch-failure' → safe to retry after 30s // 'agent-loop-failure' → safe to retry once // 'sse-stream-fatal' → retry via POST /explain/async (no stream) return handlePartial(result) } ``` You can also branch off the response header without parsing the body: `X-Oculr-Result-Quality: complete | partial`. ## Sync vs async **Sync** (`POST /explain`, SSE) - streams until complete; the final `{ type: 'result' }` event is the full result. Metered pricing, requires `tempo.session.manager().sse()`. Use when you want the result in one call, live progress, or exact metered cost. **Async** (`POST /explain/async` + `GET /result/:jobId`) - returns a `jobId` immediately; fixed quote per request, plain `fetch()` works. Use for UIs, batch processing, sub-agent loops, and anywhere with a short HTTP timeout. Poll every 2-3 seconds; results expire after 1 hour. ```typescript // Sub-agent friendly - non-blocking start. const { jobId } = await fetch('https://mpp.oculr.xyz/explain/async', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ txHash }), }).then(r => r.json()) // Poll until complete. while (true) { await new Promise(r => setTimeout(r, 3000)) const job = await fetch(`https://mpp.oculr.xyz/result/${jobId}`).then(r => r.json()) if (job.status === 'complete') return job.result if (job.status === 'error') throw new Error(job.error) } ``` ## SSE streaming (web UIs) SSE is how sync `POST /explain` works: set `Accept: text/event-stream` and each agent step emits its own SSE message - `preflight_status`, `agent_text` (token-by-token reasoning), `tool_call`, `tool_result`, `cost`, then a final `result` (JSON) or `report` (HTML). The stream is also what makes [metered payment](/pricing) possible - vouchers renew as `cost` events accrue. If you don't want to consume a stream, use the async path - single-event polling is easier to integrate into a tool-use loop, and it's priced as a fixed quote so plain `fetch()` works. ## Related - [Under the hood](/concepts/under-the-hood) - how oculr buys traces, labels, and compute over MPP per request - [Glossary](/concepts/glossary) - definitions for every term used across oculr - [Endpoints reference](/reference/endpoints) - full API surface with request/response schemas - [Use as an agent](/quickstart/agent) - sub-agent integration with `/tool-spec.json` --- ### [Under the hood](https://oculr.xyz/docs/concepts/under-the-hood) # Under the hood > oculr is machine-payments native. You pay oculr over [MPP](https://mpp.dev) - and oculr buys its on-chain data, web research, and compute the same way, per request. You never hold an API key, and everything oculr spends on your analysis is accounted per request. ## The self-funding loop When your payment settles, part of it immediately funds the upstream work for *your* analysis: ``` You (your wallet) │ MPP session - metered vouchers, USDC.e on Tempo ▼ oculr API │ MPP charges & sessions, per request ├──▶ RPC provider - transaction traces (50+ EVM mainnets) ├──▶ Blockchain database - decoded history, wallet activity, SQL ├──▶ Web research - novel protocols, incident context ├──▶ Code execution - sandboxed calldata & bytecode analysis ├──▶ Price data - token prices, USD values │ │ usage-based billing, metered per token └──▶ LLM inference - the analysis agent itself ``` This is why oculr needs no accounts and no signup: there is no platform subscription being amortised across users. Each request buys exactly the upstream work it needs, and the [metered price](/pricing) you pay is that cost plus a fixed margin. ## What gets bought per request Every analysis draws from a mix of paid MPP services and free public infrastructure. The paid calls are what you see itemised in the `costs` buckets on your response. ### Paid over MPP | Supplier role | What oculr buys | Typical per-call cost | `costs` bucket | |---|---|---|---| | RPC provider | `debug_traceTransaction` call trees, receipts, bytecode across 50+ mainnets | ~$0.001 | `dataCollection` | | Blockchain database | Decoded transaction history, wallet activity, address labels via SQL over 80+ chains | ~$0.01-0.03 | `dataCollection` | | Web research | Search + page extraction when the agent meets a novel protocol or fresh incident | ~$0.01 | `dataCollection` | | Price data | Token prices and market data for USD valuation | ~$0.001 | `dataCollection` | | Code execution | Sandboxed environment for calldata decoding and bytecode analysis | ~$0.0001/exec | `codeExecution` | ### LLM inference The agent's reasoning tokens are the dominant cost of most analyses. Inference is billed usage-based with the model provider - metered per token, with prompt-cache discounts passed through - and lands in the `llms` bucket at exactly what your run consumed. Same per-request accounting, different payment rail. ### Free public infrastructure Not everything costs money. oculr prefers free, keyless sources when they're as good: - **Block explorers (Blockscout)** - decoded token transfers, verified contract source, ERC-4337 UserOperations. - **Signature databases (Openchain)** - function selector and event topic lookups for every frame in the call tree. - **Safe Transaction Service** - multisig context when a Gnosis Safe appears in the trace. - **Local decompilation (heimdall)** - Solidity reconstruction for unverified contracts, run on oculr's own hardware. Free sources are tried first where they overlap with paid ones - address labels, for example, come from Blockscout instantly and fall back to a paid database lookup only when Blockscout has nothing. ## Adaptive spend The agent decides per transaction which tools to invoke, so upstream spend tracks transaction complexity: - A **simple transfer** needs a trace, a couple of label lookups, and a short agent loop - a handful of sub-cent calls. - A **multi-protocol DeFi transaction** adds price lookups, decoded history, and more agent iterations. - A **novel exploit** can trigger web research, contract decompilation, sandboxed re-analysis of calldata, and a long agent loop. You can watch this happen live: in [SSE mode](/concepts/#sse-streaming-web-uis), each `tool_call` event is the agent buying one of the resources above, and `cost` events show the running total. ## Why this architecture Running the supply chain on per-request payments instead of platform subscriptions has three customer-visible consequences: 1. **True metered pricing.** oculr's costs are per-request, so your [price](/pricing) can be too. There's no monthly platform fee being recovered from you. 2. **Full cost transparency.** Because every upstream call is individually paid or metered, every upstream call is individually accounted - that's what makes the `costs` breakdown on your response exact rather than estimated. 3. **No key for you to manage.** Your side of the relationship is a funded wallet and a protocol - no signup, no oculr API key to store, rotate, or leak. ## Related - [Pricing](/pricing) - the metered formula and real production costs - [Core concepts](/concepts/) - the analysis pipeline these suppliers feed - [Glossary](/concepts/glossary) - MPP, sessions, vouchers, and the rest of the vocabulary --- ### [Glossary](https://oculr.xyz/docs/concepts/glossary) # Glossary > Canonical definitions for every term used across oculr documentation. Alphabetised. | Term | Definition | |---|---| | **Analysis** | The process oculr performs on a transaction. Use "analysis", not "interpretation" or "scan". | | **`chain`** | Optional response field carrying the detected chain slug (e.g. `"ethereum-mainnet"`, `"base-mainnet"`). oculr auto-detects from the tx hash by fanning `eth_getTransactionByHash` across every supported chain in parallel - you don't pass a `chain` in the request. Companion field `chainName` carries the human-readable name (`"Ethereum"`, `"Base"`, …). The chain slugs are listed in [FAQ → Chains](/faq#chains). | | **Confidence** | `high` / `medium` / `low` on every `ExplanationResult`. Reflects how complete the actor and protocol identification is. Agents should branch on this field - see [Core concepts → Confidence levels](/concepts/#confidence-levels). | | **`degradationReason`** | Set when `degraded === true`. One of: `'trace-fetch-failure'` (RPC unreachable), `'agent-loop-failure'` (LLM timeout or similar), `'sse-stream-fatal'` (stream itself broke). Stable enum - safe to switch on. | | **ExplanationResult** | The JSON returned by `POST /explain` and `GET /result/:jobId` (when status is `complete`). Required: `txHash`, `status`, `summary`, `steps`, `risks`, `protocol` (nullable), `txType` (nullable), `confidence`, `addresses[]`, `contracts[]`, `usdValue` (nullable). Optional: `chain` + `chainName`, `costs` (11 keys), `toolCalls[]`, `mermaidDiagram`, `balanceChanges[]`, `tokenTransfers[]`, `prettyTrace[]`, `findings[]`, `degraded`, `degradationReason`. Full schema at [`POST /explain` → Response](/reference/endpoints/explain#response). | | **`findings`** | Optional array on `ExplanationResult`, populated when `txType === 'exploit'`. One entry per distinct vulnerability, each with `broken_invariant`, `category`, `severity`, `confidence`, `victim[]`, `attacker[]`, `evidence[]`, and `missing_data_to_confirm[]`. | | **Job** | An async analysis task started by `POST /explain/async`. Returns a UUID `jobId`. Poll `GET /result/:jobId` to get the result. Jobs expire 1 hour after they're started. | | **Metered pricing** | oculr's pricing model: `charge = upstream cost × 1.20 + $0.02`, settled in $0.01 voucher increments as the analysis accrues cost. You pay what your specific analysis cost, not a flat quote. Implemented as MPP [streamed payments](https://mpp.dev/guides/streamed-payments). See [Pricing](/pricing). | | **MPP** | [Machine Payments Protocol](https://mpp.dev). Per-request USDC payments settled via Tempo MPP sessions. oculr uses the `mppx` implementation. | | **`mppx`** | TypeScript library implementing MPP/x402. Client-side: `Mppx.create({ methods: [tempo({ account, maxDeposit }) ] })` intercepts `402` challenges and pays them transparently. Also ships a CLI (`npm install -g mppx`). [npm](https://www.npmjs.com/package/mppx). | | **Partial result** | A successful (`200 OK`) response where the upstream stack hit a transient issue mid-analysis. Carries `degraded: true` (field name kept for compat), `degradationReason`, `confidence: 'low'`, and a minimal-but-typed body. Branch on `degraded` *first*, before reading any other field - see [Core concepts → Partial results](/concepts/#partial-results). | | **Pre-flight** | Phase 1 of the [analysis pipeline](/concepts/#the-analysis-pipeline). Parallel fetch of trace, labels, source code, and token transfers before any LLM iteration. | | **Protocol** | Snake_case slug in `ExplanationResult` (e.g. `uniswap_v3`, `aave_v3`). `null` when unknown. | | **Session** | One of two MPP intents (the other is `charge`). The customer opens a payment channel with a `maxDeposit` and signs cumulative vouchers per request - like a bar tab. oculr uses session intents inbound, in the metered variant: vouchers are signed incrementally *during* an analysis as cost accrues, not once per request. See **Metered pricing** above. | | **`SKILL.md`** | Prose entry point for autonomous agents at `https://mpp.oculr.xyz/SKILL.md`. The agent fetches it once at startup, learns the API surface, and dispatches with `mppx`. See [Use as an agent → Skill mode](/quickstart/agent#skill-mode). Contrast with **`tool-spec.json`** below. | | **SSE** | Server-Sent Events. How sync `POST /explain` streams progress and delivers the final `result` - and the transport that carries the metered payment (vouchers renew as cost accrues). Sub-agents preferring plain fetch should use the async path. | | **Tempo** | The chain on which MPP payments settle. Settlement currency is USDC.e at contract `0x20C000000000000000000000b9537d11c60E8b50`. See [tempo.xyz](https://tempo.xyz). | | **`tool-spec.json`** | Typed Anthropic + OpenAI tool-call schemas served at `https://mpp.oculr.xyz/tool-spec.json`. Sub-agents drop the array straight into their LLM's tool-use API. Eliminates the "model parses markdown" class of integration bugs that pure-prose skill mode can produce. | | **txHash** | 32-byte EVM transaction identifier (works on any supported chain - 50+ EVM mainnets; oculr auto-detects). Must match `^0x[0-9a-fA-F]{64}$`. | | **txType** | Closed enum on `ExplanationResult`: `swap` \| `transfer` \| `exploit` \| `liquidation` \| `bridge` \| `deployment` \| `mev` \| `governance` \| `routine_infra` \| `approval` \| `stake` \| `other`. Parent agents should route on this. | | **Voucher** | Off-chain signed payment artifact in MPP session intents. Each request increments the cumulative amount on the channel; the payee redeems the highest voucher on-chain. | | **x402** | HTTP extension for machine-to-machine payments via `402 Payment Required` challenge/response. oculr implements this through `mppx`. Spec at [paymentauth.org](https://paymentauth.org). | ## Related - [Core concepts](/concepts/) - pipeline, confidence levels, sync vs async - [Endpoints reference](/reference/endpoints) - full API schema - [Use as an agent](/quickstart/agent) - `SKILL.md` vs `tool-spec.json` decision guidance --- ## API / Reference ### [Endpoints overview](https://oculr.xyz/docs/reference/endpoints) # Endpoints > Complete API reference for the public oculr endpoints. Each endpoint has its own page with request, response, errors, and a worked example. **Base URL:** `https://mpp.oculr.xyz` All `/explain*` endpoints require MPP/x402 payment. Discovery surfaces (`/openapi.json`, `/tool-spec.json`, `/SKILL.md`, `/llms.txt`, `/health`) are free to fetch. ## Endpoints | Method | Path | Purpose | Page | |---|---|---|---| | `POST` | `/explain` | Synchronous analysis over metered SSE - the final event is the result | [POST /explain](/reference/endpoints/explain) | | `POST` | `/explain/async` | Non-blocking - returns a `jobId` immediately | [POST /explain/async](/reference/endpoints/explain-async) | | `GET` | `/result/:jobId` | Poll an async job for its result | [GET /result/:jobId](/reference/endpoints/result) | | `GET` | `/health` | Service health probe (free) | [GET /health](/reference/endpoints/health) | | `GET` | `/openapi.json` | OpenAPI 3.1 spec (free) | [Discovery](/reference/endpoints/discovery) | | `GET` | `/tool-spec.json` | Typed Anthropic + OpenAI tool-use schemas (free) | [Discovery](/reference/endpoints/discovery) | | `GET` | `/SKILL.md` | Prose entry point for agents (free) | [Discovery](/reference/endpoints/discovery) | | `GET` | `/llms.txt` | Discovery index for LLM crawlers (free) | [Discovery](/reference/endpoints/discovery) | ## Response headers Every response includes: | Header | Value | |---|---| | `X-Oculr-Version` | `1` | | `X-Oculr-Cost-Model` | `mpp-x402` | | `X-Oculr-Result-Quality` | `complete` \| `partial` | | `X-Oculr-Degradation-Reason` | Only on partial: `trace-fetch-failure` \| `agent-loop-failure` \| `sse-stream-fatal` | | `Link` | `; rel="describedby", ; rel="describedby", ; rel="describedby"` (RFC 5988) | ## The `ExplanationResult` schema Returned by [`POST /explain`](/reference/endpoints/explain) and by [`GET /result/:jobId`](/reference/endpoints/result) when the job completes. Documented in detail on the [`POST /explain`](/reference/endpoints/explain#response) page. ## Related - [Use as an agent](/quickstart/agent) - sub-agent integration guide - [Analyze a transaction](/guides/analyzing-transactions) - worked example with the result schema explained - [Core concepts](/concepts/) - pipeline, confidence levels, sync vs async --- ### [POST /explain](https://oculr.xyz/docs/reference/endpoints/explain) # POST /explain > Synchronous transaction analysis over SSE - streams progress events, ends with the full result. For plain-fetch callers, use [`POST /explain/async`](/reference/endpoints/explain-async) and poll. **URL:** `https://mpp.oculr.xyz/explain` **Auth:** MPP/x402 metered session - requires `tempo.session.manager().sse()` from [`mppx`](https://www.npmjs.com/package/mppx), which signs voucher increments as the analysis accrues cost ([metered pricing](/pricing)). A plain JSON request (no SSE) returns `402` with `code: 'use_metered_sse'`. ## Request ### Body | Field | Type | Required | Description | |---|---|---|---| | `txHash` | `string` | yes | EVM tx hash matching `^0x[0-9a-fA-F]{64}$`. Works on any supported chain (50+ EVM mainnets); oculr auto-detects the chain. | | `chainId` | `number` | no | EIP-155 chain ID. When provided, skips multi-chain auto-detection. Must be one of the supported chains. | | `context` | `string` | no | Caller intent passed to the analysis agent. Improves accuracy on ambiguous transactions. | | `model` | `string` | no | `claude-opus-4-8` \| `claude-opus-4-7` \| `claude-sonnet-4-6` \| `claude-haiku-4-5-20251001`. Defaults to a server-side default (usually `claude-opus-4-8`). | | `report` | `boolean` | no | If `true`, the response body is **HTML** (`Content-Type: text/html`) instead of JSON. In SSE mode the rendered HTML arrives via a separate `report` event. | ### Headers | Header | Value | Notes | |---|---|---| | `Content-Type` | `application/json` | Required. | | `Accept` | `text/event-stream` | Required for paid calls - sync analysis is metered over SSE. See [SSE streaming](#sse-streaming-web-uis). | ## Response ### `200 OK` - `ExplanationResult` Delivered as the final `{ "type": "result", … }` SSE event; the earlier events are progress. The result fields: ```json { "txHash": "0x…", "chain": "ethereum-mainnet", "chainName": "Ethereum", "status": "success", "summary": "Uniswap V3 swap: 1,000 USDC → 0.42 WETH via the 0.05% fee pool", "steps": ["Sender called exactInputSingle on Uniswap V3 Router", "..."], "risks": [], "protocol": "uniswap_v3", "txType": "swap", "confidence": "high", "usdValue": 1000.00, "addresses": [ { "address": "0x…", "label": "Uniswap V3 Router", "role": "router" } ], "contracts": [ { "address": "0x…", "name": "UniswapV3Pool", "purpose": "0.05% fee USDC/WETH pool" } ], "costs": { "llms": 0.33, "dataCollection": 0.03, "codeExecution": 0.01, "other": 0.00, "totalUsd": 0.37 } } ``` ### Response fields **Always present** | Field | Type | Notes | |---|---|---| | `txHash` | `string` | Echoes the request. | | `status` | `"success"` \| `"reverted"` | See [Analyze a transaction → status](/guides/analyzing-transactions#enum-values). | | `summary` | `string` | One-line plain-English explanation. | | `steps` | `string[]` | Ordered narrative of what the transaction did. | | `risks` | `string[]` | Empty when no risks flagged. | | `protocol` | `string \| null` | Snake-case slug, e.g. `uniswap_v3`. | | `txType` | enum \| null | See [Analyze a transaction → txType](/guides/analyzing-transactions#enum-values). | | `confidence` | `"high"` \| `"medium"` \| `"low"` | Branch on this. | | `addresses` | `Array<{ address, label, role }>` | Resolved with labels. | | `contracts` | `Array<{ address, name, purpose }>` | Code at the contract addresses. | | `usdValue` | `number \| null` | Primary-action USD value. | **Often present** | Field | Type | Notes | |---|---|---| | `chain` | string | Auto-detected slug, e.g. `ethereum-mainnet`. | | `chainName` | string | Human-readable chain name. | | `costs` | object | Category buckets - `llms`, `dataCollection`, `codeExecution`, `other`, `totalUsd`. See [Analyze a transaction → costs](/guides/analyzing-transactions#enum-values). | | `toolCalls` | array | Each tool the agent invoked, with `durationMs`, `ok`, `costUsd`. | **Optional, depending on the transaction** | Field | Type | Meaning | |---|---|---| | `degraded` | boolean | `true` when the upstream stack hit a transient issue mid-analysis and oculr returned a partial result instead of a 5xx. **Always check this first.** | | `degradationReason` | enum | Set when `degraded === true`. One of `'trace-fetch-failure'` \| `'agent-loop-failure'` \| `'sse-stream-fatal'`. | | `analysisModel` | string | The Anthropic model used. Special value `'partial-synthesis'` pairs with `degraded === true`. | | `mermaidDiagram` | string | Mermaid sequence-diagram source, when the call flow is non-trivial. | | `balanceChanges` | array | Per-address signed balance flow with USD values. | | `tokenTransfers` | array | Every ERC-20/721/1155 transfer touched in the trace. | | `prettyTrace` / `prettyTraceMeta` / `traceAnnotations` | array | Annotated call tree used by the web app's **Trace** tab. | | `rawTrace` | object | Raw `CallFrame` from the RPC. | | `htmlReport` | boolean | `true` when the agent called `generate_report` during analysis. | | `txMeta` | object | Block number, timestamp, gas used, gas price, sender, recipient. | | `findings` | array | Structured exploit findings - one per distinct vulnerability. Populated when `txType === 'exploit'`. | ## Examples ### TypeScript ```typescript import { tempo } from 'mppx/client' const session = tempo.session.manager({ account, maxDeposit: '15' }) const stream = await session.sse('https://mpp.oculr.xyz/explain', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' }, body: JSON.stringify({ txHash: '0x4e4b8ed4de38be29e3a7a15e2b14b5d8262e3c5b3f1e9d6a7c8b9e0f1a2d3c4e', model: 'claude-sonnet-4-6', // optional - cheaper than Opus }), }) let result for await (const payload of stream) { const msg = JSON.parse(payload) if (msg.type === 'result') result = msg } ``` ## SSE streaming Each agent step emits its own SSE message - `preflight_status`, `agent_text` (token-by-token reasoning), `tool_call`, `tool_result`, `cost`, then either a final `result` (JSON) or `report` (HTML). The stream is what carries the metered payment: the session client signs a voucher increment as `cost` accrues. Prefer a plain `fetch()` and no stream? Use [`POST /explain/async`](/reference/endpoints/explain-async) - single-event polling, fixed quote per request. ## Errors | Code | When it happens | Body shape | |---|---|---| | `400` | `txHash` is missing or malformed (must be `0x` + 64 hex). | `{ "error": "txHash must be a valid 32-byte hex hash (0x...)" }` | | `402` | No active MPP payment session (standard challenge, handled by the session client) - **or** a paid JSON request without SSE. | Standard MPP/x402 challenge, no `code` field - or `{ "error": "...", "code": "use_metered_sse" }` for JSON-mode requests. | | `500` | Transaction not found on any supported chain, or an internal error not caught by the partial-result path. | `{ "error": "Transaction 0x… not found on any supported chain (…)" }` | | `502` | oculr could not pay an upstream service for this request. Stable contract via the `code` field. | `{ "error": "...", "code": "upstream_payment_unavailable" }` | ### `402` handling If the body carries `code: 'use_metered_sse'`, you sent a plain JSON request - switch to `Accept: text/event-stream` with `tempo.session.manager().sse()`, or use [`POST /explain/async`](/reference/endpoints/explain-async) for plain fetch. Otherwise, a `402` with the session client active usually means your wallet ran out of USDC.e - top up at the [USDC.e contract on Tempo](https://tempo.xyz) (`0x20C000000000000000000000b9537d11c60E8b50`). ### `502 upstream_payment_unavailable` handling The server's outbound payment to one of its upstreams failed. Surface to the user as something like *"the transaction analysis service is temporarily unable to bill its upstreams - try again shortly."* Cap retries at 2: ```typescript const res = await fetch('https://mpp.oculr.xyz/explain/async', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ txHash }), }) if (!res.ok) { const body = await res.json().catch(() => ({})) as { error?: string; code?: string } if (body.code === 'upstream_payment_unavailable') { throw new Error('oculr is temporarily unable to bill its upstreams - try again shortly') } throw new Error(`oculr ${res.status}: ${body.error ?? 'no body'}`) } ``` > Note: most upstream issues don't surface as `500` or `502`. oculr returns `200 OK` with `degraded: true` and a populated `degradationReason` whenever it can produce a usable partial result. See [Core concepts → Partial results](/concepts/#partial-results). ## Related - [POST /explain/async](/reference/endpoints/explain-async) - non-blocking variant - [Analyze a transaction](/guides/analyzing-transactions) - worked example with the result schema explained - [Core concepts → Partial results](/concepts/#partial-results) --- ### [POST /explain/async](https://oculr.xyz/docs/reference/endpoints/explain-async) # POST /explain/async > Non-blocking variant of [`POST /explain`](/reference/endpoints/explain). Returns a `jobId` immediately. Poll [`GET /result/:jobId`](/reference/endpoints/result) for the result. **URL:** `https://mpp.oculr.xyz/explain/async` **Auth:** MPP/x402, fixed quote per request ([pricing](/pricing)) - handled transparently by the standard [`mppx`](https://www.npmjs.com/package/mppx) polyfill; plain `fetch()` works. ## Request ### Body Same as [`POST /explain`](/reference/endpoints/explain#body): | Field | Type | Required | Description | |---|---|---|---| | `txHash` | `string` | yes | EVM tx hash matching `^0x[0-9a-fA-F]{64}$`. | | `chainId` | `number` | no | EIP-155 chain ID. When provided, skips multi-chain auto-detection. | | `context` | `string` | no | Caller intent passed to the analysis agent. | | `model` | `string` | no | `claude-opus-4-8` \| `claude-opus-4-7` \| `claude-sonnet-4-6` \| `claude-haiku-4-5-20251001`. | | `report` | `boolean` | no | If `true`, the completed job also carries a self-contained HTML report. | ### Headers | Header | Value | |---|---| | `Content-Type` | `application/json` | ## Response ### `202 Accepted` ```json { "jobId": "550e8400-e29b-41d4-a716-446655440000", "status": "pending" } ``` | Field | Type | Notes | |---|---|---| | `jobId` | UUID | Pass this to [`GET /result/:jobId`](/reference/endpoints/result). | | `status` | `"pending"` | Always `pending` on creation. Becomes `running` then `complete` (or `error`) over time. | Jobs expire 1 hour after they're created. After expiry, polling `/result/:jobId` returns `404`. ## Examples ### TypeScript ```typescript const { jobId } = await fetch('https://mpp.oculr.xyz/explain/async', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ txHash: '0x…' }), }).then(r => r.json()) ``` ### Shell (`mppx` CLI) ```bash JOB=$(mppx mpp.oculr.xyz/explain/async \ -J '{"txHash":"0x4e4b8ed4…"}' | jq -r '.jobId') echo "Job: $JOB" ``` ## Errors | Code | When it happens | Body | |---|---|---| | `400` | `txHash` is missing or malformed. | `{ "error": "txHash must be a valid 32-byte hex hash (0x...)" }` | | `402` | No active MPP payment session. `mppx` handles this transparently. | Standard MPP/x402 challenge. | | `502` | oculr could not pay an upstream service to start the job. | `{ "error": "...", "code": "upstream_payment_unavailable" }` | See [`POST /explain` → Errors](/reference/endpoints/explain#errors) for the full handling pattern. ## Related - [GET /result/:jobId](/reference/endpoints/result) - poll for the result - [Use as an agent → async polling pattern](/quickstart/agent#step-4-async-polling-pattern) --- ### [GET /result/:jobId](https://oculr.xyz/docs/reference/endpoints/result) # GET /result/:jobId > Poll for the result of an async job started by [`POST /explain/async`](/reference/endpoints/explain-async). Free to call. **URL:** `https://mpp.oculr.xyz/result/:jobId` **Auth:** None - polling is free. ## Request ### Parameters | Param | Type | Required | Description | |---|---|---|---| | `jobId` | UUID (path) | yes | The `jobId` returned by [`POST /explain/async`](/reference/endpoints/explain-async). | ## Response ### `200 OK` ```json { "jobId": "550e8400-e29b-41d4-a716-446655440000", "status": "complete", "result": { /* ExplanationResult - see POST /explain */ } } ``` | Field | Type | Notes | |---|---|---| | `jobId` | UUID | Echoes the request. | | `status` | `"pending"` \| `"running"` \| `"complete"` \| `"error"` | Transitions in order. | | `result` | `ExplanationResult` | Present only when `status === "complete"`. Schema documented in [`POST /explain` → Response](/reference/endpoints/explain#response). | | `error` | `string` | Present only when `status === "error"`. | | `errorCode` | `string` | Present when the error has a stable code, e.g. `upstream_payment_unavailable`. | | `html` | `string` | Present when the job was started with `report: true` - a self-contained HTML report. | ## Examples ### TypeScript ```typescript const job = await fetch(`https://mpp.oculr.xyz/result/${jobId}`).then(r => r.json()) if (job.status === 'complete') return job.result if (job.status === 'error') throw new Error(job.error) // else still pending or running - poll again ``` ### Shell (`mppx` CLI) ```bash mppx mpp.oculr.xyz/result/$JOB | jq '{status, summary: .result.summary}' ``` ### Polling loop ```typescript while (true) { await new Promise(r => setTimeout(r, 3000)) const job = await fetch(`https://mpp.oculr.xyz/result/${jobId}`).then(r => r.json()) if (job.status === 'complete') return job.result if (job.status === 'error') throw new Error(job.error) } ``` ## Errors | Code | When it happens | Body | |---|---|---| | `404` | Job expired (TTL 1 hour) or `jobId` is unknown. | `{ "error": "Job not found - expired or invalid ID" }` | Errors raised *during* analysis are surfaced via the `status: "error"` response, not via an HTTP error code. Check `job.errorCode === "upstream_payment_unavailable"` for the same outbound-payment failure documented on [`POST /explain`](/reference/endpoints/explain#errors). ## Related - [POST /explain/async](/reference/endpoints/explain-async) - starts the job - [POST /explain](/reference/endpoints/explain) - the synchronous alternative - [Use as an agent → async polling pattern](/quickstart/agent#step-4-async-polling-pattern) --- ### [GET /health](https://oculr.xyz/docs/reference/endpoints/health) # GET /health > Service health probe. Free to call - no MPP payment required. **URL:** `https://mpp.oculr.xyz/health` **Auth:** None. ## Request No parameters, no body. ## Response ### `200 OK` ```json { "status": "ok", "version": "1" } ``` | Field | Type | Notes | |---|---|---| | `status` | `"ok"` | Always `"ok"` when the server responds. Any non-200 means down. | | `version` | string | Current API version. | ## Examples ### Shell ```bash curl -s https://mpp.oculr.xyz/health ``` ### TypeScript ```typescript const { status } = await fetch('https://mpp.oculr.xyz/health').then(r => r.json()) if (status !== 'ok') throw new Error('oculr is down') ``` ## Errors Any non-200 response means the service is unreachable or starting up. No structured error body is returned. ## Related - [Endpoints overview](/reference/endpoints) --- ### [Discovery surfaces](https://oculr.xyz/docs/reference/endpoints/discovery) # Discovery surfaces > Endpoints that describe oculr itself. Call them once at startup to learn the rest of the API. All return `200` and are **free** - no payment required. | Endpoint | Returns | When to use | |---|---|---| | [`GET /openapi.json`](#get-openapijson) | OpenAPI 3.1 spec - full REST schema | Generic OpenAPI tooling, code generators | | [`GET /tool-spec.json`](#get-tool-specjson) | Anthropic + OpenAI tool-call schemas | Sub-agents using LLM tool-use APIs | | [`GET /SKILL.md`](#get-skillmd) | Prose entry point for autonomous agents | Skill-mode integrations | | [`GET /llms.txt`](#get-llmstxt) | Discovery index, one line per doc | LLM crawlers / first fetch | | [`GET /llms-full.txt`](#get-llms-fulltxt) | Concatenated full doc corpus | LLM crawlers wanting one-shot | ## Use from an agent One prompt hands an agent the whole surface - it reads the discovery doc, learns the call shape, and dispatches: :::code-group ```bash [Claude Code] claude -p "Read https://mpp.oculr.xyz/SKILL.md and analyse EVM transaction 0xYOUR_TX_HASH - what happened, any risks?" ``` ```bash [Amp] amp --execute "Read https://mpp.oculr.xyz/SKILL.md and analyse EVM transaction 0xYOUR_TX_HASH - what happened, any risks?" ``` ```bash [Codex CLI] codex exec "Read https://mpp.oculr.xyz/SKILL.md and analyse EVM transaction 0xYOUR_TX_HASH - what happened, any risks?" ``` ::: For a broader working context (the entire docs corpus, not just the task instructions), point the agent at `llms-full.txt` instead: *"Fetch https://mpp.oculr.xyz/llms-full.txt for complete oculr context, then …"*. --- ## `GET /openapi.json` Machine-readable OpenAPI 3.1 definition of every endpoint, schema, and security scheme. Drop into any OpenAPI client generator (`openapi-typescript-codegen`, `openapi-generator`, etc.). ```bash curl https://mpp.oculr.xyz/openapi.json | jq '.info' ``` ```json { "title": "oculr", "version": "1" } ``` --- ## `GET /tool-spec.json` Typed Anthropic + OpenAI tool-call schemas. Drop the appropriate array straight into your LLM's tool-use call - no markdown parsing. ### Shape ```json { "version": 1, "baseUrl": "https://mpp.oculr.xyz", "auth": "mpp-x402", "anthropic": [ /* 3 tools - Anthropic Messages format */ ], "openai": [ /* 3 tools - OpenAI Chat Completions format */ ], "endpoints": { "explain_transaction": { "method": "POST", "path": "/explain" }, "start_explain_job": { "method": "POST", "path": "/explain/async" }, "get_job_result": { "method": "GET", "path": "/result/{jobId}" } }, "skillUrl": "https://mpp.oculr.xyz/SKILL.md", "openapiUrl": "https://mpp.oculr.xyz/openapi.json" } ``` ### Tools exposed | Tool | Wraps | Blocking? | |---|---|---| | `explain_transaction` | [`POST /explain`](/reference/endpoints/explain) | Yes | | `start_explain_job` | [`POST /explain/async`](/reference/endpoints/explain-async) | No | | `get_job_result` | [`GET /result/:jobId`](/reference/endpoints/result) | No | ### Use it ```typescript const spec = await fetch('https://mpp.oculr.xyz/tool-spec.json').then(r => r.json()) const response = await anthropic.messages.create({ model: 'claude-opus-4-8', tools: spec.anthropic, messages: [{ role: 'user', content: 'Analyse tx 0x4e4b8ed4…' }], }) ``` Full integration walkthrough at [Use as an agent → Tool-use mode](/quickstart/agent#tool-use-mode). --- ## `GET /SKILL.md` Prose entry point for autonomous coding agents. The agent fetches it once at startup, learns the call shape, and dispatches with an MPP client. Use cases: - Interactive CLIs (Claude Code, Amp, Codex CLI) where a human asks an agent to look at a transaction. - Persistent skill installation - save it into your agent's skills directory (for Claude Code, `~/.claude/skills/oculr/SKILL.md`). See [Use as an agent → Skill mode](/quickstart/agent#skill-mode). --- ## `GET /llms.txt` Compact discovery index - one line per doc, designed for LLM crawlers and first-fetch context. ```bash curl https://mpp.oculr.xyz/llms.txt ``` --- ## `GET /llms-full.txt` Concatenated full doc corpus. Use when an LLM crawler wants the entire documentation surface in one fetch. ```bash curl https://mpp.oculr.xyz/llms-full.txt ``` ## Related - [Endpoints overview](/reference/endpoints) - [Use as an agent](/quickstart/agent) - `SKILL.md` vs `tool-spec.json` decision guidance --- ## Agent entry point ### [SKILL.md](https://oculr.xyz/SKILL.md) # oculr Skill > How to use oculr from an autonomous coding agent. Read this file once, then perform any of the tasks below. > **If you're a sub-agent invoked by a parent agent**, prefer the typed contract at over this markdown file. It emits Anthropic + OpenAI tool-use schemas for the same endpoints. See [Use as an agent](https://oculr.xyz/docs/quickstart/agent) for the sub-agent integration guide. ## Canonical hosts | URL | Purpose | |---|---| | `https://mpp.oculr.xyz` | **API canonical host** - POST `/explain`, `/explain/async`, GET `/result/:jobId`, `/openapi.json`, `/tool-spec.json`, `/SKILL.md`, `/llms*.txt`, `/health` | | `https://oculr.xyz` | Docs site (`/docs/*`) and human-facing web app (`/app`) | If you only remember one URL, remember `mpp.oculr.xyz`. The `mpp.` subdomain signals the API expects MPP/x402 payment; the apex domain serves the human surface. ## Result-quality contract (subagent-critical) **Every `/explain*` 200 response carries a `degraded` flag.** When the upstream stack hits a transient issue mid-pipeline (QuickNode down, Anthropic timeout, paymentauth 500), the server synthesises a partial-but-typed result rather than 5xx'ing - a parent agent should never have to handle an empty body. ```jsonc { "txHash": "0x…", "degraded": true, // <- branch on this "degradationReason": "trace-fetch-failure", // | "agent-loop-failure" | "sse-stream-fatal" "analysisModel": "partial-synthesis", "confidence": "low", "summary": "**Partial result - …**", "risks": ["partial-synthesis: …"], // …minimal-but-valid rest of ExplanationResult } ``` Response headers carry the same signal - sufficient to branch without parsing the body: ``` X-Oculr-Result-Quality: complete | partial X-Oculr-Degradation-Reason: trace-fetch-failure | agent-loop-failure | sse-stream-fatal (only on partial) Link: ; rel="describedby", ; rel="describedby", ; rel="describedby" ``` **Retry policy**: - `trace-fetch-failure` → safe to retry after 30s (RPC outage usually transient). - `agent-loop-failure` → safe to retry once; second failure usually means an Anthropic-side issue, escalate. - `sse-stream-fatal` → retry via `POST /explain/async` + poll (no stream) - the stream itself is what broke. ## When to use this skill Use this skill when you need to: - Understand what an EVM transaction did on any of 50+ supported EVM mainnets - Ethereum, Base, Arbitrum, Optimism, Polygon, BNB Chain, Avalanche, and many more (chain auto-detected from tx hash) - Identify protocol, actors, risks, and USD value of a transaction - Classify a transaction type (MEV, exploit, routine DeFi, etc.) - Get structured JSON data about a transaction for further processing ## Setup Install the MPP client and point it at a wallet holding USDC.e on Tempo (contract `0x20C000000000000000000000b9537d11c60E8b50`): ```bash npm install mppx export WALLET_PRIVATE_KEY=0xYOUR_PRIVATE_KEY ``` Initialise `mppx` once before making any requests: ```typescript import { Mppx, tempo } from 'mppx/client' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as `0x${string}`) // `maxDeposit` is REQUIRED - without it the first paid call throws "No deposit // or maxDeposit configured". `'5'` is $5 USDC.e, ~6-10 oculr analyses. // oculr uses the TIP-1034 precompile session protocol (`tempo.session`). // The standard `tempo()` polyfill below auto-pays on 402 - no custom session // management needed for the async endpoint. await Mppx.create({ methods: [tempo({ account, maxDeposit: '5' })] }) // All subsequent fetch() calls auto-pay on 402 ``` > **Which endpoint?** `POST /explain/async` + poll is the agent default - plain `fetch()` with the setup above, fixed quote per request. Sync `POST /explain` is a metered SSE stream and needs `tempo.session.manager().sse()` (recipe below); a plain JSON `POST /explain` returns `402` with `code: "use_metered_sse"`. ## Core tasks ### Analyze a transaction (async - agent default) **Step 1 - start the job:** ```typescript const { jobId } = await fetch('https://mpp.oculr.xyz/explain/async', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ txHash: '0xTX_HASH_HERE' }), }).then(r => r.json()) ``` **Step 2 - poll until complete (`GET /result/:jobId` is free):** ```typescript while (true) { await new Promise(r => setTimeout(r, 3000)) const job = await fetch(`https://mpp.oculr.xyz/result/${jobId}`).then(r => r.json()) if (job.status === 'complete') return job.result if (job.status === 'error') throw new Error(job.error) } ``` Or with the `mppx` CLI: ```bash mppx mpp.oculr.xyz/explain/async -J '{"txHash":"0xTX_HASH_HERE"}' # → {"jobId":"…"} - then poll with plain curl (free): curl -s https://mpp.oculr.xyz/result/JOB_ID | jq '{status, result: {summary, txType, confidence}}' ``` **Expected output:** on `complete`, `job.result` is an `ExplanationResult` containing `txHash`, `chain` (e.g. `"ethereum-mainnet"`), `chainName`, `status`, `summary`, `steps`, `risks`, `protocol`, `txType`, `confidence`, `usdValue`, `addresses[]`, `contracts[]`, and a `costs` object with category buckets: `llms`, `dataCollection`, `codeExecution`, `other` (fallthrough), and `totalUsd`. **Verification:** `status` field transitions: `pending` → `running` → `complete`. HTTP 200 throughout. On `complete`, `result.txHash` matches your input; `result.status` is `"success"` or `"reverted"`; `result.confidence` is `"high"`, `"medium"`, or `"low"`. Branch on `result.txType` (closed enum: `swap` | `transfer` | `exploit` | `liquidation` | `bridge` | `deployment` | `mev` | `governance` | `routine_infra` | `approval` | `stake` | `other`). **Error handling:** a 502 with `code: "upstream_payment_unavailable"` means oculr could not pay an upstream service - surface to the user and cap retries at 2. --- ### Analyze a transaction (sync, metered SSE) One call, streamed progress, exact metered price. Requires the session manager (not the `Mppx.create` polyfill): ```typescript import { tempo } from 'mppx/client' const session = tempo.session.manager({ account, maxDeposit: '15' }) const stream = await session.sse('https://mpp.oculr.xyz/explain', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' }, body: JSON.stringify({ txHash: '0xTX_HASH_HERE' }), }) for await (const payload of stream) { const msg = JSON.parse(payload) if (msg.type === 'result') return msg // the full ExplanationResult // other events: preflight_status, agent_text, tool_call, tool_result, cost } ``` **Verification:** same `ExplanationResult` checks as the async path. --- ### Analyze a transaction with context hint Improve analysis by passing your intent (e.g. "check if this is an exploit") - works on both endpoints: ```typescript body: JSON.stringify({ txHash: '0xTX_HASH_HERE', context: 'check if this is a reentrancy exploit', }) ``` **Verification:** Same as above. The `context` field improves accuracy for exploit/MEV detection. --- ### Pick a cheaper model Pass `model` on the request body. Defaults to a server-side default (usually Opus). ```typescript body: JSON.stringify({ txHash: '0x…', model: 'claude-sonnet-4-6', // or 'claude-haiku-4-5-20251001' }) ``` Sonnet is ~3-5× cheaper than Opus; Haiku is ~10× cheaper. Use Opus when accuracy matters (exploit triage, complex MEV); Haiku for routine transfer/swap classification. --- ### Check service health ```typescript const { status } = await fetch('https://mpp.oculr.xyz/health').then(r => r.json()) // → { "status": "ok", "version": "1" } ``` **Verification:** HTTP 200 and `status === "ok"`. --- ### Fetch the OpenAPI spec ```typescript const spec = await fetch('https://mpp.oculr.xyz/openapi.json').then(r => r.json()) // → spec.info.title === "oculr", spec.info.version === "1" ``` ## Verification After any analysis, confirm success by checking: 1. HTTP status is `200` (or `202` for async start) 2. `result.txHash` matches your input 3. `result.status` is `"success"` or `"reverted"` (not undefined) 4. `result.confidence` is `"high"`, `"medium"`, or `"low"` If any check fails, see **Troubleshooting** below. ## Troubleshooting | Symptom | Cause | Fix | |---------|-------|-----| | HTTP 402 (standard MPP body, has `accepts[]`) | No MPP payment session | Install and initialize `mppx` with a funded wallet | | HTTP 502 with body `{"code":"upstream_payment_unavailable"}` | oculr could not pay one of its upstream services (wallet low, or stale outbound MPP session). Async jobs surface this as `{"status":"error","errorCode":"upstream_payment_unavailable"}` on `GET /result/:jobId` | Branch on `.code` / `.errorCode`. Surface to user as "service temporarily unable to bill upstreams; try again shortly" - do not retry more than 2× | | HTTP 400 | Invalid `txHash` | Ensure txHash is `0x` + 64 hex chars (32 bytes) | | HTTP 404 on `/result/:jobId` | Job expired | Jobs expire after 1 hour - re-submit | | HTTP 500 | Tx not found on any supported chain, or upstream error | Verify the tx exists on a supported chain - oculr covers 50+ EVM mainnets, auto-detected (no `chain` field in request needed). A "not found" usually means the chain isn't supported yet. | | `confidence: "low"` | Unverified contracts or unusual trace | Expected for novel protocols - summary still returned | | `mppx` not found | Not installed | Run `npm install mppx` | | `InsufficientBalance` from `mppx` | Wallet doesn't hold enough USDC.e on Tempo | Top up wallet at USDC.e contract `0x20C000000000000000000000b9537d11c60E8b50`. Make sure `maxDeposit` ≤ wallet balance | ## Pricing Metered: `charge = upstream cost × 1.20 + $0.02`, settled in $0.01 voucher increments as the analysis runs. Typical analyses on the default model (`claude-opus-4-8`) cost **$0.45-$0.90**; simple transfers less, complex incident investigations more. Cost scales with transaction complexity, and passing a cheaper `model` (`claude-sonnet-4-6`, `claude-haiku-4-5-20251001`) cuts the dominant LLM component substantially. Budget guidance for agents: a `maxDeposit` of `'5'` (USD) covers ~6-10 typical analyses in one session. The exact cost is in `result.costs.totalUsd`. All 11 cost buckets are always present in the `costs` object. Full pricing details: https://oculr.xyz/docs/pricing ## Reference - [Endpoints reference](https://oculr.xyz/docs/reference/endpoints) - full request/response schemas - [Concepts](https://oculr.xyz/docs/concepts/) - how the analysis pipeline works - [OpenAPI spec](https://mpp.oculr.xyz/openapi.json) - machine-readable API definition