Skip to content

Instantly share code, notes, and snippets.

@thor-wong
Created January 20, 2026 16:39
Show Gist options
  • Select an option

  • Save thor-wong/2438c0e3970e22c75f4302ac2d75ac1b to your computer and use it in GitHub Desktop.

Select an option

Save thor-wong/2438c0e3970e22c75f4302ac2d75ac1b to your computer and use it in GitHub Desktop.
KITE Stablecoin Gasless Transfer Service Demo
import { ethers } from "ethers";
import { randomBytes } from "crypto";
import * as dotenv from "dotenv";
dotenv.config();
const RPC_URL = process.env.MAINNET_RPC_URL ?? "https://rpc.gokite.ai";
const BACKEND_URL = process.env.MAINNET_BACKEND_URL ?? "https://gasless.gokite.ai/mainnet";
const CONTRACT_ADDRESS = process.env.MAINNET_CONTRACT_ADDRESS ?? "0x7aB6f3ed87C42eF0aDb67Ed95090f8bF5240149e";
const USER_PRIVATE_KEY = process.env.USER_PRIVATE_KEY!;
const TRANSFER_TO = process.env.MAINNET_TRANSFER_TO ?? "0x2Ba214b0AfCa4Cad5A6A9b8AF05032cE574F16e6"; // defaults to user address if unset
const EIP712_NAME = process.env.MAINNET_EIP712_NAME ?? "Bridged USDC (Kite AI)";
const EIP712_VERSION = process.env.MAINNET_EIP712_VERSION ?? "2";
const TRANSFER_VALUE = "10000"
function requireEnv(value: string, name: string): string {
if (!value) {
throw new Error(`${name} must be set`);
}
return value;
}
async function main() {
const rpcUrl = requireEnv(RPC_URL, "MAINNET_RPC_URL");
const contractAddress = requireEnv(CONTRACT_ADDRESS, "MAINNET_CONTRACT_ADDRESS");
const userKey = requireEnv(USER_PRIVATE_KEY, "MAINNET_USER_PRIVATE_KEY");
const provider = new ethers.JsonRpcProvider(rpcUrl);
const userWallet = new ethers.Wallet(userKey, provider);
const toAddress = TRANSFER_TO ?? userWallet.address;
const latest = await provider.getBlock("latest");
if (!latest) throw new Error("cannot fetch latest block");
const latest_block_now = BigInt(latest.timestamp);
const now = BigInt(Math.floor(Date.now() / 1000));
const domain = {
name: EIP712_NAME,
version: EIP712_VERSION,
chainId: (await provider.getNetwork()).chainId,
verifyingContract: contractAddress
};
const types = {
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" }
]
};
const validAfter = latest_block_now - 1n;
const validBefore = now + 25n;
const buildPayload = async () => {
const message = {
from: userWallet.address,
to: toAddress,
value: TRANSFER_VALUE,
validAfter,
validBefore,
nonce: `0x${randomBytes(32).toString("hex")}`
};
const signature = await userWallet.signTypedData(domain, types, message);
const sig = ethers.Signature.from(signature);
return {
from: message.from,
to: message.to,
value: message.value.toString(),
validAfter: message.validAfter.toString(),
validBefore: message.validBefore.toString(),
tokenAddress: contractAddress,
nonce: message.nonce,
v: sig.v,
r: sig.r,
s: sig.s
};
};
console.log("Submitting to backend:", BACKEND_URL);
console.log("From:", userWallet.address);
console.log("To:", toAddress);
console.log("Value:", TRANSFER_VALUE.toString());
console.log("EIP712 domain:", EIP712_NAME, EIP712_VERSION);
const payload = await buildPayload();
console.log("Request payload:", JSON.stringify(payload, null, 2));
const res = await fetch(BACKEND_URL, {
method: "POST",
body: JSON.stringify(payload)
});
const body = await res.json().catch(() => ({}));
if (!res.ok) {
console.error("Submit failed", res.status, body);
process.exit(1);
}
console.log("Submit success:", body);
if (body?.txHash) {
console.log("Fetching receipt...");
const receipt = await provider.waitForTransaction(body.txHash);
if (!receipt) {
console.log("Receipt not found.");
return;
}
const statusLabel = receipt.status === 1 ? "success" : "failed";
console.log(`Receipt: from=${receipt.from}, status=${statusLabel}`);
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment