How an agent uses PaladinFi: trust check, quote, sign, submit

2026-05-11 · Walkthrough mcp agent-flow

PaladinFi runs an MCP server at swap.paladinfi.com/mcp that gives an agent three tools: swap_quote for ready-to-sign swap calldata on Base, trust_check_preview for a sample response shape of the token-risk gate, and swap_health for router status. One install line, three tools, no SDK. This post walks through a complete agent transaction — a USDC → WETH swap on Base — from install to on-chain receipt.

Install

The MCP server is HTTP-transport and discoverable by any MCP client. From Claude Code or Claude Desktop:

claude mcp add --transport http --scope user paladin-swap https://swap.paladinfi.com/mcp

That's the install. After the client reloads, three tools are available to the agent. Cost: swap_quote and swap_health are free. trust_check_preview is free and rate-limited. The live paid trust-check at POST /v1/trust-check (called separately, not via MCP) is $0.001 USDC per call via x402.

The MCP server is non-custodial by construction: it returns data and calldata, never holds funds, never signs. The returned calldata's outer router target and function selector are both validated against an explicit allowlist before return (see selector_enforcement in swap_health below). A malicious or compromised upstream cannot return calldata that drains the signer's other balances — it would be rejected before the agent ever sees it.

For agents not running on an MCP-aware host, the same three tools are published as framework-specific npm packages: @paladinfi/eliza-plugin-trust for ElizaOS, @paladinfi/agentkit-actions for Coinbase AgentKit. The walkthrough below uses MCP tool names; the plugins wrap the same calls with framework-native types.

Stage 1 — Sanity check with swap_health

Before issuing a quote, the agent should confirm the router is reachable and which upstreams are serving. swap_health returns a small JSON document with per-source counters, the active fee, the supported chain set, and a few defense-in-depth flags:

{
  "status": "ok",
  "version": "0.11.73",
  "supportedChains": [8453],
  "feeBps": 10,
  "feeRecipient": "0xeA8C33d018760D034384e92D1B2a7cf0338834b4",
  "sources": {
    "0x":     { "ok": 16, "err": 0, "enabled": true, "avg_latency_ms": 466 },
    "velora": { "ok": 16, "err": 0, "enabled": true, "avg_latency_ms": 292 }
  },
  "velora_canary": { "verdict": "ok", "detail": "destAmount=<int>" },
  "selector_enforcement": {
    "mode": "enforce",
    "rejected_total": { "0x": 0, "velora": 0 },
    "allowlist_sizes": { "0x": 1, "velora": 11 },
    "deny_list_size": 7
  }
}

Three things this discloses up front. The fee is 10 bps (the feeBps field) and the recipient address is visible — no hidden routing rake. Both upstream quote sources (0x and Velora) report ok/err counters since the current service instance started, so an agent can spot one-sided degradation (e.g., 0x serving while Velora errors). And selector_enforcement is in enforce mode, which means returned calldata is checked against an explicit allowlist of router function selectors and a hard deny-list of permit-style selectors — defense-in-depth against the calldata being substituted for an arbitrary token-transfer.

velora_canary is a startup-time round-trip check against a stable pair (USDC ↔ USDT 1:1); the detail value is the Velora-quoted destination amount and floats with each restart. If verdict flips to anything other than ok, the agent should de-weight Velora-sourced quotes until it returns.

Stage 2 — Risk-check the destination token

The agent is about to send funds to a swap that lands in a token the user named. Before committing the paid call, the agent calls trust_check_preview to confirm the response shape and the integration is wired correctly. Preview is free, rate-limited, and returns a sample fixture — not a live evaluation:

// request
{
  "chainId": 8453,
  "address": "0x4200000000000000000000000000000000000006"  // WETH on Base
}

// response (trimmed)
{
  "trust": {
    "risk_score": null,
    "recommendation": "sample-allow",
    "recommendation_enum": ["allow", "warn", "block"],
    "factors": [
      { "source": "ofac",            "signal": "not_listed", "real": false,
        "details": "Live on paid endpoint; not evaluated on preview" },
      { "source": "etherscan_source","signal": "verified",   "real": false,
        "details": "SAMPLE — illustrative only" },
      { "source": "goplus",          "signal": "ok",         "real": false,
        "details": "SAMPLE — illustrative only" },
      { "source": "anomaly",         "signal": "ok",         "real": false,
        "details": "SAMPLE — illustrative only" }
    ],
    "version": "1.0",
    "_preview": true,
    "_message": "Preview response — request shape was validated by Pydantic. The trust block is a SAMPLE FIXTURE with no live data evaluation. ..."
  },
  "_mcp_paid_endpoint_info": {
    "url":        "https://swap.paladinfi.com/v1/trust-check",
    "method":     "POST",
    "auth":       "x402 (USDC EIP-3009 transferWithAuthorization on Base)",
    "price_usdc": "0.001",
    "plugins":    { /* npm package URLs for eliza + agentkit */ },
    "docs":       "https://paladinfi.com/trust-check/"
  }
}

