Permit Approvals
Permit approvals let you collapse the usual two-transaction flow — an on-chain approve followed by
the action — into a single transaction backed by an off-chain signature. Instead of approving a
spender on-chain, the user signs an EIP-2612 permit
(EIP-712 typed data) and the action contract pulls the allowance from that signature when it runs.
The flow is two steps:
POST /permit/quotereturns the quote economics plus a permit recipe — the EIP-712 typed data to sign — for each leg of the route.- The client signs each recipe's
typedData, then either callsPOST /permit/buildwith the signed recipes to get executable payloads, or encodes the call locally (see Self-encoding).
permit() — all M and wM extensions and USDC do. Routes whose input token
does not support permit() return 404 PermitNotSupported.POST /permit/quote
Returns one or more quotes for a given asset route and amount, each carrying a permit recipe per leg. A recipe contains the EIP-712 typed data the client signs.
Request
Endpoint
POST /permit/quote
Headers
Content-Type: application/json
x-api-key: YOUR_API_KEY
Body Parameters and response
Check the API reference for detailed schema definitions.
Example Request
import type { components } from "./m0-swap"; // From type generation
type PermitQuoteResponse = components["schemas"]["PermitQuoteResponse"];
const response = await fetch(
"https://gateway.m0.xyz/v1/orchestration/permit/quote",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "YOUR_API_KEY",
},
body: JSON.stringify({
route: {
source: {
chain: "Ethereum",
address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC token
},
destination: {
chain: "Solana",
address: "xoUSDq85Rjsb6SbUwJyreFgeWQvxdkT7R3c3g7s6p5Y", // XO token
},
},
amountIn: "1000000", // 1 USDC (6 decimals)
funder: "0xYourWalletAddress",
sender: "0xYourWalletAddress",
recipient: "yourSolanaWalletAddress",
}),
},
);
const result: PermitQuoteResponse = await response.json();
// Getting first quote and first recipe for signing
const quote = result.quotes[0];
const recipe = quote.recipes[0];
// See Self-encoding section for how to sign the recipe and encode the call locally.
Error Responses
All errors share the { code, message, requestId } body shape. Branch on code rather than on
the HTTP status.
| Status | Code | Description |
|---|---|---|
400 | BadPermitRequest | Malformed or invalid body (missing fields, unsupported asset, bad params) |
404 | PermitNotSupported | The route is valid but not permit-capable (input token has no permit()) |
500 | PermitQuoteError | Unexpected internal failure while computing the permit quote |
POST /permit/build
Takes the signed recipes and returns executable transaction payloads — the same Payload shape
returned by POST /quote. Use this if you want the server to
encode the *WithPermit call for you; otherwise see Self-encoding.
Request
Endpoint
POST /permit/build
Headers
Content-Type: application/json
x-api-key: YOUR_API_KEY
Body Parameters
PermitBuildParams:
| Field | Type | Required | Description |
|---|---|---|---|
signedRecipes | SignedRecipe[] | Yes | The signed recipes, one per leg |
Each SignedRecipe:
| Field | Type | Required | Description |
|---|---|---|---|
recipe | PermitRecipe | Yes | The recipe echoed verbatim from /permit/quote |
signature | string | Yes | The 65-byte hex permit signature over recipe.typedData |
Response
PermitBuildResponse:
| Field | Type | Description |
|---|---|---|
requestId | string | Identifier for the request, echoed in error bodies |
payloads | Payload[] | Executable payloads (same shape as /quote) |
issues | PermitIssues | Optional soft warnings; does not fail the request |
Each Payload is { provider, annotation?, data: { type: "evm", chain, chainId, to, data, value } },
identical to the payloads returned by POST /quote. Execute
them in order — see
Executing Quote Payloads.
Example Request
import type { components } from "./m0-swap"; // From type generation
type PermitBuildResponse = components["schemas"]["PermitBuildResponse"];
const response = await fetch(
"https://gateway.m0.xyz/v1/orchestration/permit/build",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "YOUR_API_KEY",
},
body: JSON.stringify({
signedRecipes: [
{
recipe: quote.recipes[0], // echoed verbatim from /permit/quote
signature: "0x...", // 65-byte signature over recipe.typedData
},
],
}),
},
);
const result: PermitBuildResponse = await response.json();
Example Response
{
"requestId": "req_abc123",
"payloads": [
{
"provider": "limit-order",
"annotation": "Limit Order: USDC → XO",
"data": {
"type": "evm",
"chain": "Ethereum",
"chainId": 1,
"to": "0xSwapFacilityAddress",
"data": "0xabcdef...",
"value": "0"
}
}
]
}
Error Responses
All errors share the { code, message, requestId } body shape.
| Status | Code | Description |
|---|---|---|
400 | BadPermitRequest | Malformed or invalid body |
410 | PermitExpired | The recipe's permit deadline has passed; re-quote to get a fresh recipe |
422 | InvalidPermitSignature | ecrecover does not match owner, or the signature is over a wrong domain |
500 | PermitBuildError | Unexpected internal failure while building the payloads |
Self-encoding
You do not have to send the signature back to the server. The recipe carries everything needed to build the final transaction calldata locally — useful if you would rather not round-trip the signature. The flow is:
- Sign
recipe.typedDatato get the 65-byte permit signature. - Encode the call named by
recipe.functionSignature, usingrecipe.operationargs plus the signature. - Send the transaction to
recipe.spenderwithrecipe.value.
The example below signs the typed data with Viem and encodes an M Swap Facility
swapWithPermit(...) call — the bytes signature overload:
import { encodeFunctionData, parseAbi, type WalletClient } from "viem";
import type { components } from "./m0-swap"; // From type generation
type PermitRecipe = components["schemas"]["PermitRecipe"];
// The standard EIP-2612 Permit type set (omitted from the recipe payload).
const eip2612Types = {
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
} as const;
async function encodeSwapWithPermit(
recipe: PermitRecipe,
wallet: WalletClient,
) {
// 1. Sign the recipe's typed data -> 65-byte permit signature.
const signature = await wallet.signTypedData({
account: wallet.account!,
domain: recipe.typedData.domain,
types: eip2612Types,
primaryType: "Permit",
message: recipe.typedData.message,
});
// 2. Encode the call straight from `recipe.functionSignature` — no need to
// hardcode the ABI. For the M Swap Facility this is the `bytes` overload.
const op = recipe.operation; // type: "swap"
const functionName = recipe.functionSignature.split("(")[0];
const abi = parseAbi([`function ${recipe.functionSignature}`]);
const data = encodeFunctionData({
abi,
functionName,
args: [
op.extensionIn,
op.extensionOut,
BigInt(op.amount),
op.recipient,
BigInt(recipe.typedData.message.deadline),
signature,
],
});
// 3. The transaction: send to recipe.spender with recipe.value.
return {
to: recipe.spender as `0x${string}`,
data,
value: BigInt(recipe.value),
};
}
ammSwap operation does not take a packed bytes signature — its selfPermit expects the
signature split into v, r, and s. Slice the 65-byte signature with Viem
(r = slice(sig, 0, 32), s = slice(sig, 32, 64), v = hexToNumber(slice(sig, 64, 65))) before
encoding the call.Notes
- Permit approvals are EVM-only and opt-in — use the standard
POST /quoteflow when you do not need a single-transaction approval. - A quote is no longer buildable after
expiresAt(the earliest permit deadline across its recipes). Re-quote to get fresh recipes. minAmountOutis the floor that must be met or the transaction reverts; it equalsamountOutfor exact-output providers. AMM legs (e.g. Uniswap) apply the server's standard slippage tolerance.- The build step encodes the same
Payloadshape as/quote, so payload execution is identical.