Skip to content

Full Integration Example

A complete, copy-paste TypeScript example wiring quote → sign (EIP-7702) → submit → poll. The user signs two things off-chain (the EIP-712 order signature + an EIP-7702 authorization); a relayer pays the gas.

Setup

bash
npm install viem

Full example

typescript
import {
  createWalletClient,
  http,
} from "viem";
import { base } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

// ---- Config ----------------------------------------------------------------

const ORCHESTRATOR_URL = "https://api.sweepster.xyz";
const SOURCE_CHAIN_ID  = 8453; // Base
const BUILDER_API_KEY  = process.env.SWEEPSTER_API_KEY ?? "";

// ---- Helpers ---------------------------------------------------------------

type Hex = `0x${string}`;

interface SweepOrderWire {
  owner: Hex; tokenIn: Hex; amountIn: string; feeAmount: string;
  builder: Hex; builderFee: string; tokenOut: Hex; minAmountOut: string;
  destination: Hex; swapTarget: Hex; swapCalldata: Hex;
  deadline: string; nonce: string; destChainId: string;
}

interface AuthorizationWire {
  address: Hex; chainId: number; nonce: number; r: Hex; s: Hex; yParity: number;
}

/**
 * EIP-712 type definition for the SweepOrder — all 14 fields, matching the
 * on-chain SWEEP_ORDER_TYPEHASH (see Signing guide).
 */
const SWEEP_ORDER_TYPES = {
  SweepOrder: [
    { name: "owner", type: "address" }, { name: "tokenIn", type: "address" },
    { name: "amountIn", type: "uint256" }, { name: "feeAmount", type: "uint256" },
    { name: "builder", type: "address" }, { name: "builderFee", type: "uint256" },
    { name: "tokenOut", type: "address" }, { name: "minAmountOut", type: "uint256" },
    { name: "destination", type: "address" }, { name: "swapTarget", type: "address" },
    { name: "swapCalldata", type: "bytes" }, { name: "deadline", type: "uint256" },
    { name: "nonce", type: "uint256" }, { name: "destChainId", type: "uint256" },
  ],
} as const;

/**
 * Builds the EIP-712 typed data the EOA signs via signTypedData (see Signing
 * guide). On-chain it is verified as
 * ECDSA.recover(toTypedDataHash(domainSeparator, structHash(order)), sig)
 * == address(this) == owner. `verifyingContract` is the delegated EOA (== owner)
 * and `chainId` is the SOURCE chain id.
 */
function buildDelegateTypedData(order: SweepOrderWire, sourceChainId: number) {
  return {
    domain: {
      name: "Sweepster",
      version: "1",
      chainId: BigInt(sourceChainId),
      verifyingContract: order.owner,
    },
    types: SWEEP_ORDER_TYPES,
    primaryType: "SweepOrder",
    message: {
      owner: order.owner, tokenIn: order.tokenIn,
      amountIn: BigInt(order.amountIn), feeAmount: BigInt(order.feeAmount),
      builder: order.builder, builderFee: BigInt(order.builderFee),
      tokenOut: order.tokenOut, minAmountOut: BigInt(order.minAmountOut),
      destination: order.destination, swapTarget: order.swapTarget,
      swapCalldata: order.swapCalldata, deadline: BigInt(order.deadline),
      nonce: BigInt(order.nonce), destChainId: BigInt(order.destChainId),
    },
  } as const;
}

type IntentStatus = "quoted" | "submitted" | "settled_source" | "delivered" | "failed";

async function pollIntent(id: string): Promise<{ status: IntentStatus; txHash?: Hex }> {
  const TERMINAL: IntentStatus[] = ["delivered", "failed"];
  for (let i = 0; i < 100; i++) {
    const res = await fetch(`${ORCHESTRATOR_URL}/intents/${id}`);
    if (!res.ok) throw new Error(`Poll failed: ${res.status}`);
    const intent = await res.json() as { status: IntentStatus; txHash?: Hex };
    console.log(`[poll ${i}] ${intent.status}`);
    if (TERMINAL.includes(intent.status)) return intent;
    await new Promise((r) => setTimeout(r, 3000));
  }
  throw new Error("Timed out waiting for intent");
}

// ---- Main ------------------------------------------------------------------

