For resolvers

Fill swaps against Aqua liquidity: what the resolver role requires, how a fill works on-chain and how to quote and execute one with the SwapVM SDK.

10 min readUpdated July 2026

What resolvers do in Aqua

Across the 1inch Network, swaps that fill Aqua liquidity are routed by 1inch Resolvers: market makers and arbitrage traders that complete the 1inch verification process.

Aqua itself does no discovery and no matching. It prices and executes one position against one counterparty per swap call. Finding which position to fill, and at what size, is the resolver’s job: 1inch routing hands swaps to the network, and your own systems can call the contracts directly.

New to the underlying mechanics? Start with how Aqua works.

Before you start

  • Resolver verification through the 1inch Business portal, which grants the per-chain access token
  • The access token on the operator EOA that sends the swap transaction
  • Node access on the target chain, plus the input tokens and gas for fills
  • The canonical contract addresses, identical on every supported chain and listed below
  • An API key from the Business portal for the Aqua API discovery path

Contract addresses

Aqua and the SwapVM router are deterministic deployments with the same address on every supported chain. Only interact with these two contracts. Anything else is not Aqua.

Aqua (registry)
0x1111113ccf1426a8e30e2bff5e005d929bf6a90a
SwapVM router
0x111111338c5091e8440b67b168bae16a668ac0de

How a fill works

  1. 1

    Get verified

    Complete resolver onboarding through the Business portal and receive the access token for each chain you fill on.

  2. 2

    Discover a position

    Pull open positions from the Aqua API, rebuild the live set from Shipped and Docked events, or receive swaps from 1inch routing as a network resolver.

  3. 3

    Quote right before you submit

    Static-call the router’s quote function with the decoded order. Quotes read live wallet backing, so they move as other fills land.

  4. 4

    Set your bounds

    Derive a minimum output from the fresh quote and a short deadline. A moved curve then reverts instead of filling at a worse rate.

  5. 5

    Swap and verify

    Send the swap from the EOA holding the access token, require a successful receipt and read the executed amounts from the Swapped event.

Quickstart: quote and fill one position

The samples below discover an open position through the Aqua API, quote it and submit a protected fill. They run on Node 22 with the TypeScript SDKs. The contracts, events, SDKs and the API all call a position a strategy, so the code does too.

Install the SDKsbash
pnpm add @1inch/aqua-sdk @1inch/swap-vm-sdk viem
Discover open positions via the Aqua APItypescript
import assert from 'node:assert'
import { HexString, Order } from '@1inch/swap-vm-sdk'

// Every currently open position from every LP, newest first. Auth is the
// same API key the other 1inch APIs use - issued in the Business portal.
const response = await fetch('https://api.1inch.com/aqua/v1.0/strategies/opened?chainIds=1&limit=50', {
  headers: { Authorization: `Bearer ${process.env.ONEINCH_API_KEY}` },
})
assert(response.ok, `Aqua API responded ${response.status}`)
const { items, nextCursor } = await response.json()

// Pick by your own criteria. Per-token balance and allowance in each item are
// useful pre-filters, but they are indexed with a delay - only a fresh
// on-chain quote() is authoritative.
const strategy = items[0]
const order = Order.decode(new HexString(strategy.strategyBytes))
console.log('candidate:', strategy.strategyHash, 'tokens:', strategy.tokens.length, 'more:', nextCursor !== null)
Quote, fill and verifytypescript
import assert from 'node:assert'
import { ABI, Address, HexString, Order, SwappedEvent, SwapVMContract, TakerTraits } from '@1inch/swap-vm-sdk'
import { createPublicClient, createWalletClient, decodeFunctionResult, erc20Abi, http, isHex } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { mainnet } from 'viem/chains'

// The SwapVM router is a deterministic deployment - the same address on every
// supported chain (see the contract addresses above). The strategy bytes come
// from the Aqua API or a Shipped event. The operator key belongs to the EOA
// holding the resolver credential - it must be the transaction origin.
const router = new Address('0x111111338c5091e8440b67b168bae16a668ac0de')
const strategyBytes = process.env.STRATEGY_BYTES
const operatorPrivateKey = process.env.OPERATOR_PRIVATE_KEY
assert(isHex(strategyBytes) && isHex(operatorPrivateKey))

const order = Order.decode(new HexString(strategyBytes))

const USDC = new Address('0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48')
const WETH = new Address('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2')
const amountIn = 3_000n * 10n ** 6n

const operator = privateKeyToAccount(operatorPrivateKey)
const publicClient = createPublicClient({ chain: mainnet, transport: http() })
const wallet = createWalletClient({ chain: mainnet, transport: http(), account: operator })

