Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

POST /explain

Synchronous transaction analysis over SSE - streams progress events, ends with the full result. For plain-fetch callers, use POST /explain/async and poll.

URL: https://mpp.oculr.xyz/explain

Auth: MPP/x402 metered session - requires tempo.session.manager().sse() from mppx, which signs voucher increments as the analysis accrues cost (metered pricing). A plain JSON request (no SSE) returns 402 with code: 'use_metered_sse'.

Request

Body

FieldTypeRequiredDescription
txHashstringyesEVM tx hash matching ^0x[0-9a-fA-F]{64}$. Works on any supported chain (50+ EVM mainnets); oculr auto-detects the chain.
chainIdnumbernoEIP-155 chain ID. When provided, skips multi-chain auto-detection. Must be one of the supported chains.
contextstringnoCaller intent passed to the analysis agent. Improves accuracy on ambiguous transactions.
modelstringnoclaude-opus-5 | claude-opus-4-8 | claude-opus-4-7 | claude-sonnet-4-6 | claude-haiku-4-5-20251001. Defaults to a server-side default (claude-opus-5); GET /tool-spec.json publishes the live value in defaultModel.
reportbooleannoIf 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

HeaderValueNotes
Content-Typeapplication/jsonRequired.
Accepttext/event-streamRequired for paid calls - sync analysis is metered over SSE. See SSE streaming.

Response

200 OK - ExplanationResult

Delivered as the final { "type": "result", … } SSE event; the earlier events are progress. The example below is abbreviated - the field tables that follow list every field, and Analyze a transaction → Example result shows a response with all of them populated:

{
  "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", "description": "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 (partial results populate these with empty or placeholder values rather than omitting them)

FieldTypeNotes
txHashstringEchoes the request.
chainstringAuto-detected slug, e.g. ethereum-mainnet.
chainNamestringHuman-readable chain name.
explorerBasestringBlock-explorer origin for the detected chain (no trailing slash) - build links as ${explorerBase}/tx/<hash>.
status"success" | "reverted"See Analyze a transaction → status.
analysisModelstringThe model that produced the result. Special value 'partial-synthesis' marks a partial result.
summarystringOne-line plain-English explanation.
stepsstring[]Ordered narrative of what the transaction did.
risksstring[]Empty when no risks flagged.
protocolstring | nullSnake-case slug, e.g. uniswap_v3.
txTypeenum | nullSee Analyze a transaction → txType.
confidence"high" | "medium" | "low"Branch on this.
addressesArray<{ address, label, role }>Resolved with labels.
contractsArray<{ address, name, description }>Code at the contract addresses.
usdValuenumber | nullPrimary-action USD value.
costsobjectCategory buckets - llms, dataCollection, codeExecution, other, totalUsd. See Analyze a transaction → costs.
toolCallsarrayEach tool the agent invoked, with durationMs, ok, costUsd.
prettyTracearrayAnnotated call tree used by the web app's Trace tab.
rawTraceobject | nullRaw CallFrame from the RPC. null when the trace was compressed for transport (see prettyTraceMeta).
txMetaobjectBlock number, timestamp, gas used, gas price, sender, recipient.
tokenTransfersarrayEvery ERC-20/721/1155 transfer touched in the trace.
mermaidDiagramstringMermaid sequence-diagram source, rendered by the web app's Flow tab.
htmlReportbooleantrue when the agent called generate_report during analysis.
Optional, depending on the transaction
FieldTypeMeaning
skillsUsedstring[]Analysis skills/playbooks the agent engaged this run.
balanceChangesarrayPer-address signed balance flow with USD values. Omitted when no balance flow could be computed.
traceAnnotationsobjectAI comments keyed by trace-node index, when the agent annotated the call tree.
prettyTraceMetaobjectSet when a pathological trace was compressed for transport - original vs kept node counts plus collapsed-loop markers.
findingsarrayStructured exploit findings - one per distinct vulnerability. Populated when txType === 'exploit'.
nonFindingsstring[]Things the analysis considered and ruled out - real observations that are not the cause of this transaction. Absent when there is nothing to report.

Examples

TypeScript

import { tempo } from 'mppx/client'
 
const session = tempo.session.manager({ account, maxDeposit: '32' })
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_start, preflight_done, preflight_status, iteration, agent_text (token-by-token reasoning), tool_call, tool_result, skill_call, tokens, complete, then either a final result (JSON) or report (HTML); fatal failures arrive as an error frame. The stream is what carries the metered payment: the session client signs voucher increments as cost accrues.

Prefer a plain fetch() and no stream? Use POST /explain/async - single-event polling, metered to the same total price.

Errors

CodeWhen it happensBody shape
400txHash is missing or malformed (must be 0x + 64 hex).{ "error": "txHash must be a valid 32-byte hex hash (0x...)" }
402No 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.
500An internal error not caught by the partial-result path. On the SSE path, a transaction not found on any supported chain arrives as an SSE error event (or a partial result) rather than an HTTP status.{ "error": "Transaction 0x… not found on any supported chain (…)" }
502Self-hosted deployments only. oculr could not pay an upstream service for this request. The hosted API never returns this status: the only branch that puts it on the wire is the unmetered blocking-JSON one, and if (!DEV_MODE) return 402 use_metered_sse stands in front of it - so a 502 reaches you only on a deployment running PRECOG_DEV_MODE=true. On the metered SSE path the same condition normally degrades to a partial result inside an HTTP 200; see below.{ "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 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 (0x20C000000000000000000000b9537d11c60E8b50).

upstream_payment_unavailable handling

The server's outbound payment to one of its upstreams failed. Your payment is fine, so do not retry it as a payment failure. 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.

One condition, three shapes - and on the hosted API the HTTP status is the wrong thing to branch on. Which shape you get depends on the rail:

RailWhat actually arrivesBranch on
Metered SSE POST /explainHTTP 200 (the status was sent before the analysis ran). Mid-analysis the condition degrades to a partial result frame (confidence: "low", summary starting "Partial result") - that partial IS the upstream-payment signal on this rail. The stream's last-resort fatal frame {"type":"error","code":"internal_error","message":"…"} today always carries internal_error; upstream_payment_unavailable is part of the frame's code enum (shared with ErrorBody) but is currently never emitted on it.the partial-result markers (confidence / summary), not the frame's code
POST /explain/asyncHTTP 202 at submit, then GET /result/:jobId answers HTTP 200 with {"status":"error","errorCode":"upstream_payment_unavailable"}.errorCode
Blocking JSON POST /explain - self-hosted PRECOG_DEV_MODE=true onlyHTTP 502 with {"error":"...","code":"upstream_payment_unavailable"}.code

A client that branches only on res.status === 502 therefore never fires against the hosted API. The async pattern:

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())
 
while (true) {
  await new Promise(r => setTimeout(r, 5000))
  // 200 even when the analysis failed: the status describes the poll, not the job.
  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') {
    if (job.errorCode === 'upstream_payment_unavailable') {
      throw new Error('oculr is temporarily unable to bill its upstreams - try again shortly')
    }
    throw new Error(job.error)
  }
}

Note: most upstream issues don't surface as 500 or 502. oculr returns 200 OK with confidence: 'low' and a summary that starts with "Partial result" whenever it can produce a usable partial result. See Core concepts → Partial results.

Related