Three defense-in-depth markers keep the preview from being misrepresented as a live verdict in a screenshot. recommendation is "sample-allow", not the live "allow". Every factor carries real: false. And _preview: true is a top-level flag clients can branch on. Agent code that wants to treat the preview and the paid response uniformly should assert !resp.trust._preview before trusting recommendation.

The preview fixture is intentionally stable — it's a shape document, not a snapshot of the live signal set. The live paid endpoint at POST /v1/trust-check can return a different factor set; the preview's goplus + anomaly entries map to the production paladin.anomaly source in the live response, and the live response carries version: "1.1" (per the v0.11.73 fail-closed contract) where the preview fixture stays at "1.0". Branch on field presence and signal names rather than counting factor array length.

For a real evaluation the agent calls POST /v1/trust-check — x402-authenticated, $0.001 USDC per call on Base via EIP-3009 transferWithAuthorization. The response shape is the same minus the sample prefixes and the _preview flag, with the factors carrying live signals: OFAC SDN screening refreshed daily from the Treasury XML feed, an anomaly heuristic, and Etherscan source-verification. Behavior under upstream outage is the v0.11.73 fail-closed contract — if every upstream is unreachable, the response sets recommendation: "warn" with three factors flagged signal: "unreachable", and only an OFAC-confirmed block can override that. There is no silent-allow code path.

Stage 3 — Get a swap quote

The agent has cleared the trust gate and wants to swap 100,000 base units of USDC (0.1 USDC) into WETH on Base. It calls swap_quote:

// request
{
  "chainId":    8453,
  "sellToken":  "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",  // USDC on Base
  "buyToken":   "0x4200000000000000000000000000000000000006",  // WETH on Base
  "sellAmount": "100000",
  "taker":      "0x000000000000000000000000000000000000bEEF"   // replace with the agent's signer address
}

// response (trimmed; values from a recent block — exact integers float)
{
  "source":        "velora",
  "chainId":       8453,
  "router":        "0x6a000f20005980200259b80c5102003040001068",
  "calldata":      "0xe3ead59e...",   // truncated; ~1.4 KB in practice
  "buyAmount":     "43219845316948",  // best quoted out-amount, wei of WETH
  "minBuyAmount":  "43003746090363",  // slippage floor (50 bps default)
  "sellAmount":    "100000",
  "gas":           "108740",
  "ourFeeBps":             10,
  "ourFeeRecipient":       "0xeA8C33d018760D034384e92D1B2a7cf0338834b4",
  "estimatedOurFeeAmount": "43263108426",
  "estimatedOurFeeToken":  "0x4200000000000000000000000000000000000006",
  "rawResponse":   { /* full upstream priceRoute + transaction; opaque, do not rely on */ },
  "trust": {
    "risk_score":     0,
    "recommendation": "allow",
    "factors": [
      { "source": "ofac",            "signal": "not_listed",          "details": "" },
      { "source": "paladin.anomaly", "signal": "address_kind_contract","details": "eth_getCode = non-empty" },
      { "source": "etherscan_source","signal": "verified",            "details": "Verified as WETH9" }
    ],
    "version": "1.1"
  }
}

A few fields are doing work here. source: "velora" means best-of-2 routing across 0x and Velora picked Velora for this pair. Both upstreams quote in parallel; the router compares buyAmount first and breaks ties on minBuyAmount. Per a 7-day production measurement (2026-05-04 → 2026-05-11, n ≈ 105–110 routed quotes with ~90% USDC ↔ WETH at small notionals), Velora won roughly 81% and 0x roughly 19%; the split shifts with upstream liquidity and will look different on long-tail pairs and at larger sizes. The source field in the response is the canonical answer for any given quote.

The embedded trust block. The quote response carries a live trust evaluation at response.trust — the same data the paid /v1/trust-check would return for the destination token, on version: "1.1" (the v0.11.73 fail-closed contract). The common case of "swap on allow, abort on warn or block" can branch on quote.trust.recommendation without a second call. trust_check_preview from Stage 2 is for integration testing against a stable fixture; the live evaluation comes back on every quote.

router and calldata are what the agent's wallet passes to eth_sendTransaction (or equivalent). For the Velora path the router is AugustusSwapper v6.2 on Base (0x6a00...1068); for the 0x path the router is the AllowanceHolder contract on Base (0x0000000000001ff3684f28c67538d4d072c22734), which dispatches to the 0x Settler (0x7747...2359) via its exec() entry. Both routers are pinned to a per-source allowlist on the swap router. Selector enforcement validates both the outer function selector AND, on the 0x path specifically, the inner target argument inside the exec calldata — so a substituted target that would have dispatched to a malicious contract gets rejected before return. allowlist_sizes in swap_health shows the count: 1 selector for the 0x path (exec only), 11 swap-function selectors covering Augustus v6.2 on the Velora path.

minBuyAmount is the slippage floor — the swap reverts on-chain if execution would land below this. gas is the upstream's pre-computed gas estimate, suitable for the wallet's gasLimit with a small safety margin. ourFeeBps: 10 is the integrated 10 bps fee taken in the destination token at execution time; no separate fee transaction. The fee is exact and computed against buyAmount, so the agent can quote it to the user up front: estimatedOurFeeAmount is the wei amount, estimatedOurFeeToken is the contract address it lands in.