// 1. Preview the swap with a static quote() call, right before you submit.
const quoteTx = SwapVMContract.buildQuoteTx(router, {
  order,
  tokenIn: USDC,
  tokenOut: WETH,
  amount: amountIn,
  takerTraits: TakerTraits.new({ exactIn: true }),
})
const quoted = await publicClient.call({ to: quoteTx.to, data: quoteTx.data })
assert(quoted.data, 'quote call returned no data')
const [, quotedOut] = decodeFunctionResult({ abi: ABI.SWAP_VM_ABI, functionName: 'quote', data: quoted.data })
console.log('quoted output:', quotedOut)

// 2. Approve the router to move the input token (once per token and chain).
const approveHash = await wallet.writeContract({
  address: USDC.toString(),
  abi: erc20Abi,
  functionName: 'approve',
  args: [router.toString(), amountIn],
})
await publicClient.waitForTransactionReceipt({ hash: approveHash })

// 3. Swap with a minimum-output floor and a short deadline. Balances and
// curves move between quote and fill - never submit unprotected traits.
const swapTx = SwapVMContract.buildSwapTx(router, {
  order,
  tokenIn: USDC,
  tokenOut: WETH,
  amount: amountIn,
  takerTraits: TakerTraits.new({
    exactIn: true,
    threshold: (quotedOut * 995n) / 1_000n,
    deadline: BigInt(Math.floor(Date.now() / 1_000) + 60),
  }),
})
const fillHash = await wallet.sendTransaction(swapTx)
const receipt = await publicClient.waitForTransactionReceipt({ hash: fillHash })
assert(receipt.status === 'success', 'fill reverted')

// 4. Verify the executed amounts from the router's Swapped event.
const swappedLog = receipt.logs.find((log) => log.topics[0] === SwappedEvent.TOPIC.toString())
assert(swappedLog, 'no Swapped event in the receipt')
const swapped = SwappedEvent.fromLog(swappedLog)
console.log('filled:', swapped.amountIn, '->', swapped.amountOut)

Try the loop on a fork first

Everything above dry-runs safely on a local fork. Foundry's anvil clones the chain at a block, so you get the real contracts and the real live positions with nothing at stake. With keyless sends enabled you can act as any EOA that already holds the access token and walk the whole loop, discover, quote, fill, verify, before your first real transaction.

Dry-run against a mainnet forkbash
# Real Aqua bytecode and live positions at the forked block, zero real spend
anvil --fork-url $YOUR_RPC_URL --auto-impersonate

# Point the quickstart at http://127.0.0.1:8545 and send from any EOA that
# already holds the per-chain access token - no key needed on a fork

How a fill executes on-chain

A fill is one transaction to the SwapVM router. The router runs the position's instruction program: the access check first, then the pricing curve, then the token moves. The input token goes from your EOA to the LP's wallet and the output token comes back to you in the same transaction, while the Aqua registry updates the position's balances and emits Pushed and Pulled. Nothing sits in a pool at any point.

Everything is all or nothing. If any check fails at execution time, the access token missing, the threshold not met, the deadline passed or the wallet backing too low, the whole transaction reverts and no tokens move. A reverted fill costs gas and nothing else.

The router exposes quote and swap, and both run the same pricing path. Neither is a view function, so call quote through eth_call as a static call: it reads live state, prices the exact swap you pass and changes nothing. That preview is not a commitment. Only the traits you send with swap bind the execution.

TakerTraits carry your execution bounds into the transaction. The chain enforces them, not the SDK. The ones you will use most:

exactIn
Which side is fixed. With exact input you set the amount you send and bound what you receive. With exact output you fix what you receive and bound what you send.
threshold
The bound itself. On exact input, the minimum output the fill must deliver. On exact output, the cap on what you spend. A zero threshold disables the check, so always derive it from a fresh quote.
deadline
A unix-seconds expiry. A transaction mined after it reverts, so a fill stuck in the mempool cannot execute against a stale curve.
customReceiver
Delivers the output somewhere other than the sending EOA, for example a treasury address. The access check still runs against the transaction origin.
shouldUnwrap
Receive the native token instead of the wrapped form when the output is a wrapped native token.

Do not trust your own math for the outcome. Require a successful receipt, then decode the router's Swapped event: orderHash, maker, taker, tokenIn, tokenOut, amountIn and amountOut. Those are the executed amounts, and they are what your accounting should record.

Failure modes to handle

Closed position
The LP can close a position at any time, and balance reads revert once it is gone. A position missing from the API opened list is closed, but between polls the on-chain revert stays the source of truth. Re-check before every fill.
Low wallet backing
Positions quote from a shared wallet balance. A competing fill can drain it first, so your transfer reverts even though the quote looked fine. The balance and allowance fields from the API help pre-filter, but they are indexed with a delay. Only a fresh on-chain quote is authoritative.
Moved price
Quotes go stale as fills land and time passes. The minimum-output floor and the deadline turn a bad fill into a clean revert.
Missing access token
Without the credential on the transaction origin, dApp-created positions revert the swap before pricing runs.
Non-canonical addresses
Only the two canonical addresses above are Aqua. Fills sent anywhere else miss the live positions. There is nothing to verify per chain, the deployments are identical everywhere.

