How an agent uses PaladinFi: trust check, quote, sign, submit
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.