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 viemFull 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-chainSweepDelegateimpl 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
routehint is"base->base"for same-chain,"base->dest"for cross-chain
Environment variables
bash
SWEEPSTER_API_KEY=sk_live_...
USER_PRIVATE_KEY=0x...Related
- Signing guide
- API Reference — Orchestrator
- MCP Server — let an AI agent drive the integration