Skip to content

Virtual-Protocol/bondv5-trader

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bondv5-trader

Per-agent-wallet Virtuals BondingV5 + USDC↔VIRTUAL trading helpers — ACP wallet edition.

A self-contained TypeScript library for programmatically trading on the Virtuals Protocol BondingV5 curve, from a Virtuals ACP smart wallet. Every transaction is submitted via the official acp CLI's wallet send-transaction command from a per-wallet isolated config directory — no private keys ever touch this codebase.

Adapted from a private fleet-based market maker running on Base mainnet.


Table of contents


What this is

A small library (~1,000 LOC of TypeScript across three files) that does exactly one thing well: submits BondingV5 buy/sell transactions and USDC↔VIRTUAL Uniswap V3 swaps from one ACP smart wallet at a time.

Three primitives:

Function What it does
balanceOf(holder, token) ERC20.balanceOf via eth_call. Pure read, no tx.
usdcToVirtualSwap(walletId, opts) Multi-hop USDC → WETH(0.05%) → VIRTUAL(0.05%) via Uniswap V3 SwapRouter02, as two single-hop calls (more on this below).
virtualToUsdcSwap(walletId, opts) Reverse of the above.
bondingV5Trade(walletId, { side, preToken, amountInWei, ... }) Calls BondingV5.buy or .sell. Handles approval to FRouterV3 (not the V5 proxy — more on this below) and fee-on-transfer balance reconciliation.

Each operation is structured as: prep calldata → idempotent approve (if needed) → trade. No retries, no concurrency control — that belongs in your strategy layer.

What it is not

  • ❌ A wallet generator or key custodian. The agent wallet signs internally.
  • ❌ ACP wallet onboarding. You need acp wallet create + session signer setup separately.
  • ❌ Post-graduation UniV2 trading. Use uniV2Router02 directly or an aggregator.
  • ❌ A retry/backoff layer.
  • ❌ A pricing oracle. minOutWei defaults to 1n for BondingV5 trades — production users should compute this from a fresh quote.
  • ❌ Multi-chain. Base only (chainId 8453).
  • ❌ A KyberSwap aggregator wrapper. Our production system uses Kyber for USDC↔VIRTUAL — the UniV3 helpers in this repo are the lower-level fallback. See docs/ARCHITECTURE.md.

Architecture in 90 seconds

┌─────────────────────────────────────────────────────────────────────┐
│  Your strategy code                                                 │
│  └─ bondingV5Trade(walletId, { side, preToken, amountInWei })       │
│                              │                                      │
│                              ▼                                      │
│  bondingV5.ts                                                       │
│    ├─ getAcpWalletAddress(walletId)   ─┐                            │
│    ├─ balanceOf(addr, tokenIn)         │                            │
│    ├─ ensureApproval(...)              │ all of these spawn         │
│    └─ sendTx(walletId, to, data)       │ `acp wallet ...` in a      │
│                              │         │ per-wallet ACP_CONFIG_DIR  │
│                              ▼         │                            │
│  acpCli.ts ───────────────────────────┘                             │
│    spawn("acp", ["wallet", "send-transaction", "--chain-id", ...])  │
│                              │                                      │
│                              ▼                                      │
│  Virtuals ACP smart wallet (one per walletId)                       │
│  Session signer in keystore → signs userOp → submits to bundler     │
│                              │                                      │
│                              ▼                                      │
│  Base mainnet (chainId 8453)                                        │
│   • Approve to FRouterV3 (0x02fe...)  for BondingV5 trades          │
│   • BondingV5 proxy   (0x1a54...)     buy()/sell()                  │
│   • UniV3 SwapRouter02 (0x2626...)    USDC↔VIRTUAL via WETH         │
└─────────────────────────────────────────────────────────────────────┘

The orchestrator never holds a private key. It identifies a wallet by integer walletId, which maps to a config dir ${ACP_CONFIG_ROOT}/mainnet/${walletId} containing that wallet's keystore + session signer. Different walletIds run in completely isolated environments — no shared state.

For the full architecture, including why we use ACP wallets, how fees-on-transfer break naive sizing, and how the dynamic agent-router picks BondingV5 vs UniV2, see docs/ARCHITECTURE.md.

Quick start

