Quickstart. This is the recommended surface: build an Order-encoded program with @1inch/swap-vm-sdk and ship it via @1inch/aqua-sdk onto the shared AquaSwapVMRouter. Every call below matches the deployed v1.0.2 router (5-argument quote/swap that take tokenIn/tokenOut explicitly). To write your own pricing contract instead, see Build an AquaApp (Path A).
TypeScript
123456789101112131415161718192021
pnpm add @1inch/aqua-sdk @1inch/swap-vm-sdk viem
// 1) build an XYC strategy program (high-level builder)
const strategy = AquaXYCAmmStrategy.new()
.withFeeTokenIn(30) // conventional bps: 30 = 0.30% (scaled x100,000 internally)
.withTxOriginAccessToken(aquaKycToken); // attach the launch taker access gate
// 2) wrap as an Order and ship to the shared router
const order = Order.new({ maker, program: strategy.build(), traits: MakerTraits.default() });
const { to, data } = aqua.ship({
app: AQUA_SWAP_VM_CONTRACT_ADDRESSES[chainId], // AquaSwapVMRouter v1.0.2
strategy: order.encode(),
amountsAndTokens,
});
// sign { to, data } in the maker wallet, then broadcast via your own RPC
// 3) taker: quote then swap (SwappedEvent.orderHash === strategyHash)
// the deployed v1.0.2 router takes the token pair explicitly; tokenIn/tokenOut is the direction
const args = { order, tokenIn, tokenOut, amount, takerTraits: TakerTraits.default() };
const quoteTx = swapVm.quote(args); // static preview -> (amountIn, amountOut, orderHash)
const swapTx = swapVm.swap(args);
Only the aquaInstructions subset executes on the deployed router; opcodes outside it are no-ops when reached (reserved gap indices) or revert with a Solidity array-out-of-bounds Panic(0x32) (an index past the table) — there is no named OpcodeNotFound error. See the SDK and Write your own opcode pages for the full surface, the fee scale, and the TakerTraits fields.
Ship your first XYC constant-product AMM strategy to 1inch Aqua in four steps. This walkthrough uses viem together with the two Aqua SDK packages, @1inch/swap-vm-sdk and @1inch/aqua-sdk. For a fuller, fork-runnable version (Foundry + TypeScript that asserts every event), see the Strategy template.
Aqua is self-custodial. Your tokens stay in your own wallet under a revocable, per-chain, per-token ERC-20 allowance and move only when a taker fills against your strategy atomically. The protocol holds zero tokens; the "virtual balances" are an internal accounting counter in Aqua.sol. Standard smart-contract and approval risk still applies.
What a strategy is. A strategy is a small program — a stream of SwapVM opcodes — wrapped in an
Order. You build it with the@1inch/swap-vm-sdkbuilders (for exampleAquaXYCAmmStrategyor the lower-levelAquaProgramBuilder), encode the Order, and register it withship()on the sharedAquaSwapVMRouter. The strategy type (xyc, concentrated, or pegged) is selected by the program bytes, not by a per-strategy app address.
Prerequisites
- Node.js ≥ 18
- A funded Ethereum wallet (maker private key in
MAKER_PRIVATE_KEY) - WETH and USDC, each with an ERC-20 allowance granted to the Aqua registry at
0x1111113ccf1426a8e30e2bff5e005d929bf6a90a. The allowance is revocable and is set per chain and per token, so one approval per chain backs many strategies on that chain.
Step 1: install the SDKs
Bash
1
pnpm add @1inch/swap-vm-sdk @1inch/aqua-sdk viem
@1inch/swap-vm-sdk provides the SwapVM program builders and opcode encoding. @1inch/aqua-sdk wraps Aqua.sol, the Aqua registry at 0x1111113ccf1426a8e30e2bff5e005d929bf6a90a: it builds ship() and dock() transactions, exposes AQUA_CONTRACT_ADDRESSES, and decodes lifecycle events. Both packages live in the 1inch/sdks monorepo.
Step 2: build the strategy program and ship it
Build the opcode program with the SDK, wrap it in an Order, and encode() it — that encoded Order is the strategy bytes you register. Then call ship() through @1inch/aqua-sdk; the app is the single production AquaSwapVMRouter.
Solidity
12345678910111213141516171819202122232425262728293031323334
import { AquaProtocolContract, AQUA_CONTRACT_ADDRESSES } from '@1inch/aqua-sdk';
import { AquaXYCAmmStrategy, Order, MakerTraits, Address, NetworkEnum } from '@1inch/swap-vm-sdk';
import { parseUnits, http, createWalletClient } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
const account = privateKeyToAccount(process.env.MAKER_PRIVATE_KEY as `0x${string}`);
const WETH = new Address('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2');
const USDC = new Address('0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48');
// A bare XYC (x*y=k) strategy is just the xycSwapXD opcode. Compose more with the builder:
// .withFeeTokenIn(30) -> 0.30% LP swap fee
// .withTxOriginAccessToken(kycNft) -> gate takers to a credential
// AquaXYCAmmStrategy.newConcentrate({ rawPriceMin, rawPriceMax }) -> concentrated range
const program = AquaXYCAmmStrategy.new().build();
const order = Order.new({ maker: new Address(account.address), program, traits: MakerTraits.default() });
const strategy = order.encode(); // HexString == abi.encode(Order); this is what ship() registers
const aqua = new AquaProtocolContract(AQUA_CONTRACT_ADDRESSES[NetworkEnum.ETHEREUM]);
const shipTx = aqua.ship({
app: new Address('0x111111338c5091e8440b67b168bae16a668ac0de'), // AquaSwapVMRouter v1.0.2 (all 13 chains)
strategy,
amountsAndTokens: [
{ token: USDC, amount: parseUnits('4000', 6) }, // 4000 USDC
{ token: WETH, amount: parseUnits('1', 18) }, // 1 WETH
],
});
const wallet = createWalletClient({ chain: mainnet, transport: http(), account });
const shipHash = await wallet.sendTransaction({ to: shipTx.to, data: shipTx.data, value: shipTx.value });
console.log('Shipped:', shipHash);
When the transaction confirms, the registry emits a Shipped event, plus a Pushed event per token for the initial allocation, and the strategy is live. Takers who hold the required access credential can then quote and swap against it. The strategyHash carried by Shipped is keccak256(strategy) (the strategy bytes are already abi.encode(Order)) and is immutable once shipped. The full Shipped, Docked, Pulled, and Pushed schema is documented in the registry reference.
Step 3: quote and swap (taker side)
A taker approves the router for the input token, then calls quote() (a free static preview) and swap() on the AquaSwapVMRouter. On the deployed v1.0.2 router both take the token pair explicitly — tokenIn/tokenOut is the swap direction — and return (amountIn, amountOut, orderHash). The SDK mirrors this surface:
TypeScript
12345678910111213141516
import { SwapVMContract, AQUA_SWAP_VM_CONTRACT_ADDRESSES, TakerTraits } from "@1inch/swap-vm-sdk";
const swapVm = new SwapVMContract(AQUA_SWAP_VM_CONTRACT_ADDRESSES[NetworkEnum.ETHEREUM]);
const swapArgs = {
order, // the same Order object shipped above
tokenIn: USDC, // taker pays USDC ...
tokenOut: WETH, // ... and receives WETH
amount: parseUnits("1000", 6), // exact-in amount
takerTraits: TakerTraits.default()
};
// quote is a simulation (eth_call); swap executes pull() + push() atomically.
const quoteTx = swapVm.quote(swapArgs);
const swapTx = swapVm.swap(swapArgs);
await takerWallet.sendTransaction({ to: swapTx.to, data: swapTx.data, value: swapTx.value });
The fill is atomic: swap() runs pull() then push() in a single transaction, all or nothing. It fires Pulled (the maker's output token leaves the maker's wallet for the taker) and Pushed (the taker's input token arrives in the maker's wallet), and the router emits Swapped. Parse the registry events with the SDK's PulledEvent.fromLog() and PushedEvent.fromLog(); Swapped.orderHash equals the strategy's strategyHash.
At launch the router enforces a taker access gate at swap time: the caller's tx.origin must hold the taker credential (the KycNFT). Makers are permissionless; only takers are gated. Because the check reads tx.origin, smart accounts, multisigs, and 4337 bundlers cannot pass it.
Step 4: dock (close the strategy)
When you are done, dock the strategy to clear its allocation:
TypeScript
123456
const dockTx = aqua.dock({
app: new Address("0x111111338c5091e8440b67b168bae16a668ac0de"),
strategyHash: AquaProtocolContract.calculateStrategyHash(strategy),
tokens: [USDC, WETH]
});
await wallet.sendTransaction({ to: dockTx.to, data: dockTx.data, value: dockTx.value });
After docking, the Docked event fires and the virtual allocation is cleared, so the strategy stops filling. Your tokens were always in your wallet; dock() moves no tokens and revokes the strategy's ability to pull them. To rebalance, dock() the old strategy and ship() a new one; strategies are immutable, so any change produces a new strategyHash.
What to explore next
- Strategy template: a fork-runnable Foundry + TypeScript version of this exact flow
- Patterns & decision tree: concentrated liquidity, pegged, and other strategy types beyond XYC
- Strategy Lifecycle: the full lifecycle, events, and re-shipping patterns
- Contract Addresses: contract addresses on all 13 supported chains