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. 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.
With an agent
One-line prompt for a coding agent:
claude -p "Use the oculr MPP at https://mpp.oculr.xyz/SKILL.md to analyse EVM transaction 0xYOUR_TX_HASH on Ethereum - 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 (metered to the same total as the sync stream: $0.01 at submit, the rest auto-paid by your polls as the analysis accrues it; the sync SSE alternative is in Call the oculr MPP):
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: '32' })] })
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, 5000))
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)
mppx https://mpp.oculr.xyz/explain/async \
-J '{"txHash":"0x4e4b8ed4de38be29e3a7a15e2b14b5d8262e3c5b3f1e9d6a7c8b9e0f1a2d3c4e"}'
# → {"jobId":"…"} - poll with mppx: each poll collects the cost accrued so far,
# and the first poll after the job finishes charges the true-up.
mppx https://mpp.oculr.xyz/result/JOB_ID | jq '{status, result: {summary, txType, confidence, risks}}'mppx handles the 402 payment challenges automatically. Bare curl against a paid endpoint will return the challenge body and stop - so every poll needs an MPP client. Stop polling for 90 seconds and the analysis is aborted, leaving a partial result.
Async pattern for batch processing
JOB=$(mppx https://mpp.oculr.xyz/explain/async \
-J '{"txHash":"0x…"}' | jq -r '.jobId')
# Poll until done.
while true; do
S=$(mppx https://mpp.oculr.xyz/result/$JOB | jq -r '.status')
[ "$S" = "complete" ] && break
[ "$S" = "error" ] && { echo "Job failed"; exit 1; }
sleep 5
done
mppx https://mpp.oculr.xyz/result/$JOB | jq -r '.result.summary'In the web app
Paste your hash into 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
You always receive the full ExplanationResult JSON - including the annotated call tree, per-address balance flow, and the Mermaid diagram source. It arrives as the final { "type": "result", … } SSE event on the sync path, or as job.result from GET /result/:jobId on the async path. (A plain blocking POST /explain without SSE is not paid-accessible - it returns 402 with code: 'use_metered_sse'.)
A Uniswap V3 swap on Ethereum mainnet, with every field present (long values abbreviated with …; arrays trimmed to one or two entries):
{
"txHash": "0x4e4b…",
"chain": "ethereum-mainnet",
"chainName": "Ethereum",
"explorerBase": "https://etherscan.io",
"status": "success",
"analysisModel": "claude-opus-5",
"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", "description": "0.05% fee USDC/WETH pool" }
],
"costs": {
"llms": 0.33,
"dataCollection": 0.03,
"codeExecution": 0.01,
"other": 0.00,
"totalUsd": 0.37
},
"toolCalls": [
{ "tool": "fetch_transaction_trace", "durationMs": 1840, "ok": true, "costUsd": 0.003 },
{ "tool": "get_token_prices", "durationMs": 620, "ok": true, "costUsd": 0.001 }
],
"skillsUsed": [],
"txMeta": {
"from": "0x…",
"to": "0x…",
"valueWei": "0x0",
"blockNumber": 19876543,
"blockTimestamp": 1719834000,
"transactionIndex": 42,
"gasUsed": 184523,
"gasPrice": "0x77359400"
},
"tokenTransfers": [
{ "type": "erc20", "from": "0x…", "to": "0x…", "tokenName": "USD Coin", "tokenSymbol": "USDC", "tokenAddress": "0x…", "total": "1000000000", "decimals": "6" },
{ "type": "erc20", "from": "0x…", "to": "0x…", "tokenName": "Wrapped Ether", "tokenSymbol": "WETH", "tokenAddress": "0x…", "total": "420000000000000000", "decimals": "18" }
],
"balanceChanges": [
{
"address": "0x…",
"label": "",
"role": "sender",
"isSender": true,
"tokens": [
{ "tokenAddress": "0x…", "tokenSymbol": "USDC", "tokenId": null, "balance": "-1000.00", "rawSignedAmount": "-1000000000", "isNFT": false, "priceUsd": 1.00, "valueUsd": -1000.00 },
{ "tokenAddress": "0x…", "tokenSymbol": "WETH", "tokenId": null, "balance": "+0.42", "rawSignedAmount": "420000000000000000", "isNFT": false, "priceUsd": 2380.95, "valueUsd": 1000.00 }
],
"totalUsd": 0.00
}
],
"mermaidDiagram": "sequenceDiagram\n Sender->>Router: exactInputSingle(USDC→WETH)\n …",
"prettyTrace": [
{ "index": 0, "depth": 0, "type": "CALL", "from": "0x…", "fromLabel": "Sender", "to": "0x…", "toLabel": "Uniswap V3 Router", "selector": "0x414bf389", "functionName": "exactInputSingle", "functionSignature": "exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))", "input": "0x414bf389…", "output": "0x…", "valueWei": "0x0", "valueEth": "0", "gasUsed": 184523, "eventSignature": null, "decodedArgs": [ { "name": "tokenIn", "type": "address", "value": "0x…" } ] }
],
"rawTrace": { "type": "CALL", "from": "0x…", "to": "0x…", "gas": "0x4c4b40", "gasUsed": "0x2d0cb", "input": "0x414bf389…", "calls": [] },
"htmlReport": false
}Every field above is present on every result (balanceChanges being the one exception - see below) - partial results populate them with empty or placeholder values rather than omitting them. prettyTrace and traceAnnotations power the web app's Trace tab; mermaidDiagram powers the Flow tab.
Fields that appear only in specific situations:
traceAnnotations- AI comments keyed by trace-node index, when the agent annotated the call tree.prettyTraceMeta- set when a pathological trace was compressed for transport (shows original vs kept node counts);rawTraceisnullin that case.balanceChanges- omitted when no balance flow could be computed for the transaction.findings- structured exploit findings (one entry per distinct vulnerability), populated whentxType === 'exploit'.
Full schema in POST /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.
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
Check for partial results first. If the summary starts with "Partial result" (and confidence is 'low'), the upstream stack hit a transient issue mid-analysis and you're looking at an incomplete body. Don't trust it 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 - full request/response schemas
- Core concepts - how the analysis pipeline works
- Use as an agent - agent-loop integration