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 | Interactive prompts - "ask Claude to look at this tx" | One-line prompt, or install a persistent skill |
| Tool-use mode | Sub-agents inside a parent tool-use loop | Fetch /tool-spec.json, drop into your LLM API call |
| 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
Copy the prompt into any agent, or use a CLI directly. Swap the transaction hash for your own:
Use the oculr MPP at https://mpp.oculr.xyz/SKILL.md to analyse EVM transaction 0xfb60b6918d0d4bc5b3f72a261002bfea2e6b6543aad231eb6206c0dfebb65414 on Ethereum - 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/:
mkdir -p ~/.claude/skills/oculr
curl -s https://mpp.oculr.xyz/SKILL.md -o ~/.claude/skills/oculr/SKILL.mdOther agents follow their own convention - drop the file wherever that agent looks for skills. Once installed, prompts can reference the skill by name:
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. 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 - 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 tempoThe agent handles the rest. Fund it with tempo wallet fund (USDC.e tokens), not tempo wallet fund --credits: card-based MPP Credits settle one-time charges only, and oculr is session-based (intent="session"), so credits cannot pay for an analysis.
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):
# Create a new account (key written to the OS keychain - no plaintext on disk)
mppx account createOnce 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
const spec = await fetch('https://mpp.oculr.xyz/tool-spec.json').then(r => r.json())Returns:
{
"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
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-5',
tools: spec.anthropic,
messages: [{ role: 'user', content: 'Analyse tx 0x4e4b8ed4…' }],
})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:
import { Mppx, tempo } from 'mppx/client'
import { privateKeyToAccount } from 'viem/accounts'
// `maxDeposit` is required - it caps the channel total this session funds. It is
// not the opening deposit: the channel opens at min(suggestedDeposit, maxDeposit),
// so '32' still escrows the $16 oculr suggests and refunds the rest on close.
await Mppx.create({
methods: [tempo({
account: privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as `0x${string}`),
maxDeposit: '32',
})],
})
// The explicit `Promise<any>` is required: the function recurses, and under
// `strict` TypeScript cannot infer a return type for a self-referencing function.
async function dispatchToolCall(name: string, args: Record<string, unknown>): Promise<any> {
// 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, 5000))
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, (_: string, k: string) => 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
async function explainAsync(txHash: string, context?: string) {
const { jobId } = await dispatchToolCall('start_explain_job', { txHash, context })
while (true) {
await new Promise(r => setTimeout(r, 5000))
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 - settlement runs over Tempo MPP sessions. The Tempo mainnet USDC.e contract is 0x20C000000000000000000000b9537d11c60E8b50.
export WALLET_PRIVATE_KEY=0xYOUR_PRIVATE_KEYOr use mppx account create to store the key in your OS keychain rather than an env var.
Step 2 - install mppx
npm install mppx viemmppx is the MPP/x402 client library. It intercepts 402 responses, pays the challenge, and replays the request - your fetch() sees a single round-trip.
viem is an mppx peer dependency (>=2.54.0), and the samples below import from it directly (privateKeyToAccount) - so install it explicitly rather than relying on npm's peer auto-install, which pnpm and yarn don't do.
Step 3 - boot mppx and analyse a transaction
Call Mppx.create() once at startup; every subsequent fetch() on globalThis auto-pays 402s for the configured account. For agents, use the async path - POST /explain/async works with plain fetch() and returns a jobId immediately so your parent agent's tool-call turn stays fast. Payment is metered to the same total as the sync stream: $0.01 at submit, then each poll auto-pays what the analysis has accrued since the previous one, with the first poll after it finishes charging the true-up. Keep polling with the polyfill active - 90 seconds with nothing collected aborts the run and leaves a partial result. (Sync POST /explain is a metered SSE stream needing tempo.session.manager().sse() - see Call the oculr MPP; a plain JSON POST /explain returns 402 use_metered_sse.)
import { Mppx, tempo } from 'mppx/client'
import { privateKeyToAccount } from 'viem/accounts'
const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY! as `0x${string}`)
// `maxDeposit` is your hard per-session spend cap. It is NOT the amount escrowed:
// the channel opens at min(suggestedDeposit, maxDeposit) and oculr suggests $16,
// so a cap of '32' still escrows $16, and unused deposit is refunded on close.
// Use '32' - above the suggestion, never equal to it, or the channel opens on its
// own ceiling and can never be topped up. The $16 deposit covers any single
// analysis; a $5 cap can be exhausted by one exploit investigation on its own.
await Mppx.create({ methods: [tempo({ account, maxDeposit: '32' })] })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, 5000))
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:
const { summary, txType, confidence, risks } = result
// Always check the partial signal first.
if (summary.startsWith('**Partial result')) {
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:
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}}'mppx handles the 402 payments behind the scenes - bare curl against a paid endpoint will just return the payment challenge, so every poll needs an MPP client. Stop polling for 90 seconds and the analysis is aborted, leaving a partial result.
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_resultoverexplain_transaction. Async polling keeps each parent-agent turn fast. - Branch on
result.confidence.'high'→ use the summary verbatim;'medium'→ cross-checkrisks;'low'→ escalate. - Check for partial results first. A partial result is still a
200but withconfidence: 'low', a summary that starts with "Partial result", and limited fields - don't trust the body uncritically. - Use
result.txTypefor routing. It's a closed enum (swap | transfer | exploit | mev | …) - dispatch to specialised follow-up logic per type. - Pass
contextaggressively. 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
risksarray is a signal. Even iftxTypeis benign, populatedrisks[]warrants escalation. - Cap your spend client-side.
mppx'smaxDepositis your session ceiling - use'32', above the $16 suggested deposit and never equal to or below it. It bounds what a leaked wallet key can spend through this channel; the channel still escrows only $16.
Related
/tool-spec.json- typed contract for tool-use mode/SKILL.md- prose entry point for skill mode/openapi.json- full REST schema- Endpoints reference - request/response details