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

Use as an agent

Three integration paths for AI agents - pick by how tightly your agent loop needs to control the call.

ModeBest forSetup
Skill modeInteractive prompts - "ask Claude to look at this tx"One-line prompt, or install a persistent skill
Tool-use modeSub-agents inside a parent tool-use loopFetch /tool-spec.json, drop into your LLM API call
Raw APICustom server pipelines, anything that wants full HTTP controlmppx + 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:

Prompt
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.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:

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 tempo

The 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 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

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:

ToolPurposeBlocking?
explain_transactionAnalyse a tx, return result inlineYes
start_explain_jobStart async analysis, return jobIdNo
get_job_resultPoll an async job for its resultNo

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

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-5',
  tools: spec.anthropic,
  messages: [{ role: 'user', content: 'Analyse tx 0x4e4b8ed4…' }],
})
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:

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_KEY

Or use mppx account create to store the key in your OS keychain rather than an env var.

Step 2 - install mppx

npm install mppx viem

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.

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_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.
  • Check for partial results first. A partial result is still a 200 but with confidence: 'low', a summary that starts with "Partial result", 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 - 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