Verify Payloads
POST /quote returns fully-encoded transaction payloads,
ready to sign and submit. This page explains what to check in those payloads before signing, so you can confirm the quote is valid and the transaction is safe.
Quote-level checks
Every Quote echoes your request back. Before looking at individual payloads, confirm:
| Field | Check |
|---|---|
route | source and destination match the chain and token address you requested |
recipient | Matches the recipient you passed, or your sender if you omitted it |
amountIn | Matches the amount you requested, in the token's smallest unit |
amountOut | Within your expectations for the route; reject quotes with an implausible rate |
Then verify each entry in payloads according to its runtime, discriminated by payload.data.type
("evm" or "svm"). Payloads execute in order, so check all of them before signing the first.
EVM payloads
An EVM payload's data is a complete transaction for your wallet to sign. Its shape is { type: "evm", chain, chainId, to, data, value }. Check:
- Chain.
chainandchainId. Pin that mapping yourself rather than deriving it from the response. The first leg always executes onroute.source.chain, and approvals execute on the same chain as the leg they approve. Multi-hop routes include legs on intermediate chains: each leg must execute either on the same chain as the previous leg or on the destination chain decoded from the previous bridge leg's calldata. - Target contract (
to) is known. For action legs (swap, bridge, order),tomust be a contract listed on the Deployments pages (e.g.SwapFacility, a Portal). For approval legs,tois the input token itself, the address from yourroute.source, which also appears on the Deployments pages and inGET /supported-assets. For third-party legs (wormhole-cctp), verify against that provider's official deployment docs. - Amount. Decode the calldata and confirm the amount equals the1 quote's
amountIn. - Recipient. On the final leg, confirm the decoded recipient equals the quote's
recipient. - Approvals are exact and correctly scoped. An approval leg is
approve(spender, amount)wherespenderis the contract executing the next payload andamountis exactlyamountIn. The API never asks for unlimited approvals; reject any payload that does. - Native value.
valueis"0"for approvals and same-chain swaps. Bridge legs carry the cross-chain delivery fee invalue; sanity-check its magnitude against current gas prices.
Example: decoding and checking an approval leg
import { decodeFunctionData, erc20Abi } from "viem";
import type { components } from "./m0-swap"; // From type generation
type Quote = components["schemas"]["Quote"];
type EvmPayload = components["schemas"]["EvmPayload"];
function verifyApprovalLeg(
quote: Quote,
payload: EvmPayload,
nextLegTo: string,
) {
// Approval legs call approve() on the input token itself
if (payload.to.toLowerCase() !== quote.route.source.address.toLowerCase()) {
throw new Error("approval target is not the input token");
}
const { functionName, args } = decodeFunctionData({
abi: erc20Abi,
data: payload.data as `0x${string}`,
});
if (functionName !== "approve") throw new Error("unexpected function");
const [spender, amount] = args;
if (spender.toLowerCase() !== nextLegTo.toLowerCase()) {
throw new Error("spender is not the contract executing the next leg");
}
if (amount !== BigInt(quote.amountIn)) {
throw new Error("approval amount does not equal amountIn");
}
}
For action legs, decode against the contract's ABI (linked from the
Deployments tables) and apply the same amount and recipient checks. As a
final safety net, simulate the transaction (eth_call / Viem simulateContract) and inspect the
resulting balance changes before broadcasting.
Permits
The opt-in permit flow replaces the on-chain
approval with an off-chain EIP-2612 signature, so the
verification target shifts: what you sign is the recipe's typedData, and a bad permit is as
dangerous as a bad approval. Before signing a recipe from POST /permit/quote, check:
| Field | Check |
|---|---|
typedData.message.owner | Your address (the account funding the input tokens) |
typedData.message.spender | Equals recipe.spender and is a known contract on Deployments |
typedData.message.value | Exactly the quote's amountIn, never unlimited |
typedData.domain.verifyingContract | The input token address from your route.source |
typedData.domain.chainId | The source chain's id |
typedData.message.deadline | Near-future timestamp, not far beyond the quote's expiresAt |
Payloads returned by
POST /permit/build are the same
EvmPayload shape as /quote, so all the EVM checks above apply to them unchanged.
You can also skip the build round-trip entirely: the recipe carries everything needed to encode the final calldata offline, which means you construct, and therefore fully control, the exact transaction you sign. See Self-encoding.
SVM payloads
An SVM payload is { type: "svm", chain, transaction }, where transaction is a base64-encoded
Solana VersionedTransaction. Multi-step Solana routes are combined server-side into a single
transaction where possible, and the server pre-signs any auxiliary signers it controls. Your
wallet adds the fee-payer signature. Deserialize to inspect the transaction. Check:
- Fee payer is your sender. The first account key (the fee payer and first required signer)
must be the
senderyou passed to/quote. - Every invoked program is known. Each instruction's program id must be one you recognize: the M0 programs listed in the Solana section of Deployments, standard Solana programs (System, Compute Budget, SPL Token / Token-2022, Associated Token Account), or the bridge programs of the leg's provider (e.g. Wormhole).
- Resolve lookup tables first. Versioned transactions may load accounts through address lookup tables, so fetch the tables and audit the resolved key set, not just the static keys.
- Amount and recipient. The destination token account must be the associated token account of
the quote's
recipientfor the destination mint, and the transferred amount must equalamountIn. The most robust way to check both is simulation: runsimulateTransactionand inspect the resulting token balance changes. - Keep existing signatures intact. The server may have already signed with auxiliary signers. Add your signature to the transaction as-is; mutating the message invalidates the pre-applied signatures.
Example: inspecting an SVM payload
import { Connection, PublicKey, VersionedTransaction } from "@solana/web3.js";
import type { components } from "./m0-swap"; // From type generation
type SvmPayload = components["schemas"]["SvmPayload"];
// Pin to /resources/addresses. Do not learn addresses from API responses
const KNOWN_PROGRAMS = new Set([
"mzp1q2j5Hr1QuLC3KFBCAUz5aUckT6qyuZKZ3WJnMmY", // M0 Portal
"11111111111111111111111111111111", // System
"ComputeBudget111111111111111111111111111111", // Compute Budget
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb", // Token-2022
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", // SPL Token
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", // Associated Token Account
// ...plus the provider's bridge programs (e.g. Wormhole) for cross-chain legs
]);
async function verifySvmPayload(
connection: Connection,
payload: SvmPayload,
sender: string,
) {
const tx = VersionedTransaction.deserialize(
Buffer.from(payload.transaction, "base64"),
);
// 1. Fee payer (first required signer) must be your sender
const feePayer = tx.message.staticAccountKeys[0];
if (!feePayer.equals(new PublicKey(sender))) {
throw new Error("unexpected fee payer");
}
// 2. Resolve address lookup tables so every account key is visible
const lookupTables = await Promise.all(
tx.message.addressTableLookups.map(
async (lookup) =>
(await connection.getAddressLookupTable(lookup.accountKey)).value!,
),
);
const accounts = tx.message.getAccountKeys({
addressLookupTableAccounts: lookupTables,
});
// 3. Every invoked program must be one you recognize
for (const ix of tx.message.compiledInstructions) {
const programId = accounts.get(ix.programIdIndex)!.toBase58();
if (!KNOWN_PROGRAMS.has(programId)) {
throw new Error(`unknown program: ${programId}`);
}
}
// 4. Simulate and inspect balance changes before signing
const sim = await connection.simulateTransaction(tx, { sigVerify: false });
if (sim.value.err) {
throw new Error(`simulation failed: ${JSON.stringify(sim.value.err)}`);
}
return tx; // sign as-is, do not rebuild the message
}
Notes
- Payloads embed a recent blockhash (SVM) or current fees (bridge
value), so they are short-lived. If verification or signing takes long, re-quote rather than patching payloads. - The
providerandannotationfields describe each leg and tell you which contract family to expect atto(or which programs to expect on Solana), but they are informational, anchor your checks on the decoded transaction contents and the Deployments pages.