If a price can only be fetched from one server, you can only ever believe that server. If it's a contract call, you can ask anyone.
So I did: same block, two independent Base RPC providers, one question. Here is the whole experiment, including the curl version.
The question is 100 bytes
BlazePhoenix is an on-chain DEX aggregator. Its Quoter is a contract, and previewPlan(address,address,uint256) is a view function on it: give me the plan for swapping X of token A into token B. Because it is a view function, the "API" is just eth_call. The calldata for 1 WETH → USDC on Base is a 4-byte selector plus three 32-byte words:
0x49d5f197
0000…4200000000000000000000000000000000000006 WETH
0000…833589fcd6edb6e08f4c7c32d4f71b54bda02913 USDC
0000…0de0b6b3a7640000 1e18
You can send it with nothing but curl (the Quoter address is the one the SDK pins for Base, generation 1.0.0):
curl -s https://mainnet.base.org -H 'content-type: application/json' -d '{
"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{
"to":"0x4cEF0615614B212895F45Aa1D4833B16666E18d3",
"data":"0x49d5f1970000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000de0b6b3a7640000"
},"latest"]}'
What comes back is one long hex string: the ABI-encoded Preview struct (route, gross output, fee, net output, floors…). Decoding it by hand gets old fast, so let's use viem.
Pin a block, ask two nodes
import { createPublicClient, http, parseUnits, formatUnits, encodeFunctionData, keccak256 } from 'viem';
import { QUOTER_ABI, EMBEDDED_DEPLOYMENTS } from '@blazephoenix/sdk';
const RPCS = (process.env.BASE_RPCS ?? 'https://mainnet.base.org,https://base-rpc.publicnode.com').split(',');
const WETH = '0x4200000000000000000000000000000000000006';
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
// The Quoter the SDK pins for Base (chain id 8453), generation 1.0.0
const quoter = EMBEDDED_DEPLOYMENTS.versions.find((v) => v.version === '1.0.0')!.chains['8453'].quoter as `0x${string}`;
// 1. calldata for previewPlan(WETH, USDC, 1e18)
const data = encodeFunctionData({ abi: QUOTER_ABI, functionName: 'previewPlan', args: [WETH, USDC, parseUnits('1', 18)] });
// 2. pin ONE block, then ask every node the same question at that block
const first = createPublicClient({ transport: http(RPCS[0]) });
const block = (await first.getBlockNumber()) - 2n; // 2 blocks back: every node has it
for (const url of RPCS) {
const c = createPublicClient({ transport: http(url) });
const { data: raw } = await c.call({ to: quoter, data, blockNumber: block });
console.log(new URL(url).host.padEnd(26), 'block', block, 'keccak256(result)', keccak256(raw!).slice(0, 18), `(${(raw!.length - 2) / 2} bytes)`);
}
// 3. decode once, on the first node
const [pv] = await first.readContract({ address: quoter, abi: QUOTER_ABI, functionName: 'previewPlan',
args: [WETH, USDC, parseUnits('1', 18)], blockNumber: block });
const bps = (Number(pv.protocolFee) / Number(pv.grossOut)) * 1e4;
console.log('gross', formatUnits(pv.grossOut, 6), '| fee', formatUnits(pv.protocolFee, 6), `(${bps.toFixed(3)} bps)`,
'| net', formatUnits(pv.netOut, 6), '| floor', formatUnits(pv.ironFloor, 6));
console.log('hops', pv.hops, 'legs', pv.legs, 'impact bps', pv.route.expectedImpactBps, 'canExecute', pv.canExecute);
The SDK package is used here only for two constants (the ABI and the pinned address, both embedded in the package). The script makes no request to blazephoenix.xyz: the only network traffic is JSON-RPC to the nodes you list.
npm i @blazephoenix/sdk viem tsx
npx tsx quote.mts # .mts so top-level await works without "type": "module"
What it printed
mainnet.base.org block 53222961n keccak256(result) 0x561a6dde8b88f1c5 (3008 bytes)
base-rpc.publicnode.com block 53222961n keccak256(result) 0x561a6dde8b88f1c5 (3008 bytes)
gross 2343.754198 | fee 6.562511 (28.000 bps) | net 2336.49053 | floor 2058.519312
hops 1n legs 5n impact bps 17n canExecute true
Two providers I don't control, one block, the same 3,008 bytes, the same hash. The numbers are that block's sample and will have moved by the time you read this; the point is that anyone can reproduce them for that block.
Two details worth checking yourself in that output:
-
The fee is a visible line, not a spread. 6.562511 on a gross of 2,343.754198 is 28.000 bps. 0.28 % is the constant
PROTOCOL_FEE_BPS = 28in the contract source, not a setting on a server. -
The floor is part of the answer.
ironFlooris the minimum output the Router enforces for that plan, and the quote carries it. (Background: the iron floor article.)
Why bother hashing?
Because "I trust my RPC provider" is the same sentence as "I trust the quote server", one hop removed. Asking a second, unrelated provider about the same block turns trust into comparison. If two nodes ever disagree on a pinned block, you have found something worth looking at, and you found it without anyone's permission.
What this does not show
- A quote is a snapshot. The block moves, so the price moves; nothing in a read can promise what a later transaction receives. That is the job of the minimum output you sign, not of the quote.
- Matching bytes prove two nodes agree on chain state, not that the route is the best one available. The route logic lives in the contracts, and the source is public to read.
Docs and the machine-readable index are linked from blazephoenix.xyz; the SDK is @blazephoenix/sdk (source).
Status: BlazePhoenix is pre-launch and an independent external audit is scheduled. Read-only quote sample, not advice and not a performance claim.
Top comments (0)