{"version":1,"baseUrl":"https://mpp.oculr.xyz","auth":"mpp-x402","paymentChallenge":{"status":402,"contentType":"application/problem+json","detect":"HTTP status 402 — the body is an RFC 9457 problem document, NOT an x402 envelope: there is no `accepts` array and no `x402Version`. Payment terms live in the WWW-Authenticate header; the base64url `request` param decodes to JSON { amount, currency, recipient, suggestedDeposit, unitType, methodDetails{ chainId, escrowContract, operator, sessionProtocol } }. `amount` and `suggestedDeposit` are integer strings in the BASE UNITS of `currency` (6 dp) — \"10000\" is $0.01 and \"16000000\" is $16.00; see the `amountEncoding` block for the arithmetic, and do not read the scale off `unitType`. Never mutate a challenge — it is HMAC-bound. Use an mppx client (Mppx.create / tempo.session.manager()) and it is handled for you. One exception: a plain-JSON POST /explain returns 402 with Content-Type application/json and body { error, code: \"use_metered_sse\" } - a transport redirect, not a separate bill. Distinguish it by Content-Type or by the code field, never by header presence: unauthenticated calls carry the ordinary channel-open WWW-Authenticate challenge on it (the same session the payable rails bill against) while credentialed retries get none. Switch to Accept: text/event-stream or use start_explain_job; do not keep paying this response.","bodyFields":["type","title","status","detail","hint","challengeId"],"header":"WWW-Authenticate","headerParams":["id","realm","method","intent","request","description","expires"]},"amountEncoding":{"appliesTo":["amount","suggestedDeposit"],"units":"currency-base-units","decimals":6,"toUsd":"Integer strings in the BASE UNITS of the challenge's own `currency` token (USDC.e on Tempo, 6 decimals). Divide by 10^6 (1000000) to get USD. They are NOT dollars and NOT cents — do not infer the scale from `unitType`, which describes the metered unit, not the currency.","examples":[{"field":"amount","wire":"10000","usd":"$0.01"},{"field":"suggestedDeposit","wire":"16000000","usd":"$16.00"}],"unitTypeMeaning":"`unitType` names the unit that `amount` is the PRICE OF (\"amount per unitType\"). It is free-form in MPP and carries no currency information whatsoever. Read the scale from `currency` + its decimals, never from this field.","unitTypeByEndpoint":{"POST /explain":"\"tick\" — metered SSE: amount is the price of one $0.01 tick, charged repeatedly as cost accrues.","POST /explain/async":"\"request\" — one-shot: amount is the price of the submit request.","POST /session/deposit":"\"request\" — one-shot probe whose only purpose is to hand you a challenge to open a channel with.","GET /explain/run/{requestId}":"\"request\" — one-shot: amount is the outstanding balance of that run.","_note":"The values differ ON PURPOSE and are not interchangeable: MPP binds `unitType` into its cross-route credential-replay check, and treats the literal \"request\" as \"charge a streamed response once\" rather than \"charge per emitted event\"."}},"pricing":{"currency":"USD","unitUsd":0.01,"suggestedDepositUsd":16,"minDepositUsd":2,"recommendedMaxDepositUsd":32,"depositIsEscrow":"The deposit is ESCROW, not a price. You are charged only what the analysis actually costs; every unspent cent returns to you when the channel closes. Your client's `maxDeposit` and the channel deposit are NOT the same number and must not be set equal. The channel opens at `min(suggestedDeposit, maxDeposit)` = $16, which is sized for one analysis; `maxDeposit` is the ceiling that decides whether that channel can ever grow. Set it to $32. At exactly $16 the channel opens ON its ceiling and is welded shut — both an automatic top-up and an explicit `session.topUp()` throw at the moment growth is needed — so the second heavy run on that channel dies with \"requested voucher amount exceeds local maxDeposit\" AFTER you have paid for the work completed up to that point, and you get nothing for it. A cap below the real cost of a run fails the same way, sooner. Opening below $2.00 is refused outright with `deposit_below_minimum`. Spend the headroom BETWEEN analyses — one `session.topUp()`, or close and reopen — never by letting a single analysis run past the deposit: mppx sizes an automatic top-up to the exact shortfall, so on the $0.01-tick metered stream that means one on-chain transaction per cent, stalling the stream. For scale: $16 clears the most expensive analysis on record with only a couple of dollars to spare — any one analysis, or roughly four median-priced ones back to back, but only ONE at the worst case.","perAnalysisUsd":{"status":"measured","asOf":"2026-07-29","sampleSize":48,"source":"oculr's own benchmark corpus, NOT neutral customer traffic: 48 production analyses over 24 distinct transactions, deliberately weighted toward hard exploit cases because that is what the service is built for. A small sample that describes what has been measured, not a guaranteed bound — your own mix decides which band you sit in.","model":"claude-opus-5","percentiles":{"min":0.09,"p25":1.77,"median":3.68,"p75":6.47,"p90":8.06,"max":13.37},"bands":[{"kind":"routine","lowUsd":1,"highUsd":2,"note":"transfers, swaps, straightforward DeFi"},{"kind":"incident-or-exploit","lowUsd":3,"highUsd":4,"note":"many more tool calls over far more data"}],"caveat":"Multi-modal: work clusters in the two bands above with very little in between, so the median ($3.68) sits in a gap few individual analyses actually occupy and must NOT be treated as an expected value. Budget against your band, or against p90 ($8.06). One analysis in ten costs more than $8.06."}},"defaultModel":"claude-opus-5","anthropic":[{"name":"explain_transaction","description":"Analyse a single EVM transaction (any supported EVM mainnet - 50+ chains, auto-detected; or pass chainId to skip detection) and return a structured explanation. Returns: { txHash, chain (e.g. \"ethereum-mainnet\"), chainName, status (success|reverted), summary, steps[], risks[], protocol, txType, confidence (high|medium|low), usdValue (USD of the economic outcome — for exploits the victim's loss, never the flash-loan notional), addresses[], contracts[] }. Transport: metered SSE - POST /explain with Accept: text/event-stream via an mppx session client (tempo.session.manager().sse()); the final event ({ type: \"result\", ... }) is the ExplanationResult. A plain JSON POST returns 402 with code \"use_metered_sse\". Latency: typically 1-3 minutes; complex MEV/DeFi/exploit transactions can take several minutes - prefer start_explain_job if blocking that long is a problem or if you cannot consume SSE. Use this for: synchronous workflows where blocking the agent is acceptable.","input_schema":{"type":"object","properties":{"txHash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"EVM transaction hash (32-byte hex, 0x-prefixed). Works on any supported EVM mainnet (50+ chains including Ethereum, Base, Arbitrum, Optimism, Polygon, BNB Chain, Avalanche, Linea, zkSync, Scroll, Blast, Gnosis, Celo, Mantle, Sonic, Berachain, Sei, Unichain, World Chain, Polygon zkEVM, Hyperliquid EVM, and more). Chain auto-detected if chainId is omitted. Example: 0x4e4b8ed4de38be29e3a7a15e2b14b5d8262e3c5b3f1e9d6a7c8b9e0f1a2d3c4e"},"chainId":{"type":"integer","minimum":1,"description":"Optional EIP-155 chainId of the transaction (e.g. 1 for Ethereum, 8453 for Base, 42161 for Arbitrum). When provided, skips multi-chain auto-detection. Omit if unknown; auto-detection will discover it."},"context":{"type":"string","maxLength":2048,"description":"Optional analyst hint passed to the analysis agent. Improves accuracy on edge cases. Example: \"check if this is a reentrancy exploit\" or \"this is a known MEV bot, classify the strategy\". Hard limit 2048 characters — the server rejects anything longer with 400 \"context too long (N chars); max 2048\"."},"model":{"type":"string","enum":["claude-opus-5","claude-opus-4-8","claude-opus-4-7","claude-sonnet-4-6","claude-haiku-4-5-20251001"],"default":"claude-opus-5","description":"Optional model override; must be one of the enum values (anything else is rejected with 400). Omit to get this deployment's default, which is the value in `default` below and is also reported as `defaultModel` at the top level of /tool-spec.json — published price bands are anchored to it. Sonnet/Haiku cost less; Opus is the default for accuracy."},"report":{"type":"boolean","description":"Optional. When true the analyst also renders a self-contained HTML report. Delivery differs by transport, and on the SSE path the report REPLACES the JSON: the metered SSE stream ends with a single { type: \"report\", html } event INSTEAD OF { type: \"result\", ... }, so only set this on explain_transaction if HTML is all you need. On the async path it is additive: the finished job carries both `result` (the JSON ExplanationResult) and `html`. Default false."}},"required":["txHash"]}},{"name":"start_explain_job","description":"Start an asynchronous transaction analysis. Returns immediately with a jobId. Plain fetch works - payment is metered to the same total price as sync explain_transaction, collected as the analysis runs: $0.01 is charged at submit, each get_job_result poll collects what has accrued since the previous poll, and the first poll after the job finishes charges the true-up. Every charge is handled automatically by the standard mppx polyfill (Mppx.create). You MUST keep polling with a paying client until the job is finished: if nothing collects for 90 seconds the analysis is aborted and the job is left with a partial result. Use this for: sub-agent contexts where blocking the parent agent is undesirable, when your HTTP client has a short timeout, or when you cannot consume the metered SSE stream that sync explain_transaction requires. After calling this, poll get_job_result with the returned jobId until status is \"complete\" or \"error\".","input_schema":{"type":"object","properties":{"txHash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"EVM transaction hash (32-byte hex, 0x-prefixed). Works on any supported EVM mainnet (50+ chains including Ethereum, Base, Arbitrum, Optimism, Polygon, BNB Chain, Avalanche, Linea, zkSync, Scroll, Blast, Gnosis, Celo, Mantle, Sonic, Berachain, Sei, Unichain, World Chain, Polygon zkEVM, Hyperliquid EVM, and more). Chain auto-detected if chainId is omitted. Example: 0x4e4b8ed4de38be29e3a7a15e2b14b5d8262e3c5b3f1e9d6a7c8b9e0f1a2d3c4e"},"chainId":{"type":"integer","minimum":1,"description":"Optional EIP-155 chainId of the transaction (e.g. 1 for Ethereum, 8453 for Base, 42161 for Arbitrum). When provided, skips multi-chain auto-detection. Omit if unknown; auto-detection will discover it."},"context":{"type":"string","maxLength":2048,"description":"Optional analyst hint passed to the analysis agent. Improves accuracy on edge cases. Example: \"check if this is a reentrancy exploit\" or \"this is a known MEV bot, classify the strategy\". Hard limit 2048 characters — the server rejects anything longer with 400 \"context too long (N chars); max 2048\"."},"model":{"type":"string","enum":["claude-opus-5","claude-opus-4-8","claude-opus-4-7","claude-sonnet-4-6","claude-haiku-4-5-20251001"],"default":"claude-opus-5","description":"Optional model override; must be one of the enum values (anything else is rejected with 400). Omit to get this deployment's default, which is the value in `default` below and is also reported as `defaultModel` at the top level of /tool-spec.json — published price bands are anchored to it. Sonnet/Haiku cost less; Opus is the default for accuracy."},"report":{"type":"boolean","description":"Optional. When true the analyst also renders a self-contained HTML report. Delivery differs by transport, and on the SSE path the report REPLACES the JSON: the metered SSE stream ends with a single { type: \"report\", html } event INSTEAD OF { type: \"result\", ... }, so only set this on explain_transaction if HTML is all you need. On the async path it is additive: the finished job carries both `result` (the JSON ExplanationResult) and `html`. Default false."}},"required":["txHash"]}},{"name":"get_job_result","description":"Poll for the result of an async job started with start_explain_job. Returns: { jobId, status (pending|running|complete|error), result?, html?, error? }. Recommended polling cadence: 2-5 seconds. Job results expire 1 hour after job creation. Polls collect what you already owe: each one charges the analysis cost accrued since your last poll (402, handled automatically by the mppx polyfill), a poll with nothing yet to collect is free, and the first poll after the job finishes charges the fee-bearing true-up. Cumulative charges equal the sync explain_transaction price exactly. Re-fetches of an already-paid finished result are free. Stop polling and the run stops: 90 seconds with nothing collected aborts the analysis and leaves the job holding a partial result.","input_schema":{"type":"object","properties":{"jobId":{"type":"string","format":"uuid","description":"The jobId returned by start_explain_job. UUID format."}},"required":["jobId"]}}],"openai":[{"type":"function","function":{"name":"explain_transaction","description":"Analyse a single EVM transaction (any supported EVM mainnet - 50+ chains, auto-detected; or pass chainId to skip detection) and return a structured explanation. Returns: { txHash, chain (e.g. \"ethereum-mainnet\"), chainName, status (success|reverted), summary, steps[], risks[], protocol, txType, confidence (high|medium|low), usdValue (USD of the economic outcome — for exploits the victim's loss, never the flash-loan notional), addresses[], contracts[] }. Transport: metered SSE - POST /explain with Accept: text/event-stream via an mppx session client (tempo.session.manager().sse()); the final event ({ type: \"result\", ... }) is the ExplanationResult. A plain JSON POST returns 402 with code \"use_metered_sse\". Latency: typically 1-3 minutes; complex MEV/DeFi/exploit transactions can take several minutes - prefer start_explain_job if blocking that long is a problem or if you cannot consume SSE. Use this for: synchronous workflows where blocking the agent is acceptable.","parameters":{"type":"object","properties":{"txHash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"EVM transaction hash (32-byte hex, 0x-prefixed). Works on any supported EVM mainnet (50+ chains including Ethereum, Base, Arbitrum, Optimism, Polygon, BNB Chain, Avalanche, Linea, zkSync, Scroll, Blast, Gnosis, Celo, Mantle, Sonic, Berachain, Sei, Unichain, World Chain, Polygon zkEVM, Hyperliquid EVM, and more). Chain auto-detected if chainId is omitted. Example: 0x4e4b8ed4de38be29e3a7a15e2b14b5d8262e3c5b3f1e9d6a7c8b9e0f1a2d3c4e"},"chainId":{"type":"integer","minimum":1,"description":"Optional EIP-155 chainId of the transaction (e.g. 1 for Ethereum, 8453 for Base, 42161 for Arbitrum). When provided, skips multi-chain auto-detection. Omit if unknown; auto-detection will discover it."},"context":{"type":"string","maxLength":2048,"description":"Optional analyst hint passed to the analysis agent. Improves accuracy on edge cases. Example: \"check if this is a reentrancy exploit\" or \"this is a known MEV bot, classify the strategy\". Hard limit 2048 characters — the server rejects anything longer with 400 \"context too long (N chars); max 2048\"."},"model":{"type":"string","enum":["claude-opus-5","claude-opus-4-8","claude-opus-4-7","claude-sonnet-4-6","claude-haiku-4-5-20251001"],"default":"claude-opus-5","description":"Optional model override; must be one of the enum values (anything else is rejected with 400). Omit to get this deployment's default, which is the value in `default` below and is also reported as `defaultModel` at the top level of /tool-spec.json — published price bands are anchored to it. Sonnet/Haiku cost less; Opus is the default for accuracy."},"report":{"type":"boolean","description":"Optional. When true the analyst also renders a self-contained HTML report. Delivery differs by transport, and on the SSE path the report REPLACES the JSON: the metered SSE stream ends with a single { type: \"report\", html } event INSTEAD OF { type: \"result\", ... }, so only set this on explain_transaction if HTML is all you need. On the async path it is additive: the finished job carries both `result` (the JSON ExplanationResult) and `html`. Default false."}},"required":["txHash"],"additionalProperties":false}}},{"type":"function","function":{"name":"start_explain_job","description":"Start an asynchronous transaction analysis. Returns immediately with a jobId. Plain fetch works - payment is metered to the same total price as sync explain_transaction, collected as the analysis runs: $0.01 is charged at submit, each get_job_result poll collects what has accrued since the previous poll, and the first poll after the job finishes charges the true-up. Every charge is handled automatically by the standard mppx polyfill (Mppx.create). You MUST keep polling with a paying client until the job is finished: if nothing collects for 90 seconds the analysis is aborted and the job is left with a partial result. Use this for: sub-agent contexts where blocking the parent agent is undesirable, when your HTTP client has a short timeout, or when you cannot consume the metered SSE stream that sync explain_transaction requires. After calling this, poll get_job_result with the returned jobId until status is \"complete\" or \"error\".","parameters":{"type":"object","properties":{"txHash":{"type":"string","pattern":"^0x[0-9a-fA-F]{64}$","description":"EVM transaction hash (32-byte hex, 0x-prefixed). Works on any supported EVM mainnet (50+ chains including Ethereum, Base, Arbitrum, Optimism, Polygon, BNB Chain, Avalanche, Linea, zkSync, Scroll, Blast, Gnosis, Celo, Mantle, Sonic, Berachain, Sei, Unichain, World Chain, Polygon zkEVM, Hyperliquid EVM, and more). Chain auto-detected if chainId is omitted. Example: 0x4e4b8ed4de38be29e3a7a15e2b14b5d8262e3c5b3f1e9d6a7c8b9e0f1a2d3c4e"},"chainId":{"type":"integer","minimum":1,"description":"Optional EIP-155 chainId of the transaction (e.g. 1 for Ethereum, 8453 for Base, 42161 for Arbitrum). When provided, skips multi-chain auto-detection. Omit if unknown; auto-detection will discover it."},"context":{"type":"string","maxLength":2048,"description":"Optional analyst hint passed to the analysis agent. Improves accuracy on edge cases. Example: \"check if this is a reentrancy exploit\" or \"this is a known MEV bot, classify the strategy\". Hard limit 2048 characters — the server rejects anything longer with 400 \"context too long (N chars); max 2048\"."},"model":{"type":"string","enum":["claude-opus-5","claude-opus-4-8","claude-opus-4-7","claude-sonnet-4-6","claude-haiku-4-5-20251001"],"default":"claude-opus-5","description":"Optional model override; must be one of the enum values (anything else is rejected with 400). Omit to get this deployment's default, which is the value in `default` below and is also reported as `defaultModel` at the top level of /tool-spec.json — published price bands are anchored to it. Sonnet/Haiku cost less; Opus is the default for accuracy."},"report":{"type":"boolean","description":"Optional. When true the analyst also renders a self-contained HTML report. Delivery differs by transport, and on the SSE path the report REPLACES the JSON: the metered SSE stream ends with a single { type: \"report\", html } event INSTEAD OF { type: \"result\", ... }, so only set this on explain_transaction if HTML is all you need. On the async path it is additive: the finished job carries both `result` (the JSON ExplanationResult) and `html`. Default false."}},"required":["txHash"],"additionalProperties":false}}},{"type":"function","function":{"name":"get_job_result","description":"Poll for the result of an async job started with start_explain_job. Returns: { jobId, status (pending|running|complete|error), result?, html?, error? }. Recommended polling cadence: 2-5 seconds. Job results expire 1 hour after job creation. Polls collect what you already owe: each one charges the analysis cost accrued since your last poll (402, handled automatically by the mppx polyfill), a poll with nothing yet to collect is free, and the first poll after the job finishes charges the fee-bearing true-up. Cumulative charges equal the sync explain_transaction price exactly. Re-fetches of an already-paid finished result are free. Stop polling and the run stops: 90 seconds with nothing collected aborts the analysis and leaves the job holding a partial result.","parameters":{"type":"object","properties":{"jobId":{"type":"string","description":"The jobId returned by start_explain_job. UUID format."}},"required":["jobId"],"additionalProperties":false}}}],"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"}