async function sweep(params: {
  userPrivateKey: Hex;
  tokenIn: Hex;
  amountIn: string;     // decimal string in token units
  tokenOut: Hex;
  destination: Hex;
  destChainId: number;
}) {
  const account = privateKeyToAccount(params.userPrivateKey);

  // 1. Quote
  const quoteRes = await fetch(`${ORCHESTRATOR_URL}/quote`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      owner:             account.address,
      tokenIn:           params.tokenIn,
      amountIn:          params.amountIn,
      tokenOut:          params.tokenOut,
      destination:       params.destination,
      destChainId:       params.destChainId,
      tier:              "tier1",
      route:             `base->${params.destChainId === 8453 ? "base" : "dest"}`,
      subscriptionActive: false,
      userId:            account.address,
      apiKey:            BUILDER_API_KEY,
    }),
  });

  if (!quoteRes.ok) {
    const err = await quoteRes.json() as { error: string };
    throw new Error(`Quote failed: ${err.error}`);
  }

  const { quote, order } = await quoteRes.json() as {
    quote: { id: string; expiresAtMs: number; total: string; crossChain: boolean; settlementAddress: Hex };
    order: SweepOrderWire;
  };

  console.log(`Quote: id=${quote.id} fee=${quote.total} expires=${new Date(quote.expiresAtMs).toISOString()}`);

  // 2. Sign TWO things off-chain (EIP-7702, gasless — no approval, no allowance)
  const walletClient = createWalletClient({
    account,
    chain: base,
    transport: http(),
  });

  // 2a. EIP-712 signTypedData the SweepOrder → the `bytes signature`
  const signature = await walletClient.signTypedData({
    account,
    ...buildDelegateTypedData(order, SOURCE_CHAIN_ID),
  });

  // 2b. Sign the EIP-7702 authorization delegating the EOA to the SweepDelegate impl
  const signedAuth = await walletClient.signAuthorization({
    account,
    contractAddress: quote.settlementAddress, // per-chain SweepDelegate impl
    chainId: SOURCE_CHAIN_ID,
  });
  const authorization: AuthorizationWire = {
    address: signedAuth.address as Hex,
    chainId: signedAuth.chainId,
    nonce: signedAuth.nonce,
    r: signedAuth.r as Hex,
    s: signedAuth.s as Hex,
    yParity: signedAuth.yParity ?? 0,
  };

  console.log("Signed:", signature.slice(0, 20) + "...");

  // 3. Submit (signature + REQUIRED authorization). The relayer pays the gas.
  const submitRes = await fetch(`${ORCHESTRATOR_URL}/intents`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ quoteId: quote.id, signature, authorization }),
  });

  if (!submitRes.ok) {
    const err = await submitRes.json() as { error: string };
    throw new Error(`Submit failed: ${err.error}`);
  }

  const intent = await submitRes.json() as { id: string; status: IntentStatus };
  console.log("Intent submitted:", intent.id, intent.status);

  // 4. Poll
  const final = await pollIntent(intent.id);
  if (final.status === "delivered") {
    console.log("Sweep delivered! txHash:", final.txHash);
  } else {
    throw new Error("Sweep failed");
  }

  return final;
}

// ---- Run -------------------------------------------------------------------

sweep({
  userPrivateKey: process.env.USER_PRIVATE_KEY as Hex,
  tokenIn:        "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",  // USDC on Base
  amountIn:       "1000000",   // 1 USDC
  tokenOut:       "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",  // USDC on Arbitrum
  destination:    "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
  destChainId:    42161,
}).catch(console.error);

Notes

  • SWEEPSTER_API_KEY — your builder API key (self-serve at dashboard.sweepster.xyz)
  • USER_PRIVATE_KEY — in production, this comes from the wallet; never hardcode it
  • The settlement address (quote.settlementAddress) is the per-chain SweepDelegate impl the EOA delegates to — taken from the quote
  • Both signatures are off-chain and gasless; the relayer broadcasts the EIP-7702 type-4 transaction and pays the gas
  • The route hint is "base->base" for same-chain, "base->dest" for cross-chain

Environment variables

bash
SWEEPSTER_API_KEY=sk_live_...
USER_PRIVATE_KEY=0x...

Non-custodial · Atomic · Audit + bug bounty planned before mainnet value.