Skip to main content

Wallet Flow

  1. List supported tokens.
  2. Quote the swap.
  3. Check quote expiry.
  4. Check Permit2 allowance.
  5. If needed, send an ERC-20 approval transaction and wait for it.
  6. Ask the user to sign the returned EIP-712 typed data.
  7. Submit the order with a clientOrderId.
  8. Poll status until the order is filled, failed, or expired.
BaiBai currently supports gasless EIP-712 orders only. Traditional/non-gasless orders are coming soon.

Constants

const API = "https://alpha.baibai.cx/api/v1";
const CHAIN_ID = 8453;
const WETH_BASE = "0x4200000000000000000000000000000000000006";

Error Handling Helper

All REST errors use this shape:
{
  "error": {
    "code": "BAD_REQUEST",
    "message": "Unsupported token."
  }
}
Use HTTP status codes for retry behavior. 429 responses include retry-after.
type BaiBaiError = Error & {
  code?: string;
  retryAfterSeconds?: number;
  status: number;
};

async function requestJson<T>(path: string, init?: RequestInit): Promise<T> {
  const response = await fetch(`${API}${path}`, {
    ...init,
    headers: {
      "content-type": "application/json",
      ...init?.headers,
    },
  });

  const body = await response.json().catch(() => ({}));

  if (!response.ok) {
    const error = new Error(
      body?.error?.message ?? `BaiBai request failed: ${response.status}`,
    ) as BaiBaiError;
    error.status = response.status;
    error.code = body?.error?.code;
    error.retryAfterSeconds = Number(response.headers.get("retry-after") ?? 0);
    throw error;
  }

  return body as T;
}

List Tokens

Use /tokens to discover supported ERC-20 tokens and decimals.
const tokens = await requestJson<
  Array<{
    chainId: number;
    address: string;
    symbol: string;
    name: string;
    decimals: number;
    logoURI?: string;
  }>
>(`/tokens?chainId=${CHAIN_ID}`);

Quote

Amounts are token base units as decimal strings.
const quote = await requestJson<{
  id: string;
  expiration: string;
  quote: {
    sellAmount: string;
    buyAmount: string;
    minBuyAmount?: string;
    validTo: number;
  };
  permit2: {
    allowanceTarget: `0x${string}`;
    spender: `0x${string}`;
    typedData: unknown;
  };
}>("/quote", {
  method: "POST",
  body: JSON.stringify({
    chainId: CHAIN_ID,
    from: owner,
    receiver: owner,
    sellToken: usdc,
    buyToken: WETH_BASE,
    kind: "sell",
    sellAmountBeforeFee: "1000000",
    slippageBps: 50,
    routingPreference: "auto",
    signingScheme: "eip712",
  }),
});
For exact-output swaps, use kind: "buy" and buyAmountAfterFee.

Routing Preference

The optional routingPreference field controls how BaiBai chooses liquidity:
ValueMeaning
autoDefault. Choose the best available route.
preferBaiBaiPrefer BaiBai Prop AMM liquidity when it is competitive.
onlyBaiBaiOnly quote BaiBai Prop AMM liquidity; the quote fails if BaiBai has no route or enough liquidity.
Use auto unless your wallet intentionally wants to bias execution.

Check Quote Expiry

Check expiry before signing. If approval, wrapping, or user confirmation takes time, request a fresh quote.
function quoteIsUsable(quote: { expiration: string; quote: { validTo: number } }) {
  const nowSeconds = Math.floor(Date.now() / 1000);
  const expiresAtMs = Date.parse(quote.expiration);

  return expiresAtMs > Date.now() + 10_000 && quote.quote.validTo > nowSeconds + 10;
}

if (!quoteIsUsable(quote)) {
  throw new Error("Quote expired. Request a fresh quote before signing.");
}

Approve Permit2

Approve quote.permit2.allowanceTarget, not quote.permit2.spender.
const allowance = await erc20.read.allowance([
  owner,
  quote.permit2.allowanceTarget,
]);

if (allowance < BigInt(quote.quote.sellAmount)) {
  const hash = await erc20.write.approve([
    quote.permit2.allowanceTarget,
    quote.quote.sellAmount,
  ]);

  await publicClient.waitForTransactionReceipt({ hash });
}
allowanceTarget is the Permit2 contract that receives the ERC-20 approval. spender is the BaiBai settlement spender included in the signed typed data.