Operating in production

Treat the operator EOA as a credential, not just a wallet. It holds the per-chain access token, so isolate its key in a dedicated signer, keep only working inventory and gas on the address, and never reuse it for anything else.

Let gas estimation be your last gate. Wallet libraries estimate gas before broadcasting, and a failing estimate is the revert surfacing early, before anything is sent. Treat it as a skipped fill and move to the next candidate rather than retrying blindly.

Watch three signals. Your revert rate, which rises when discovery data goes stale or sizing sits too close to the backing. The age of the quote behind each fill, which should stay seconds old. And the executed amounts from Swapped against what you quoted, which catches drift before it costs you.

Discovering positions in production

Two paths give you the live set. The Aqua API is the fastest way to start. Rebuilding from events is trustless and adds no external dependency. Both feed the same quote and fill flow.

The Aqua API lists every currently open position from every LP: GET /v1.0/strategies/opened on api.1inch.com/aqua, with cursor pagination up to 500 items per page and chain and app filters. Each item carries the strategyBytes you pass to Order.decode, plus per-token balance and allowance for pre-filtering candidates. Requests need the same API key as other 1inch APIs, issued in the Business portal. The index trails the chain slightly, so always re-quote on-chain before filling.

The Aqua registry, the protocol’s core contract, emits Shipped when a position opens and Docked when it closes. The live set is Shipped minus Docked, rebuilt from logs starting at the per-chain deployment block. Event fields live in the log data rather than in topics, so filter by contract address and event signature, then decode and match client-side.

The event shapes are small. Shipped carries the maker, the app, the strategyHash and the full strategy bytes. Docked carries the maker, the app and the strategyHash. The SDK ships typed helpers for both, ShippedEvent and DockedEvent, with fromLog decoders and TOPIC constants, so you never hand-roll the ABI.

Rebuild the live set from eventstypescript
import { DockedEvent, ShippedEvent } from '@1inch/aqua-sdk'
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'

const AQUA = '0x1111113ccf1426a8e30e2bff5e005d929bf6a90a'
const publicClient = createPublicClient({ chain: mainnet, transport: http() })

// Shipped and Docked carry every field in the log data, none in topics, so
// pull the registry's logs and match on the event signature client-side.
const logs = await publicClient.getLogs({
  address: AQUA,
  fromBlock: 23_000_000n, // the per-chain deployment block, or your last checkpoint
  toBlock: 'latest',
})

const live = new Map<string, ShippedEvent>()
for (const log of logs) {
  if (log.topics[0] === ShippedEvent.TOPIC.toString()) {
    const shipped = ShippedEvent.fromLog(log)
    live.set(shipped.strategyHash.toString(), shipped)
  } else if (log.topics[0] === DockedEvent.TOPIC.toString()) {
    live.delete(DockedEvent.fromLog(log).strategyHash.toString())
  }
}

// live now maps strategyHash -> Shipped payload: maker, app and the strategy
// bytes you decode with Order.decode before quoting.
console.log('open positions:', live.size)

On-chain reads give the rest. The registry's rawBalances and safeBalances views report the wallet backing behind a position, and the router's Swapped events carry executed swaps. Keep quotes fresh instead of caching them.

Sizing a fill starts from backing, not from the quote. Read the position's backing with the registry views, cap your size well inside it, then take a fresh quote for that exact size right before you submit. The API's balance and allowance fields are a cheap first pass over many candidates, the views and the quote are the truth for one.

Resolver FAQ

No. The access check requires the transaction origin to be the EOA holding the access token, so contract wallets and bundlers cannot pass it. Send fills from the verified operator EOA.

No. The quote function is a static preview of live state and moves nothing. Only the TakerTraits you send with the swap bind the execution: the threshold and the deadline are enforced on-chain.

Gas and nothing else. Any failed check reverts the whole transaction, so no tokens move on either side.

Verification through the 1inch Business portal grants a per-chain access token. The operator EOA needs that token on every chain you fill on.

Aqua API reference Endpoints, parameters and response shapes for position discovery and stats, in the Business portal. Aqua SDK & SwapVM SDK Typed builders, decoders and event parsers for the fill flow, in the 1inch/sdks repo. Resolver onboarding Verification steps and resolver documentation on the 1inch Business portal. Aqua white paper The full design of the shared liquidity layer.

Ready to fill Aqua liquidity?

Start verification and reach the team through the 1inch Business portal.

Open the Business portal