The full rawResponse from the upstream is in the envelope for debugging — for the Velora path, it includes the priceRoute object with the route breakdown (in the swap above, a 1-hop USDC → WETH via a MaverickV2 pool; the exact pool address varies by block as routing reacts to liquidity). Agents that just want to execute can ignore rawResponse; agents that want to surface route detail to the user can read it. rawResponse is upstream-shape and not part of the stable contract — treat it as opaque or expect breakage when upstreams version their responses.

Failure modes at the quote site

If one upstream errors and the other returns a fillable quote, the response carries source set to whichever upstream answered; the agent doesn't need to handle this case explicitly. If both upstreams error or neither returns a fillable quote, the endpoint returns HTTP 502 with a JSON {detail} error body — the agent should treat that as "do not trade now," not "retry immediately." Transient HTTP 5xx from a single upstream is absorbed by the surviving source; structural HTTP 5xx from both is the new-data signal the agent acts on.

Stage 4 — Sign and submit

This is where the MCP server steps out. The agent's wallet provider — viem WalletClient, Coinbase AgentKit's EvmWalletProvider, Eliza's wallet abstraction, anything else that can sign EVM transactions — takes { router, calldata, gas, value: 0 } and constructs the transaction. Two preconditions before the swap call: the taker needs an ERC-20 approve set on the sell-token granting the router enough allowance (on Velora the spender is rawResponse.priceRoute.tokenTransferProxy, which for Augustus v6.2 equals the router itself; on 0x the spender is the AllowanceHolder, which is also the router), and the taker needs enough native ETH for gas.

// sketch: viem (pseudo-code — substitute your wallet's actual helpers)
import { erc20Abi, parseAbi } from "viem";

// 1. approve the router (one-time per (router, sellToken) pair)
const allowance = await publicClient.readContract({
  address: SELL_TOKEN,
  abi:     erc20Abi,
  functionName: "allowance",
  args:    [TAKER, quote.router],
});
if (allowance < BigInt(quote.sellAmount)) {
  await walletClient.writeContract({
    address: SELL_TOKEN, abi: erc20Abi, functionName: "approve",
    args:    [quote.router, BigInt(quote.sellAmount)],
  });
}

// 2. submit the swap
const tx = await walletClient.sendTransaction({
  to:    quote.router,
  data:  quote.calldata,
  value: 0n,
  gas:   BigInt(quote.gas),
});
const receipt = await publicClient.waitForTransactionReceipt({ hash: tx });

// 3. reconcile against the slippage floor by parsing the buyToken Transfer
//    event to the taker from receipt.logs — exact parser depends on your stack
const actual = readBuyAmountFromLogs(receipt.logs, quote.buyToken, TAKER);
if (actual < BigInt(quote.minBuyAmount)) {
  throw new Error("slippage floor breached — should not happen; on-chain check would have reverted");
}

On success, the agent's wallet holds buyAmount minus estimatedOurFeeAmount of the destination token. The 10 bps fee lands in ourFeeRecipient at the same block — no second transaction, no second signature. If the upstream's pricing moved between quote and execution and the actual out-amount would fall below minBuyAmount, the transaction reverts on-chain; nothing moves, the agent re-quotes.

Why three tools, not ten

The three-tool surface is deliberate. The reasoning for each:

swap_health is the smallest surface that lets an agent verify the install worked and check live posture before committing. It doubles as a transparency surface: fee disclosure, source counters, the defense-in-depth selector enforcement state. An agent that can't reach swap_health can't trust anything else returned by the same MCP server, so it earns its own tool slot.

trust_check_preview exists because integration testing against a paid endpoint is friction. The preview is a fixture that returns the live response shape with sample data; the agent's code can branch on _preview, run unit tests against the preview, and graduate to POST /v1/trust-check when it wants real evaluation. We considered shipping only the paid endpoint and reverted — gating evaluation of the gate behind a paid call defeated the integration-test use case.

swap_quote is the actual product call: trust-checked, best-of-2-routed, fee-disclosed, defense-in-depth-verified calldata, returned as data for the agent's own wallet to sign. The router does not custody funds. The MCP server does not hold keys. The single tool covers the full quote surface; chain, token pair, amount, and taker are arguments rather than separate tools.

What's deliberately not in the surface: no order management, no portfolio reporting, no balance reads, no chain-other-than-Base. Those are answerable from wallet primitives and chain RPC; the MCP server is scoped to what only the router can answer.

Verify

After install, swap_health should return version: "0.11.73" and status: "ok". If it doesn't, the MCP client isn't reaching the router — check the transport URL. The same value is queryable directly at swap.paladinfi.com/health.

Source and contract details: github.com/paladinfi/paladin-swap-mcp (MCP server source and README walkthrough), @paladinfi/eliza-plugin-trust and @paladinfi/agentkit-actions (framework-native wrappers), paladinfi.com/swap and paladinfi.com/trust-check (endpoint contracts), the v0.11.73 post (trust-block fail-closed behavior in detail). Questions, repro cases, or integration help: dev@paladinfi.com.