ORCHESTRATION API

Permit Approvals

Opt-in EIP-2612 permit flow that replaces a separate on-chain approval with a single transaction backed by an off-chain signature.

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:

  1. POST /permit/quote returns the quote economics plus a permit recipe — the EIP-712 typed data to sign — for each leg of the route.
  2. The client signs each recipe's typedData, then either calls POST /permit/build with the signed recipes to get executable payloads, or encodes the call locally (see Self-encoding).
EVM-only and opt-in. Permit approvals are an EVM-only feature you opt into. EIP-2612 requires the input token to implement permit() — all M and wM extensions and USDC do. Routes whose input token does not support permit() return 404 PermitNotSupported.
TypeScript Types: Generate types for this API with a single command. See Type Generation.
API Reference: For detailed schema definitions and interactive testing, see the API Reference.

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.

StatusCodeDescription
400BadPermitRequestMalformed or invalid body (missing fields, unsupported asset, bad params)
404PermitNotSupportedThe route is valid but not permit-capable (input token has no permit())
500PermitQuoteErrorUnexpected 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:

FieldTypeRequiredDescription
signedRecipesSignedRecipe[]YesThe signed recipes, one per leg

Each SignedRecipe:

FieldTypeRequiredDescription
recipePermitRecipeYesThe recipe echoed verbatim from /permit/quote
signaturestringYesThe 65-byte hex permit signature over recipe.typedData

Response

PermitBuildResponse:

FieldTypeDescription
requestIdstringIdentifier for the request, echoed in error bodies
payloadsPayload[]Executable payloads (same shape as /quote)
issuesPermitIssuesOptional 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.

StatusCodeDescription
400BadPermitRequestMalformed or invalid body
410PermitExpiredThe recipe's permit deadline has passed; re-quote to get a fresh recipe
422InvalidPermitSignatureecrecover does not match owner, or the signature is over a wrong domain
500PermitBuildErrorUnexpected 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:

  1. Sign recipe.typedData to get the 65-byte permit signature.
  2. Encode the call named by recipe.functionSignature, using recipe.operation args plus the signature.
  3. Send the transaction to recipe.spender with recipe.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),
  };
}
Uniswap's 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 /quote flow 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.
  • minAmountOut is the floor that must be met or the transaction reverts; it equals amountOut for exact-output providers. AMM legs (e.g. Uniswap) apply the server's standard slippage tolerance.
  • The build step encodes the same Payload shape as /quote, so payload execution is identical.
Copyright © M0 Foundation 2026