Every exported symbol in src/, in dependency order. Each entry has the signature, what it does, when it throws, and a minimal example.
Types refer to
viem(Hex,Address,WalletClient,PublicClient). All amounts arebigintin token base units (so 1 USDC =1_000_000n, 1 VIRTUAL =10n ** 18n).
Pure constants. No functions, no I/O. Re-import from here whenever you need addresses or selectors so a future address change is a one-file edit.
export const BONDINGV5_PROXY = '0x1a540088125d00dd3990f9da45ca0859af4d3b01';
export const FROUTER_V3 = '0x02fe8ec3d9bbf7318eb54590bcc39198a8b47ded';
export const VIRTUAL = '0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b';
export const USDC = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913';
export const WETH = '0x4200000000000000000000000000000000000006';
export const UNIV3_ROUTER = '0x2626664c2603336E57B271c5C0b26F421741e481';
export const UNIV3_QUOTER = '0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a';
export const UNIV2_ROUTER = '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24';
export const WETH_VIRTUAL_POOL_500 = '0x9c087eb773291e50cf6c6a90ef0f4500e349b903';export const SEL_BALANCE_OF = '0x70a08231';
export const SEL_APPROVE = '0x095ea7b3';
export const SEL_ALLOWANCE = '0xdd62ed3e';
export const SEL_BOND_BUY = '0x706910ff';
export const SEL_BOND_SELL = '0xb233e056';
export const SEL_TOKEN_INFO = '0xf5dab711';
export const SEL_EXACT_IN_SINGLE = '0x04e45aaf';
export const SEL_EXACT_IN = '0xb858183f';
export const SEL_QUOTE_EXACT_IN_SINGLE = '0xc6a5026a';
export const SEL_QUOTE_EXACT_IN = '0xcdca1753';
export const SEL_V2_SWAP = '0x38ed1739';
export const SEL_V2_SWAP_FOT = '0x5c11d795';export const ERC20_ABI = [/* balanceOf, approve, allowance, decimals */];
export const BONDING_V5_ABI = [/* bondBuy, bondSell, tokenInfo */];
export const UNIV3_ROUTER_ABI = [/* exactInputSingle, exactInput */];
export const UNIV3_QUOTER_ABI = [/* quoteExactInputSingle, quoteExactInput */];Use the ABIs with viem's encodeFunctionData / decodeFunctionResult if you don't want to hand-roll calldata. The library uses the raw selectors for speed, but the ABIs are exported for callers who prefer the typed path.
async function getTokenInfo(
publicClient: PublicClient,
token: Address
): Promise<{ pair: Address; trading: boolean; tradingOnUniswap: boolean }>Returns the on-chain state of a Virtuals token. Decides which router to use.
| Field | Meaning |
|---|---|
pair |
The FPair (bonding curve) address. Useful for direct curve reads. |
trading |
true while the token is bonded. false after graduation. |
tradingOnUniswap |
true after graduation. Stays false while bonded. |
Throws if the address is not a Virtuals token (call reverts with no data).
const info = await getTokenInfo(client, '0xabc...');
if (info.trading) {
// bonded — use BondingV5
} else if (info.tradingOnUniswap) {
// graduated — use UniV3
} else {
throw new Error('token in invalid state');
}async function quoteBuyOnCurve(
publicClient: PublicClient,
token: Address,
virtualIn: bigint
): Promise<bigint>Returns the curve's expected output (in token base units) for spending virtualIn VIRTUAL. Uses the curve reserves directly — no fee-on-transfer adjustment. The actual delivered amount after a trade may be 0–100 bps lower for FoT tokens.
async function quoteSellOnCurve(
publicClient: PublicClient,
token: Address,
tokenIn: bigint
): Promise<bigint>Symmetric to quoteBuyOnCurve. Returns expected VIRTUAL output for selling tokenIn of token.
async function bondingV5BuyOnCurve(
wallet: WalletClient,
publicClient: PublicClient,
args: {
token: Address;
virtualIn: bigint;
slippageBps?: number; // default 100 (1%)
deadline?: bigint; // default now + 600s
}
): Promise<{
txHash: Hex;
quoted: bigint;
delivered: bigint; // pre/post balanceOf diff
slippageRealisedBps: number;
}>The main buy primitive. Steps:
quoteBuyOnCurveto get expected output.- Compute
minOut = quoted * (10000 - slippageBps) / 10000. approveIfNeeded(VIRTUAL, FROUTER_V3, virtualIn).balanceBefore = balanceOf(wallet, token).- Submit
bondBuy(token, virtualIn, minOut)toBONDINGV5_PROXY. - Wait for receipt.
balanceAfter = balanceOf(wallet, token).delivered = balanceAfter - balanceBefore.- If
delivered < minOut * 9500 / 10000→ throwBondingV5DeliveryShortfall. - Return.
Throws:
BondingV5GraduatedErrorif the token has already graduated. Caller should switch toswapV3.BondingV5InsufficientVirtualif wallet has <virtualInVIRTUAL.BondingV5DeliveryShortfallif FoT reconciliation fails (rare).- viem's standard
TransactionExecutionErrorfor tx-level failures.
const result = await bondingV5BuyOnCurve(wallet, publicClient, {
token: '0xabc...',
virtualIn: 10n ** 18n, // 1 VIRTUAL
slippageBps: 200, // 2%
});
console.log(`got ${result.delivered} tokens, tx ${result.txHash}`);async function bondingV5SellOnCurve(
wallet: WalletClient,
publicClient: PublicClient,
args: {
token: Address;
tokenIn: bigint;
slippageBps?: number;
deadline?: bigint;
}
): Promise<{ txHash: Hex; quoted: bigint; delivered: bigint; slippageRealisedBps: number }>Symmetric to the buy. Approves FRouterV3 for the token (not VIRTUAL), then calls bondSell on the V5 proxy. Same throw conditions.
async function swapV3(
wallet: WalletClient,
publicClient: PublicClient,
args: {
tokenIn: Address;
tokenOut: Address;
amountIn: bigint;
feeTier?: 100 | 500 | 3000 | 10000; // default 10000 for graduated tokens, 500 for stable pairs
slippageBps?: number; // default 100
deadline?: bigint;
forceV2?: boolean; // fallback to UniV2 router if no V3 liquidity
}
): Promise<{ txHash: Hex; quoted: bigint; delivered: bigint }>Post-graduation trading. Also used internally by usdcToVirtual / virtualToUsdc.
- Quote via
quoteV3Single(uses QuoterV2). - Compute
minOut. - Approve
SwapRouter02as spender oftokenIn. - Call
exactInputSingle.
If forceV2 === true, skips V3 entirely and uses swapExactTokensForTokensSupportingFeeOnTransferTokens on UniV2 router with path [tokenIn, tokenOut].
Throws:
V3NoLiquidityif the quote returns 0.- Standard tx errors.
async function quoteV3Single(
publicClient: PublicClient,
args: { tokenIn: Address; tokenOut: Address; amountIn: bigint; feeTier: number }
): Promise<bigint>eth_call against QuoterV2 → quoteExactInputSingle. Returns 0 if there's no pool at that fee tier.
async function usdcToVirtual(
wallet: WalletClient,
publicClient: PublicClient,
args: { amountIn: bigint; slippageBps?: number }
): Promise<{ txHashes: [Hex, Hex]; deliveredVirtual: bigint }>The funding-to-trading conversion. Always two hops. Returns both tx hashes.
const { txHashes, deliveredVirtual } = await usdcToVirtual(wallet, client, {
amountIn: 100_000_000n, // 100 USDC
});async function virtualToUsdc(
wallet: WalletClient,
publicClient: PublicClient,
args: { amountIn: bigint; slippageBps?: number }
): Promise<{ txHashes: [Hex, Hex]; deliveredUsdc: bigint }>Reverse direction. VIRTUAL → WETH → USDC, two single hops.
async function approveIfNeeded(
wallet: WalletClient,
publicClient: PublicClient,
args: { token: Address; spender: Address; amount: bigint }
): Promise<Hex | null>Reads current allowance. If allowance >= amount returns null (no tx submitted). Otherwise submits approve(spender, type(uint256).max) and returns the tx hash.
The library caches (wallet, token, spender) → maxAllowance in memory once an approve has been observed in the current process, so subsequent calls in the same process skip the RPC roundtrip. This cache is per-process; restarting the trader will hit RPC once per (wallet, token) on first trade.
function encodeV3Path(steps: Array<{ token: Address; feeTier?: number }>): HexEncodes a multi-hop path. The last element has no feeTier. See CONTRACTS.md#path-encoding.
encodeV3Path([
{ token: USDC, feeTier: 500 },
{ token: WETH, feeTier: 500 },
{ token: VIRTUAL },
])Used only for callStatic against QuoterV2. Never submit as a real swap (see InvalidFEOpcode in CONTRACTS.md).
function parseTokenAmount(amount: string, decimals: number): bigintparseTokenAmount('1.5', 18) === 1_500_000_000_000_000_000n. Thin wrapper around viem.parseUnits that handles trailing zeros and scientific notation defensively.
function formatTokenAmount(amount: bigint, decimals: number, precision?: number): stringformatTokenAmount(1_500_000_000_000_000_000n, 18, 4) === '1.5000'.
This file is only relevant if you're using ACP (Virtuals' Agent Commerce Protocol) smart wallets. If you're trading from a plain EOA, skip this file entirely.
ACP wallets are ERC-4337 smart wallets controlled by a session signer. You can't sign tx's directly — you submit a user op via the ACP CLI, and the bundler relays it on-chain. This file shells out to the CLI.
async function acpExec(args: {
walletId: string;
to: Address;
data: Hex;
value?: bigint;
cwd?: string; // per-wallet config dir; see ACP_WALLETS.md
}): Promise<{ txHash: Hex; userOpHash: Hex }>Submits an arbitrary call as a user op via the ACP CLI. Returns both the user op hash and the eventual settled tx hash.
The CLI is the binary acp (installed separately, not bundled with this repo). It expects the wallet's session signer and config in cwd. See ACP_WALLETS.md for setup.
The tx hash key varies between ACP CLI versions. The library tries txHash, transactionHash, hash, and userOpHash in that order. If you see a result with only userOpHash you can resolve the settled tx by polling the bundler — but for most use cases userOpHash is sufficient as an idempotency key.
async function acpBalanceOf(args: { walletId: string; token: Address; cwd?: string }): Promise<bigint>Convenience wrapper that runs acp balance --token <addr> and parses the output. Equivalent to calling balanceOf from a public client with the smart wallet's address.
All errors thrown by this library extend Error and have a code field for programmatic handling.
| Class | code |
When |
|---|---|---|
BondingV5GraduatedError |
BONDED_TOKEN_GRADUATED |
Caller passed a graduated token to a bondBuy/bondSell function. Switch to swapV3. |
BondingV5InsufficientVirtual |
INSUFFICIENT_VIRTUAL |
Wallet VIRTUAL balance < virtualIn. |
BondingV5InsufficientToken |
INSUFFICIENT_TOKEN |
Wallet token balance < tokenIn (sell). |
BondingV5DeliveryShortfall |
DELIVERY_SHORTFALL |
FoT reconciliation: post-trade balance diff is > 5% below minOut. Probably a rug or extreme FoT. |
V3NoLiquidity |
V3_NO_LIQUIDITY |
QuoterV2 returned 0 for the requested pair/fee. |
V3QuoteRevert |
V3_QUOTE_REVERT |
QuoterV2 call reverted — usually means the pool doesn't exist at all. |
AcpCliMissing |
ACP_CLI_MISSING |
acp binary not on PATH. |
All errors include the relevant args (token address, requested amount, etc.) on the data field.
try {
await bondingV5BuyOnCurve(...);
} catch (err) {
if (err.code === 'BONDED_TOKEN_GRADUATED') {
return swapV3(...); // auto-fallback
}
throw err;
}A few helpers in bondingV5.ts are intentionally private:
readReserves(pair)— internal curve-reserves fetch. UsequoteBuyOnCurveinstead.buildBondBuyCalldata(...)— calldata builder. Use the high-level functions.reconcileFoT(...)— the FoT delivery check. Hard-coded 5% tolerance; if you need a different threshold, fork the file.
These may change between versions. The public API above is stable.