Prerequisites

  1. Node ≥ 20 + pnpm or npm.
  2. acp CLI on PATH. Confirm with acp --version. (Get it from Virtuals.)
  3. An onboarded ACP wallet. This library trades from a wallet — it doesn't create one. Onboarding is one command: acp wallet create inside a fresh ACP_CONFIG_DIR. See docs/ACP_WALLETS.md.
  4. Base RPC URL. Public works for testing; use QuickNode/Alchemy at scale.

Install

git clone https://github.com/moomooed/bondv5-trader
cd bondv5-trader
pnpm install
cp .env.example .env       # edit BASE_RPC_URL, ACP_CONFIG_ROOT
pnpm typecheck

Smoke test

# Read balances on walletId=1 (must already be onboarded under ACP_CONFIG_ROOT/mainnet/1)
pnpm cli balances 1

# Same plus a preToken balance
pnpm cli balances 1 0xb2a99bc73c89b6bcbeb4650eedcd5f2776373c48

# Fund: swap 80-100% of USDC into VIRTUAL via UniV3
pnpm cli usdc-to-virtual 1

# Buy: spend 1 VIRTUAL on a preToken via BondingV5
pnpm cli buy 1 0xb2a99bc73c89b6bcbeb4650eedcd5f2776373c48 1000000000000000000

# Sell: dump 50 preTokens back to VIRTUAL via BondingV5
pnpm cli sell 1 0xb2a99bc73c89b6bcbeb4650eedcd5f2776373c48 50000000000000000000

# Close: VIRTUAL back to USDC
pnpm cli virtual-to-usdc 1

The CLI is a thin wrapper around the library — see examples/cli.ts.

The five public entrypoints

import {
  balanceOf,            // read ERC20 balance
  ensureApproval,       // idempotent approve to MAX_UINT256
  usdcToVirtualSwap,    // UniV3 USDC -> VIRTUAL (two-leg)
  virtualToUsdcSwap,    // UniV3 VIRTUAL -> USDC (two-leg)
  bondingV5Trade,       // BondingV5.buy or .sell
} from "bondv5-trader";

Full signatures, args, returns, errors, examples → docs/API.md.

Critical gotchas (read these)

These are the five things that will silently waste your time if you don't internalize them:

1. Approve FRouterV3, not the BondingV5 proxy

BondingV5.buy()/sell() delegate to FRouterV3, which is the actual transferFrom caller. Approving the V5 proxy reverts with ERC20: insufficient allowance.

bondingV5 proxy   0x1a540088125d00dd3990f9da45ca0859af4d3b01  ← call target for buy/sell
fRouterV3         0x02fe8ec3d9bbf7318eb54590bcc39198a8b47ded  ← ERC20 spender to approve

This library handles it correctly via ensureApproval(walletId, addr, tokenIn, ADDR.fRouterV3, amount). If you're hand-rolling an integration, don't get this wrong.

2. USDC↔VIRTUAL must be two single-hop swaps, not multi-hop

There's no USDC/VIRTUAL UniV3 pool with material liquidity on Base. The route is USDC → WETH → VIRTUAL, both 0.05% pools. The natural exactInput (multi-hop) reverts with InvalidFEOpcode on Base because of the callback boundary between the two 0.05% pools.

We work around this with two exactInputSingle calls, reading the actual WETH balance after leg 1 before sizing leg 2. Slightly more gas, but it works. See docs/CONTRACTS.md#univ3-quirk.

3. ACP wallets are smart wallets, not EOAs

The trading address is the ACP smart wallet address returned by acp wallet address --json. The owner EOA controls it via session signers, but you never reference the EOA from this library. getAcpWalletAddress(walletId) resolves the smart wallet address; everything else uses it.

4. acp wallet send-transaction JSON shape varies across CLI versions

Different builds return the tx hash under different keys:

  • txHash
  • transactionHash
  • hash
  • userOpHash (only — no tx hash yet)

The wrapper accepts all four. When it gets a tx hash, it waitForTransactionReceipt on Base (90 s timeout, ~45 blocks of headroom under congestion) and throws if the on-chain status is reverted. This is how the library distinguishes "tx submitted and reverted" from "submitted, still pending" — callers can rely on a successful return meaning the tx mined and didn't revert.

5. BondingV5 reverts on graduated tokens

Once a token graduates off the bonding curve onto Uniswap V2, BondingV5.buy/sell will revert. Before trading, either:

This library deliberately does not include the UniV2 router (we keep this repo scoped to BondingV5). Selectors and FoT-safe variants are in abi.ts though — see docs/CONTRACTS.md.

