Demo docs
A reference integration of @baking-bad/free-route-tezos-x — an SDK that turns free-route swaps on Tezos X into ready-to-sign Tezos operations. This app exercises two flows: a standalone any-token ↔ any-token swap (the Bridge), and paying any ERC20 for an XTZ-priced asset (e.g. an objkt NFT), composed into ready-to-sign ops. Either flow can be driven from either side — a Michelson op-group (Temple) or an EVM tx batch (MetaMask).
Tezos X is one chain with two interfaces — Michelson (Tezlink) and EVM (Etherlink) — that can call each other atomically in a single transaction. The SDK prepares the calls for whichever side signs:
- Michelson-native (your tz1 signs) — the op-group calls the EVM-side router via call_evm as your evm alias; the swap’s native-XTZ output auto-forwards to your tz1, which then funds a Michelson op (e.g. the marketplace fulfill). One atomic, single-signature group. Before the wallet prompt the group is sized on the node with estimateOperation — the call_evm ops need a margin over a bare wallet estimate, and a group that would revert (e.g. the ask is gone) fails here, before signing.
- EVM-native (your 0x signs) — the /swap response is a raw EVM tx you send directly; to reach a Michelson contract you call callMichelson on the gateway as your michelson alias (a KT1), so the NFT lands there. A wallet batches approve + swap + fulfill via EIP-5792, or sends them sequentially.
Either way the SDK only prepares the ops — you sign and broadcast with your own wallet.
For brevity the per-page samples build the client inline. A real dApp — this one included — keeps the keyed free-route reads on its own server and proxies them, so the API key never reaches the browser. The SDK ships the pieces for exactly this split: a reads-only FreeRouteClient for the server, serialize* / parse* DTO helpers so quotes and swaps cross the HTTP boundary without losing their bigint fields, and a FreeRouteApi interface the browser implements as a thin, keyless client over those endpoints. The gateway-bound op builders (createMichelsonOpsBuilder / createEvmOpsBuilder) then turn those keyless reads into ready-to-sign ops in the browser — see the tail of the Client sample. See the source.
The per-page samples use the bundled facade (FreeRouteTezosX, one place holds the key) for brevity. This app uses the split above — keyed FreeRouteClient on the server, keyless reads + builders on the client. The SDK README covers all three tiers (facade · client+builder split · low-level builders).
// lib/server/freeRoute.ts -- the free-route API key lives here, never in the browser
import 'server-only';
import { FreeRouteClient, tezosXPreviewnet, serializeQuote, serializeSwap } from '@baking-bad/free-route-tezos-x';
export const freeRoute = new FreeRouteClient({
baseUrl: process.env.FREE_ROUTE_API!,
chainId: tezosXPreviewnet.chainId,
apiKey: process.env.FREE_ROUTE_API_KEY!, // server-only env, never NEXT_PUBLIC
});
// thin proxy endpoints -- each route.ts exports a GET over the keyed client above
import type { NextRequest } from 'next/server';
import { parseQuoteQuery, parseSwapQuery } from '@baking-bad/free-route-tezos-x'; // SDK codec: validate untrusted params
// app/api/free-route/tokens/route.ts -- plain JSON, no bigints, no serialize step
export async function GET() {
const tokens = await freeRoute.getTokens();
return Response.json(tokens);
}
// app/api/free-route/quote/route.ts -- serialize* turns the model into a wire DTO (JSON can't carry bigint)
export async function GET(req: NextRequest) {
const query = parseQuoteQuery(req.nextUrl.searchParams); // validate untrusted params
const quote = await freeRoute.getQuote(query);
return Response.json(serializeQuote(quote));
}
// app/api/free-route/swap/route.ts
export async function GET(req: NextRequest) {
const query = parseSwapQuery(req.nextUrl.searchParams);
const swap = await freeRoute.getSwap(query);
return Response.json(serializeSwap(swap));
}// lib/freeRoute.ts -- a keyless client implementing the SDK's FreeRouteApi, via our proxy
import {
parseQuote, parseSwap, serializeQuoteQuery, serializeSwapQuery,
type FreeRouteApi, type FreeRouteToken, type QuoteResponseDto, type SwapResponseDto,
} from '@baking-bad/free-route-tezos-x';
// same-origin fetch to our proxy endpoints (the key is injected server-side)
async function get<T>(path: string, params?: URLSearchParams): Promise<T> {
const qs = params ? '?' + params : '';
const res = await fetch('/api/free-route/' + path + qs);
return res.json();
}
// FreeRouteApi is the SDK's read surface; serialize the query, parse each wire DTO back into a typed model.
export const freeRoute: FreeRouteApi = {
getTokens: () => get<FreeRouteToken[]>('tokens'),
getQuote: async (q) => parseQuote(await get<QuoteResponseDto>('quote', serializeQuoteQuery(q))), // bigints restored
getSwap: async (q) => parseSwap(await get<SwapResponseDto>('swap', serializeSwapQuery(q))),
};
// ── elsewhere (browser): turn those keyless reads into ops with a network-keyed builder.
// The builder needs only the gateway, so no API key leaves the server. This is lib/opsMichelson.ts. ──
import { createMichelsonOpsBuilder, tezosXPreviewnet } from '@baking-bad/free-route-tezos-x';
const michelson = createMichelsonOpsBuilder(tezosXPreviewnet.michelsonGateway); // pure builder, runs anywhere
const swap = await freeRoute.getSwap({ src, dst, amount, isExactOut: true, from, receiver });
const swapOps = michelson.buildSwapOperation({ swap, srcAddress: src });
// sign swapOps with your Beacon/Taquito wallet — or compose with a marketplace op (see the Buyer example).
// The EVM side mirrors this: createEvmOpsBuilder(tezosXPreviewnet.evmGateway) -> evm.buildSwapTransaction(...).Pages
Buyer
/buyerPay any EVM ERC20 for an XTZ-priced objkt NFT — swap to XTZ, then fulfill the ask, composed into one signed batch. From Temple it’s a single-signature Michelson op-group; from MetaMask, an EVM tx batch that reaches objkt via callMichelson. The NFT goes to the signer by default, or to any Michelson address you pass as the objkt proxy_for (the optional recipient).
import { TezosToolkit } from '@taquito/taquito';
import { InMemorySigner } from '@taquito/signer';
import {
FreeRouteTezosX, tezosXMainnet, XTZ, toEvmUnits, targetForMinOut,
michelsonToEvmAlias, resolveApproval, objkt, buildBatchTransaction,
} from '@baking-bad/free-route-tezos-x';
// your Taquito toolkit + signer (a Beacon wallet in a browser dApp)
const tezos = new TezosToolkit(MICHELSON_RPC);
tezos.setSignerProvider(new InMemorySigner(SECRET_KEY));
const freeRoute = new FreeRouteTezosX({
baseUrl: FREE_ROUTE_API,
network: tezosXMainnet,
apiKey: FREE_ROUTE_API_KEY, // free-route API key
});
const buyerAddress = await tezos.signer.publicKeyHash(); // your Michelson (tz1) address
const buyerAlias = michelsonToEvmAlias(buyerAddress); // its EVM identity (holds the ERC20)
const payToken = (await freeRoute.getTokens()).find((token) => token.symbol === 'USDC')!;
const priceMutez = 4_000n; // the objkt ask price (read it from the marketplace)
const slippageBps = 200; // 2%
// exact-out: size the XTZ out so the on-chain floor still covers the price
const minOutTarget = targetForMinOut(priceMutez, slippageBps);
const swapAmount = toEvmUnits(minOutTarget, XTZ.address); // mutez -> wei for the EVM API
const swap = await freeRoute.getSwap({
src: payToken.address,
dst: XTZ.address,
amount: swapAmount,
isExactOut: true,
from: buyerAlias,
receiver: buyerAlias,
slippageBps,
});
// read the on-chain allowance -> pick the minimal safe approval mode (none / approve / reset+approve)
const approval = await resolveApproval({
evmRpc: EVM_RPC,
token: payToken.address,
owner: buyerAlias,
spender: swap.tx.to,
amount: swap.srcAmount,
});
// approve(s) + swap, composed with the objkt fulfill -> one atomic group
const swapOps = freeRoute.michelson.buildSwapOperation({
swap,
srcAddress: payToken.address,
approval,
});
const fulfill = objkt.buildMichelsonFulfillAskOperation({
marketplace: OBJKT_MARKETPLACE,
askId: '1',
editions: 1,
amountMutez: priceMutez,
// recipient: 'tz1… | KT1…', // optional: send the NFT to another Michelson address (objkt proxy_for)
});
const ops = buildBatchTransaction(swapOps, fulfill);
// size the group on the node (call_evm margin + end-to-end check before signing)
const readyOps = await freeRoute.michelson.estimateOperation({ tezos, ops });
const op = await tezos.contract.batch().with(readyOps).send(); // a single signature
await op.confirmation();Bridge
/bridgeA standalone swap of any token to any token (XTZ ↔ ERC20, ERC20 ↔ ERC20), exact-input. The same builders as the buy, minus the marketplace op. The output lands on the signer by default, or on any EVM address you pass as the receiver.
import { TezosToolkit } from '@taquito/taquito';
import { InMemorySigner } from '@taquito/signer';
import {
FreeRouteTezosX, tezosXMainnet, XTZ, toEvmUnits, isXtz,
michelsonToEvmAlias, resolveApproval,
} from '@baking-bad/free-route-tezos-x';
const tezos = new TezosToolkit(MICHELSON_RPC);
tezos.setSignerProvider(new InMemorySigner(SECRET_KEY));
const freeRoute = new FreeRouteTezosX({
baseUrl: FREE_ROUTE_API,
network: tezosXMainnet,
apiKey: FREE_ROUTE_API_KEY, // free-route API key
});
const myAddress = await tezos.signer.publicKeyHash();
const alias = michelsonToEvmAlias(myAddress); // EVM identity that runs the swap
const slippageBps = 50; // 0.5%
// pick any pair + the amount in src base units
const tokens = await freeRoute.getTokens();
const src = tokens.find((token) => token.symbol === 'USDC')!;
const dst = XTZ; // receive native XTZ
const amount = 1_000_000n; // 1 USDC
// exact-in: any token -> any token (XTZ <-> ERC20, ERC20 <-> ERC20)
const swapAmount = toEvmUnits(amount, src.address); // to wei for the EVM API
const swap = await freeRoute.getSwap({
src: src.address,
dst: dst.address,
amount: swapAmount,
isExactOut: false,
from: alias,
receiver: alias, // optional: any EVM 0x address — the swap output lands here (defaults to the sender)
slippageBps,
});
// native XTZ carries value as msg.value (no approve); an ERC20 picks the minimal safe mode (none / approve / reset+approve)
const approval = isXtz(src.address)
? 'none'
: await resolveApproval({
evmRpc: EVM_RPC,
token: src.address,
owner: alias,
spender: swap.tx.to,
amount: swap.srcAmount,
});
// approve(s) + swap -> one atomic group; native-XTZ output auto-forwards to your Michelson address
const ops = freeRoute.michelson.buildSwapOperation({
swap,
srcAddress: src.address,
approval,
});
const readyOps = await freeRoute.michelson.estimateOperation({ tezos, ops }); // size on the node before signing
const op = await tezos.contract.batch().with(readyOps).send(); // a single signature
await op.confirmation();Seller
/sellerMints test NFTs and lists each as an XTZ-priced ask on objkt, so the Buyer page has something to purchase. Supporting flow for the demo.
My NFTs
/ownedA read-only view of the NFTs owned by the connected wallet’s Michelson holder — your tz1 (Temple) or the account’s KT1 alias (MetaMask) — from the test collection. Confirms a completed buy.
From a native EVM account the builders return an EvmTxRequest[] batch — approve + swap on the router, then the marketplace fulfill via callMichelson. The NFT lands on the account’s KT1 alias. Send it with one EIP-5792 wallet_sendCalls (or sequentially where unsupported).
import {
FreeRouteTezosXEvm, tezosXPreviewnet, XTZ, toEvmUnits, targetForMinOut,
evmToMichelsonAlias, resolveApproval, objkt,
} from '@baking-bad/free-route-tezos-x/evm';
const freeRoute = new FreeRouteTezosXEvm({
baseUrl: FREE_ROUTE_API,
network: tezosXPreviewnet,
apiKey: FREE_ROUTE_API_KEY, // free-route API key
});
const buyerAccount = '0x…'; // the MetaMask account (holds the ERC20, pays gas)
const buyerAlias = evmToMichelsonAlias(buyerAccount); // the KT1 where the NFT lands
const payToken = (await freeRoute.getTokens()).find((token) => token.symbol === 'USDC')!;
const priceMutez = 4_000n; // the objkt ask price (read it from the marketplace)
const slippageBps = 200; // 2%
// exact-out: size the XTZ out so the on-chain floor still covers the price
const minOutTarget = targetForMinOut(priceMutez, slippageBps);
const swapAmount = toEvmUnits(minOutTarget, XTZ.address); // mutez -> wei for the EVM API
const swap = await freeRoute.getSwap({
src: payToken.address,
dst: XTZ.address,
amount: swapAmount,
isExactOut: true,
from: buyerAccount,
receiver: buyerAccount,
slippageBps,
});
// read the on-chain allowance -> pick the minimal safe approval mode (none / approve / reset+approve)
const approval = await resolveApproval({
evmRpc: EVM_RPC,
token: payToken.address,
owner: buyerAccount,
spender: swap.tx.to,
amount: swap.srcAmount,
});
// approve(s) + swap, composed with the objkt fulfill (via callMichelson) -> one EvmTxRequest[] batch
const swapTxs = freeRoute.evm.buildSwapTransaction({ swap, srcAddress: payToken.address, approval });
const fulfill = objkt.buildEvmFulfillAskTransaction({
marketplace: OBJKT_MARKETPLACE,
askId: '1',
editions: 1,
amountMutez: priceMutez,
// recipient: 'tz1… | KT1…', // optional: send the NFT to another Michelson address (objkt proxy_for)
});
// EIP-5792 batch where supported; otherwise send sequentially (this previewnet). NFT lands on buyerAlias.
await walletClient.sendCalls({ calls: [...swapTxs, fulfill] });Running on Tezos X previewnet.