# 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