A sixth gotcha worth flagging: fee-on-transfer balance reconciliation. VIRTUAL and all AgentTokenV4 preTokens are fee-on-transfer, meaning if a prior swap deposits N wei, the wallet ends up with slightly less. bondingV5Trade caps amountInWei to the actual on-chain balance minus a 0.5% safety floor when the requested amount exceeds available — and throws if the gap is > 50% (probably a real sizing bug, not just FoT). See docs/CONTRACTS.md#fee-on-transfer.

Verified Base mainnet addresses

All independently verified on-chain in our production system (May 2026).

Contract Address Notes
BondingV5 proxy 0x1a540088125d00dd3990f9da45ca0859af4d3b01 call target for buy()/sell()
BondingV5 impl 0x22aAAfa24266CB2FC3eAE8C151b16537e5841bbD verified-source on Basescan
FRouterV3 0x02fe8ec3d9bbf7318eb54590bcc39198a8b47ded ERC20 spender to approve
VIRTUAL 0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b 18 decimals
USDC 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 6 decimals, native
WETH 0x4200000000000000000000000000000000000006 18 decimals
UniV3 SwapRouter02 0x2626664c2603336E57B271c5C0b26F421741e481 for USDC↔VIRTUAL
UniV3 QuoterV2 0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a read-only quoter
UniV3 0.05% WETH/VIRTUAL 0x9c087eb773291e50cf6c6a90ef0f4500e349b903 leg 2 pool
UniV2 Router02 0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24 post-graduation only

Function selectors

Function Selector Source
balanceOf(address) 0x70a08231 ERC20
allowance(address,address) 0xdd62ed3e ERC20
approve(address,uint256) 0x095ea7b3 ERC20
buy(uint256,address,uint256,uint256) 0x706910ff BondingV5
sell(uint256,address,uint256,uint256) 0xb233e056 BondingV5
tokenInfo(address) 0xf5dab711 BondingV5
exactInputSingle((...)) 0x04e45aaf UniV3 SwapRouter02
exactInput((...)) 0xb858183f UniV3 SwapRouter02
quoteExactInput(bytes,uint256) 0xcdca1753 UniV3 QuoterV2
quoteExactInputSingle((...)) 0xc6a5026a UniV3 QuoterV2
swapExactTokensForTokens 0x38ed1739 UniV2 Router02
swapExactTokensForTokensSupportingFeeOnTransferTokens 0x5c11d795 UniV2 Router02 (FoT-safe)

All selectors are bytes4(keccak256("signature(types)")). See src/abi.ts for the calldata encoders.

HTTP route reference

If you want to expose these helpers over HTTP, examples/http-routes.ts shows the exact Fastify route handlers we use in production:

Method Path Purpose
POST /mm/bondv5/buy spend VIRTUAL → receive preToken
POST /mm/bondv5/sell spend preToken → receive VIRTUAL
GET /mm/bondv5/balances USDC + VIRTUAL + optional preToken balances
POST /mm/usdc-to-virtual UniV3 multi-hop swap
POST /mm/virtual-to-usdc UniV3 multi-hop reverse swap

All bodies, response shapes, and example curl commands are in docs/RECIPES.md.

Deep docs

The repo includes a full docs/ folder for everything that doesn't belong in the README:

Doc What's in it
docs/ARCHITECTURE.md High-level system design, data flow, ACP wallet model, why this is shaped the way it is
docs/CONTRACTS.md BondingV5 contract internals, FRouterV3, fee-on-transfer math, UniV3 path encoding, multi-hop quirk, QuoterV2, post-graduation UniV2
docs/API.md Function-by-function reference: signature, args, returns, errors, examples for every export
docs/ACP_WALLETS.md Smart-wallet model, session signers, per-wallet config dir isolation, onboarding pointer, keystore safety
docs/RECIPES.md Common workflows: fund a wallet, buy + sell + close in one shot, fleet-style parallelism, slippage tuning, integrating in your own service
docs/TROUBLESHOOTING.md Error-message decoder. "Why does it say X?" → why and what to fix.
docs/SECURITY.md Threat model, what NOT to do, what to log, what to keep out of logs

License

MIT. Adapted from a private fleet-based market maker. Share at will — no warranty. You signed the txs, you own the outcome.

About

TypeScript library for trading Virtuals Protocol agent tokens via BondingV5 + Uniswap V3 on Base. Auto-routes bonded vs graduated. Includes ACP smart-wallet support and deep docs.

Resources

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • TypeScript 100.0%