Sign

Sign quote.permit2.typedData exactly as returned.
if (!quoteIsUsable(quote)) {
  throw new Error("Quote expired. Request a fresh quote before signing.");
}

const signature = await provider.request({
  method: "eth_signTypedData_v4",
  params: [owner, JSON.stringify(quote.permit2.typedData)],
});

Submit

Use clientOrderId to make submit retries idempotent.
const clientOrderId = crypto.randomUUID();

const order = await requestJson<{
  orderId: string;
  quoteId: string;
  status: "pending" | "filled" | "failed" | "expired";
}>("/orders", {
  method: "POST",
  body: JSON.stringify({
    quoteId: quote.id,
    signature,
    clientOrderId,
  }),
});
If the network fails after submit, retry the same request with the same clientOrderId. BaiBai returns the existing order instead of creating a duplicate.

Poll

Poll every 1-2 seconds, then back off to every 5 seconds for longer-running orders.
async function waitForOrder(orderId: string) {
  let delayMs = 1_500;

  while (true) {
    const status = await requestJson<{
      orderId: string;
      status: "pending" | "filled" | "failed" | "expired";
      txHash?: string;
      executedSellAmount?: string;
      executedBuyAmount?: string;
      error?: { code: string; message: string };
    }>(`/orders/${orderId}`);

    if (status.status !== "pending") {
      return status;
    }

    await new Promise((resolve) => setTimeout(resolve, delayMs));
    delayMs = Math.min(delayMs * 1.5, 5_000);
  }
}

Native ETH

Native ETH is not a gasless order token. Use WETH in quote and order requests.
  • Swapping from ETH: request an indicative quote with WETH as the sell token, then wrap ETH to WETH before signing/submitting.
  • Swapping to ETH: quote and buy WETH, then unwrap WETH after fill.
  • The wallet/caller owns the wrap and unwrap transactions.
Base WETH: 0x4200000000000000000000000000000000000006

Swapping from ETH

Quote with WETH before the user wraps:
const quote = await requestJson("/quote", {
  method: "POST",
  body: JSON.stringify({
    chainId: CHAIN_ID,
    from: owner,
    receiver: owner,
    sellToken: WETH_BASE,
    buyToken: usdc,
    kind: "sell",
    sellAmountBeforeFee: ethAmountWei,
    signingScheme: "eip712",
  }),
});
Then wrap, approve Permit2, check quote expiry, sign, and submit:
const wrapHash = await weth.write.deposit({ value: BigInt(ethAmountWei) });
await publicClient.waitForTransactionReceipt({ hash: wrapHash });

const approveHash = await weth.write.approve([
  quote.permit2.allowanceTarget,
  quote.quote.sellAmount,
]);
await publicClient.waitForTransactionReceipt({ hash: approveHash });

if (!quoteIsUsable(quote)) {
  // Wrapping or approval took too long. Request a fresh WETH quote.
}

Swapping to ETH

Quote WETH as the buy token, submit the order, then unwrap after it fills:
const quote = await requestJson("/quote", {
  method: "POST",
  body: JSON.stringify({
    chainId: CHAIN_ID,
    from: owner,
    receiver: owner,
    sellToken: usdc,
    buyToken: WETH_BASE,
    kind: "sell",
    sellAmountBeforeFee: usdcAmount,
    signingScheme: "eip712",
  }),
});

const status = await waitForOrder(order.orderId);

if (status.status === "filled" && status.executedBuyAmount) {
  const unwrapHash = await weth.write.withdraw([BigInt(status.executedBuyAmount)]);
  await publicClient.waitForTransactionReceipt({ hash: unwrapHash });
}

Retry Guidance

HTTP statusExampleAction
400unsupported token, expired quote, invalid signatureFix input or request a fresh quote/signature.
404unknown order IDVerify the order ID from submit.
429rate limitedWait for retry-after, then retry.
500temporary service errorRetry with backoff. If submit status is unknown, reuse the same clientOrderId.

Aggregator Flow

Quote, sign, submit, status, routing, and retry concepts.

API Reference

Endpoint fields, examples, errors, rate limits, and OpenAPI.

BaiBai Overview

Product overview and architecture.