# 1inch Business Portal > This file provides a machine-readable index of 1inch Business Portal documentation > for AI agents, LLMs, and automation tools, following the llmstxt.org specification. > This file contains full inlined content for LLM consumption. Use this file for: Latest API versions, accurate endpoints, machine-readable integrations. - Supported chains: Ethereum, Solana, Base, Binance, zkSync, Gnosis, Optimism, Polygon, Linea, Sonic, Unichain, Arbitrum, Avalanche, Monad - Integration paths: REST APIs, SDKs, WebSockets, JSON-RPC, gRPC - MCP Server: https://api.1inch.com/mcp/protocol - API categories: Trading & Liquidity, Wallet & Data, Market Data, Infrastructure ## Overview https://business.1inch.com/portal/assets/docs-v2/overview/welcome.md 1inch Business is a SaaS Web3 infrastructure platform: subscription-based access to the 1inch ecosystem, built on the same battle-tested swap and execution technology that powers 1inch -- packaged as production-grade APIs for developers and businesses building on-chain applications. One account, one API key, one platform: DEX liquidity aggregation, intent-based and cross-chain swaps, wallet and market data, and blockchain infrastructure across 15+ networks. ## Why 1inch Business

The swap, routing, and settlement technology behind these APIs processes real production volume on 1inch itself. You integrate the same engine -- not a re-implementation.

One key, one subscription

A single API key unlocks every product. Plans scale from a free tier for prototyping to enterprise agreements with custom rate limits, SLAs, and support.

AI-ready

Every API returns clean JSON, plus purpose-built AI tooling: an MCP server for coding assistants and an llms.txt index for RAG pipelines and agents.

## Who builds on it From early-stage startups to enterprises and institutions: wallets embedding swaps and portfolio views, fintechs adding on-chain trading, trading platforms consuming market data, and infrastructure providers running on managed RPC -- all operating under real-world load with production reliability. ## Where to go next - [Getting started](/portal/documentation/overview/getting-started) -- create a key and make your first request in minutes - [Products at a glance](/portal/documentation/overview/products) -- what each API does and when to use it - [Supported chains](/portal/documentation/overview/supported-chains) -- the networks every product covers - [Integration paths](/portal/documentation/overview/integration-paths) -- SDK, REST, WebSocket, JSON-RPC, or gRPC - [AI & developer resources](/portal/documentation/overview/developer-resources) -- MCP, llms.txt, SDKs, and the API playground
https://business.1inch.com/portal/assets/docs-v2/overview/getting-started.md Go from zero to your first API response in a few minutes. Everything below works on the free plan. ## 1. Create your account Sign up at the [1inch Business Portal](/portal/login). During onboarding you create your first application -- applications are how the portal organizes API keys, usage, and logs. ## 2. Get your API key Your API key is available under [Applications](/applications). Keys are bearer tokens; keep them out of client-side code and public repositories. For user-consent flows and server-to-server access without shared secrets, the platform also supports [OAuth 2.1](/portal/documentation/apis/authentication). ## 3. Make your first request Every API accepts the key as an `Authorization` header (or an `apiKey` query parameter -- see [Authentication](/portal/documentation/apis/authentication)): ```bash curl -X 'GET' \ 'https://api.1inch.com/swap/v5.2/1/tokens' \ -H 'accept: application/json' \ -H 'Authorization: Bearer YOUR_API_KEY' ``` A successful response returns the tradable token list on Ethereum (chain ID `1`) -- swap the `1` for any [supported chain](/portal/documentation/overview/supported-chains). Prefer TypeScript? The official [SDKs](/portal/documentation/sdks/overview) wrap order building, signing, and submission for the swap protocols: ```bash npm install @1inch/fusion-sdk ``` ## 4. Explore from here - Run requests interactively in the API playground on any [API reference page](/portal/documentation/apis/authentication) - Pick your product in [Products at a glance](/portal/documentation/overview/products) - Connect your AI coding assistant via the [1inch MCP server](/portal/documentation/ai-integration/mcp-server) -- it can query these docs and call the APIs while you build - Hitting an error? See [API troubleshooting](/portal/documentation/faq/api-troubleshooting) https://business.1inch.com/portal/assets/docs-v2/overview/products.md The platform groups its APIs into four areas -- the same grouping you see in the APIs menu. One API key and one subscription cover every product below; Aqua is the exception and is enabled per plan.
Swap APIBest-execution swaps across every chain
Orderbook APIOn-chain limit orders and RFQ
1inch AquaProgrammable shared liquidity and SwapVM
Wallet & Data
Portfolio APIPositions and value across networks
Balance APIToken balances and allowances anywhere
History APIDecoded transaction history per address
Traces APIExecution traces for any transaction
Token & NFT APIsToken metadata, details and NFT holdings
Market Data
Spot Price APIReal-time token prices per chain
Gas Price APILive gas estimation per network
Charts APIHistorical market chart data feeds
Infrastructure
Web3 RPC APIManaged nodes: JSON-RPC, WebSocket, gRPC
Transaction Gateway APIReliable broadcasting with MEV protection
Domains APIResolve on-chain domain names
AuthenticationAPI keys and OAuth 2.1
## Trading & Liquidity Swap execution and order management on top of aggregated DEX liquidity, plus the shared liquidity layer that liquidity providers build on. - [Swap API](/portal/documentation/apis/swap) -- classic (transaction-based) swaps, intent-based swaps, cross-chain swaps, and real-world asset support. Best for: wallets, trading interfaces, and any app that executes on-chain trades. - [Orderbook API](/portal/documentation/apis/orderbook) -- limit orders and orderbook-driven trading workflows. Best for: pro-trading UIs and order-management backends. - [1inch Aqua](/portal/documentation/aqua/overview) -- programmable shared liquidity: makers keep custody while the same capital backs multiple strategies through SwapVM. Best for: market makers and apps building on shared liquidity. Aqua is enabled per plan -- keys without it get `404` on `/aqua`. See the [Aqua API](/portal/documentation/aqua/api/introduction) for the hosted analytics endpoints. ## Wallet & Data Structured, indexed on-chain data that would be expensive to assemble from raw RPC. - [Portfolio API](/portal/documentation/apis/portfolio) -- positions and portfolio composition across networks. Best for: portfolio trackers and account dashboards. - [Balance API](/portal/documentation/apis/balance) -- token balances and allowances for any address. Best for: wallet balance views. - [History API](/portal/documentation/apis/history) and [Traces API](/portal/documentation/apis/traces) -- historical transactions and execution traces. Best for: activity feeds, analytics, auditing. - [Token API](/portal/documentation/apis/tokens), [Token Details API](/portal/documentation/apis/token-details), and [NFT API](/portal/documentation/apis/nft) -- token metadata, details, and NFT holdings. Best for: discovery, search, and display. ## Market Data Pricing and market structure for user-facing trading experiences. - [Spot Price API](/portal/documentation/apis/spot-price) -- real-time token prices. Best for: quoting and price display. - [Gas Price API](/portal/documentation/apis/gas-price) -- gas estimation per network. Best for: transaction cost UX. - [Charts API](/portal/documentation/apis/charts) -- market charts. Best for: trading interfaces and analytics. ## Infrastructure Production-ready blockchain connectivity, managed for you. - [Web3 RPC API](/portal/documentation/apis/web3) -- managed JSON-RPC node access across all supported networks, including WebSocket subscriptions and gRPC on Solana. Best for: replacing self-hosted nodes. - [Transaction Gateway API](/portal/documentation/apis/transaction) -- transaction broadcasting. Best for: reliable execution pipelines. - [Domains API](/portal/documentation/apis/domains) -- domain name resolution. Best for: address UX. - [Authentication](/portal/documentation/apis/authentication) -- API keys and OAuth 2.1 for all of the above.
https://business.1inch.com/portal/assets/docs-v2/overview/supported-chains.md The 1inch Business Portal provides access to blockchain data through RPC Nodes and, on selected networks, through additional 1inch APIs such as Swap, History, and Gas Price. ## Currently Supported The following chains are currently available. Unless noted otherwise, a chain is available as RPC Nodes only. | Chain | Chain ID | Network Family | Available Products | | :--------: | :------: | :------------: | :----------------: | | Arc | 5042 | EVM | Full API set | | Arbitrum | 42161 | EVM | Full API set | | Avalanche | 43114 | EVM | Full API set | | Base | 8453 | EVM | Full API set | | BNB Chain | 56 | EVM | Full API set | | Cronos | 25 | EVM | Full API set | | Ethereum | 1 | EVM | Full API set | | Gnosis | 100 | EVM | Full API set | | HyperEVM | 999 | EVM | Full API set | | Linea | 59144 | EVM | Full API set | | Monad | 143 | EVM | Full API set | | Optimism | 10 | EVM | Full API set | | Polygon | 137 | EVM | Full API set | | Solana | 501 | SVM | Full API set | | Sonic | 146 | EVM | Full API set | | Unichain | 130 | EVM | Full API set | | zkSync Era | 324 | EVM | Full API set | For a per-chain list of which 1inch products are live, see the [chain pages](/chains) on the 1inch Business site. Examples: [Ethereum](/chains/ethereum), [BNB Chain](/chains/bnb-chain), [Polygon](/chains/polygon), [Cronos](/chains/cronos), [Monad](/chains/monad), [HyperEVM](/chains/hyperevm), [Arc](/chains/arc). ## Notes - **Solana** is addressed as chain ID `501` across the 1inch APIs, even though it is not an EVM chain and uses `base58` addresses. See the [Solana documentation](/documentation/apis/web3/solana) for details. - **Cronos** is available with the full 1inch API set (Swap, History, Gas Price, Tokens, and RPC Nodes). See the [Cronos chain page](/chains/cronos) for the live product list. - **Monad** is available with the full 1inch API set (Swap, History, Gas Price, Tokens, and RPC Nodes). See the [Monad chain page](/chains/monad) for the live product list. - **HyperEVM** is available with the full 1inch API set (Swap, History, Gas Price, Tokens, and RPC Nodes). See the [HyperEVM chain page](/chains/hyperevm) for the live product list. - **Arc** is available with the full 1inch API set (Swap, History, Gas Price, Tokens, and RPC Nodes). See the [Arc chain page](/chains/arc) for the live product list. - Additional chains are added regularly. Check this page or the [changelog](/documentation/changelog) for updates. ## Requesting a New Chain Need a chain that is not on the list? Tell us which one and what you plan to build on it -- the chain name and chain ID, your expected usage volume, and the specific APIs you need (RPC Nodes, Swap, History, etc.).
https://business.1inch.com/portal/assets/docs-v2/overview/integration-paths.md Every product is reachable through more than one integration style. Pick the path that matches your stack -- they all share the same [authentication](/portal/documentation/apis/authentication) and API key. ## SDKs -- fastest to production Official TypeScript [SDKs](/portal/documentation/sdks/overview) for the swap protocols (cross-chain, intent swaps, limit orders) handle order building, signing, and submission so you skip the low-level details. Best for: teams shipping swap functionality quickly. ## REST APIs -- maximum flexibility Every product exposes a documented REST interface with OpenAPI specifications and an interactive playground on each [API reference page](/portal/documentation/apis/authentication). Best for: backend integrations in any language. ## WebSockets -- real-time data and events Streaming updates for intent and cross-chain swaps on the [Swap API](/portal/documentation/apis/swap), and node subscriptions (new blocks, pending transactions, logs) on the [Web3 RPC API](/portal/documentation/apis/web3). Best for: live UIs and event-driven systems. ## JSON-RPC -- direct blockchain access Standard JSON-RPC against managed nodes via the [Web3 RPC API](/portal/documentation/apis/web3) -- a drop-in replacement for self-hosted infrastructure. Best for: existing web3 tooling (ethers, viem, web3.js). ## gRPC -- high throughput, low latency gRPC streaming on Solana through the [Web3 RPC API](/portal/documentation/apis/web3). Best for: indexers, market makers, and latency-sensitive pipelines. ## Choosing a path | You are building... | Start with | | :------------------------------------- | :----------------------------------------------------------------- | | Swaps in a wallet or dApp | [SDKs](/portal/documentation/sdks/overview) on top of the Swap API | | A backend service in any language | REST APIs | | Live trading or notification UX | WebSockets | | A replacement for self-hosted nodes | JSON-RPC ([Web3 RPC API](/portal/documentation/apis/web3)) | | A high-throughput Solana data pipeline | gRPC ([Web3 RPC API](/portal/documentation/apis/web3)) | https://business.1inch.com/portal/assets/docs-v2/overview/aqua.md 1inch Aqua is a programmable shared liquidity layer: it keeps capital in maker-controlled wallets and lets the same assets back multiple swap strategies at once -- no pooled custody, no capital migration. Makers ship virtual balances into strategies; takers fill swaps atomically against them. ## What Aqua gives you - A self-custodial liquidity model -- the protocol holds no tokens; virtual balances are an on-chain accounting counter. - **SwapVM**, a programmable execution engine where strategies are data, not per-strategy contracts. - Reusable strategy patterns (constant product, concentrated liquidity, pegged, RFQ, TWAP/DCA, and more). ## Explore the Aqua docs The full Aqua documentation lives in its own top-level section: - [Aqua -- Overview & Introduction](/portal/documentation/aqua/overview) - [Getting started](/portal/documentation/aqua/getting-started) -- build your first AquaApp - [Aqua liquidity layer](/portal/documentation/aqua/liquidity-layer/core-concepts) -- core concepts, strategies, virtual balances - [SwapVM](/portal/documentation/aqua/swapvm/swapvm-engine) -- engine, instruction set, and strategy patterns - [Reference](/portal/documentation/aqua/reference/smart-contract) -- contracts, events, encoding, addresses - [SDKs](/portal/documentation/sdks/utilities) -- Aqua Protocol and SwapVM SDKs and templates https://business.1inch.com/portal/assets/docs-v2/overview/developer-resources.md Tooling that keeps humans and AI agents productive while integrating. ## Build with AI - [1inch MCP server](/portal/documentation/ai-integration/mcp-server) -- connect Cursor, Claude, and other MCP-capable assistants directly to 1inch documentation and live APIs. Your agent can search docs, inspect endpoints, and debug failing requests (pass the `X-Request-Id` response header to the MCP `debug` tool for exact log lookup). - [Supported AI clients](/portal/documentation/ai-integration/supported-clients) -- setup guides per editor and assistant. - [llms.txt](/llms.txt) -- the canonical, machine-readable index of all APIs and docs, optimized for RAG pipelines and autonomous agents. ## Reference & code - [OpenAPI specifications](/portal/documentation/apis/authentication) -- every API ships an interactive reference with try-it-out; run real requests against your key from the browser. - [Available SDKs](/portal/documentation/sdks/overview) -- TypeScript SDKs for cross-chain swaps, intent swaps, and limit orders, with example repositories. ## Help - [API troubleshooting](/portal/documentation/faq/api-troubleshooting) and [API error messages](/portal/documentation/faq/api-error-messages-explained) -- the fastest answers to common integration issues. - Include the `X-Request-Id` response header when contacting support -- it correlates directly with server-side logs. ## SDKs https://business.1inch.com/portal/assets/docs-v2/sdks/overview.md The 1inch SDKs are official TypeScript libraries for building, integrating, and extending swap functionality across networks and execution models. They wrap the low-level work -- order building, EIP-712 signing, submission, and contract interaction -- so you ship faster than calling the REST APIs directly. All SDKs are open source, published on npm under the `@1inch` scope, and share the same [API key authentication](/portal/documentation/apis/authentication). ## Choose your SDK | SDK | Use it for | Chains | | :---------------------------------------------------------------- | :----------------------------------------------- | :----------- | | [Cross-Chain SDK](/portal/documentation/sdks/cross-chain-sdk) | Gasless cross-chain swaps (Fusion+) | EVM <-> EVM | | [Fusion SDK](/portal/documentation/sdks/fusion-sdk) | Intent-based, gasless same-chain swaps (Fusion) | EVM | | [Solana Fusion SDK](/portal/documentation/sdks/solana-fusion-sdk) | Intent-based swaps on Solana | Solana | | [Limit Order SDK](/portal/documentation/sdks/limit-order-sdk) | Create, sign, and submit limit orders | EVM | | [Aqua SDK](/portal/documentation/sdks/aqua-sdk) | Create, sign, and submit Aqua trading strategies | EVM | | [Utilities](/portal/documentation/sdks/utilities) | Solidity helpers and protocol SDKs | EVM / Solana | ## Install Every SDK installs from npm and works in Node.js and modern bundlers: ```bash npm install @1inch/cross-chain-sdk # or: @1inch/fusion-sdk, @1inch/solana-fusion-sdk, @1inch/limit-order-sdk, @1inch/aqua-sdk ``` ## When to use an SDK vs the REST API - **Use an SDK** when you build swaps or orders in TypeScript and want order construction, signing, and submission handled for you. - **Use the [REST APIs](/portal/documentation/apis/authentication)** directly for backends in other languages, or when you need full control over each request. - **Use the [MCP server](/portal/documentation/ai-integration/mcp-server)** to let an AI coding assistant build and run these flows for you. ## AI & automation For AI agents and LLM tools, a machine-readable index of all APIs and SDKs is available at [`/llms.txt`](/llms.txt), and the [1inch MCP server](/portal/documentation/ai-integration/mcp-server) can retrieve full SDK examples on demand. https://business.1inch.com/portal/assets/docs-v2/sdks/cross-chain-sdk.md The 1inch Cross-Chain SDK is the official TypeScript library for building gasless cross-chain swaps on top of Fusion+. It handles quote retrieval, order construction, EIP-712 signing, and submission so you can move assets between EVM chains without managing bridges or resolver infrastructure yourself. - **Repository:** [github.com/1inch/cross-chain-sdk](https://github.com/1inch/cross-chain-sdk) - **Example:** [cross-chain-resolver-example](https://github.com/1inch/cross-chain-resolver-example) - **Language:** TypeScript - **Best for:** wallets and apps that let users swap across EVM chains in one flow. ## Install ```bash npm install @1inch/cross-chain-sdk ``` ## How it fits The SDK targets the [Cross-Chain Swaps (Fusion+)](/portal/documentation/apis/swap/cross-chain-swap) API. You build and sign an intent order client-side; 1inch resolvers compete to fill it across chains, and the taker pays no gas on the destination side. For the protocol details and endpoint reference, see the [Cross-Chain Swap docs](/portal/documentation/apis/swap/cross-chain-swap). ## Related - [Fusion SDK](/portal/documentation/sdks/fusion-sdk) -- same-chain intent swaps - [Swap API overview](/portal/documentation/apis/swap/swap) - Retrieve a full working example through the [1inch MCP server](/portal/documentation/ai-integration/mcp-server) https://business.1inch.com/portal/assets/docs-v2/sdks/fusion-sdk.md The 1inch Fusion SDK is the official TypeScript library for intent-based, gasless swaps on EVM chains. Instead of submitting a transaction yourself, you build and sign a Fusion order; 1inch resolvers compete to fill it at the best rate and cover the gas. The SDK wraps order construction, EIP-712 signing, and submission. - **Repository:** [github.com/1inch/fusion-sdk](https://github.com/1inch/fusion-sdk) - **Example:** [fusion-resolver-example](https://github.com/1inch/fusion-resolver-example) - **Language:** TypeScript - **Best for:** gasless, MEV-protected same-chain swaps in wallets and dApps. ## Install ```bash npm install @1inch/fusion-sdk ``` ## How it fits The SDK targets the [Intent Swap (Fusion)](/portal/documentation/apis/swap/intent-swap) API. For Solana intent swaps use the [Solana Fusion SDK](/portal/documentation/sdks/solana-fusion-sdk); for swaps that span two chains use the [Cross-Chain SDK](/portal/documentation/sdks/cross-chain-sdk). ## Related - [Intent Swap (Fusion) docs](/portal/documentation/apis/swap/intent-swap) - [Swap API overview](/portal/documentation/apis/swap/swap) - Retrieve a full working example through the [1inch MCP server](/portal/documentation/ai-integration/mcp-server) https://business.1inch.com/portal/assets/docs-v2/sdks/solana-fusion-sdk.md The 1inch Solana Fusion SDK is the official TypeScript library for intent-based swaps on Solana. It brings the Fusion model -- sign an intent, let resolvers fill it -- to the Solana runtime, handling order construction and signing for SPL token swaps. - **Repository:** [github.com/1inch/solana-fusion-sdk](https://github.com/1inch/solana-fusion-sdk) - **Language:** TypeScript - **Best for:** gasless, intent-based SPL token swaps on Solana. ## Install ```bash npm install @1inch/solana-fusion-sdk ``` ## How it fits The SDK targets the Solana coverage of the [Intent Swap (Fusion)](/portal/documentation/apis/swap/intent-swap) API. For EVM intent swaps use the [Fusion SDK](/portal/documentation/sdks/fusion-sdk). ## Related - [Intent Swap (Fusion) docs](/portal/documentation/apis/swap/intent-swap) - [Supported chains](/portal/documentation/overview/supported-chains) - Retrieve a full working example through the [1inch MCP server](/portal/documentation/ai-integration/mcp-server) https://business.1inch.com/portal/assets/docs-v2/sdks/limit-order-sdk.md The 1inch Limit Order SDK is the official TypeScript library for the Limit Order Protocol. It builds orders, produces the EIP-712 typed data for signing, and submits signed orders to the 1inch Orderbook -- the same flow the [MCP `orderbook` tool](/portal/documentation/ai-integration/tools-reference) uses server-side. - **Repository:** [github.com/1inch/limit-order-sdk](https://github.com/1inch/limit-order-sdk) - **Language:** TypeScript - **Best for:** creating, signing, and submitting limit orders from a dApp or backend. ## Install ```bash npm install @1inch/limit-order-sdk ``` ## Typical flow 1. Build an order with your maker/taker assets, amounts, and expiration. 2. Approve the maker asset for the Limit Order Protocol contract. 3. Sign the returned EIP-712 typed data with `eth_signTypedData_v4`. 4. Submit the signed order to the [Orderbook API](/portal/documentation/apis/orderbook). ## Related - [Orderbook API docs](/portal/documentation/apis/orderbook) - Retrieve a full working example through the [1inch MCP server](/portal/documentation/ai-integration/mcp-server) https://business.1inch.com/portal/assets/docs-v2/sdks/aqua-sdk.md The 1inch Aqua SDK is the official TypeScript toolkit for [1inch Aqua](/portal/documentation/aqua/overview/overview), the shared liquidity layer. It builds typed call data for the registry's core maker operations -- `ship` and `dock` -- decodes the registry events (`Shipped`, `Pushed`, `Pulled`, `Docked`), and bundles pre-configured contract addresses for all supported networks. - **Repository:** [github.com/1inch/sdks](https://github.com/1inch/sdks) (`typescript/aqua`) - **npm:** [@1inch/aqua-sdk](https://www.npmjs.com/package/@1inch/aqua-sdk) - **Language:** TypeScript - **Best for:** funding, closing, and tracking Aqua strategies from a dApp or backend. ## Install ```bash pnpm add @1inch/aqua-sdk ``` The SDK produces ready-to-send transaction objects; the end-to-end examples in the package README send them with [viem](https://viem.sh/), but any EVM client works. ## Ship and dock a strategy `AquaProtocolContract` encodes the maker calls against the Aqua registry. `ship()` opens a strategy by setting its virtual token balances; `dock()` closes it and withdraws whatever remains. Strategy bytes are defined by the target AquaApp -- each app declares its own schema, so encode them to match the app's contract (see [Build an AquaApp](/portal/documentation/aqua/getting-started/build-an-aquaapp)). ```typescript import { AquaProtocolContract, AQUA_CONTRACT_ADDRESSES, Address, HexString, NetworkEnum } from "@1inch/aqua-sdk"; const aqua = new AquaProtocolContract(AQUA_CONTRACT_ADDRESSES[NetworkEnum.ETHEREUM]); // Open the strategy with maker funding const shipTx = aqua.ship({ app: new Address("0x..."), // your AquaApp contract strategy: new HexString("0x..."), // strategy bytes, encoded per the app's schema amountsAndTokens: [{ token: new Address("0x..."), amount: 1000000000000000000n }] }); // shipTx is { to, data, value } -- sign and send it with your wallet client // Close the strategy and withdraw remaining balances const dockTx = aqua.dock({ app: new Address("0x..."), strategyHash: AquaProtocolContract.calculateStrategyHash(new HexString("0x...")), tokens: [new Address("0x..."), new Address("0x...")] }); ``` `AQUA_CONTRACT_ADDRESSES` resolves to the same registry address -- `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` -- on every supported network (Ethereum, Base, Arbitrum, Optimism, Polygon, BNB Chain, and more). ## Decode registry events Every registry event has a decoder class -- `ShippedEvent`, `PushedEvent`, `PulledEvent`, `DockedEvent` -- with a `fromLog()` factory, so you can track strategy activity from raw logs (or discover strategies through the [Aqua API](/portal/documentation/aqua/api/introduction)): ```typescript import { PushedEvent, ShippedEvent } from "@1inch/aqua-sdk"; const pushed = PushedEvent.fromLog({ data: log.data, topics: log.topics }); pushed.maker; // Address pushed.app; // Address pushed.strategyHash; // HexString pushed.token; // Address pushed.amount; // bigint const shipped = ShippedEvent.fromLog(log); ``` ## Related - [Aqua SDK overview](/portal/documentation/aqua/reference/sdk-overview) - [Build an AquaApp](/portal/documentation/aqua/getting-started/build-an-aquaapp) - [Aqua API](/portal/documentation/aqua/api/introduction) - [@1inch/aqua-sdk on npm](https://www.npmjs.com/package/@1inch/aqua-sdk) https://business.1inch.com/portal/assets/docs-v2/sdks/utilities.md Beyond the swap and order SDKs, 1inch publishes lower-level libraries for contract development and protocol integration. ## Solidity Utils Reusable Solidity building blocks used across 1inch protocol contracts -- safe ERC-20 helpers, EIP-712 utilities, and common patterns for on-chain integrations. - **Repository:** [github.com/1inch/solidity-utils](https://github.com/1inch/solidity-utils) - **Language:** Solidity - **Best for:** smart-contract teams integrating with or building on 1inch protocols. ## Protocol SDKs For deeper protocol-level work, dedicated SDKs and templates cover the Aqua liquidity layer and the SwapVM execution engine: | SDK | Repository | Template | | :------------ | :------------------------------------------------------------------------------------------------ | :-------------------------------------------------------------- | | Aqua Protocol | [github.com/1inch/sdks/.../aqua](https://github.com/1inch/sdks/tree/master/typescript/aqua) | [aqua-app-template](https://github.com/1inch/aqua-app-template) | | SwapVM | [github.com/1inch/sdks/.../swap-vm](https://github.com/1inch/sdks/tree/master/typescript/swap-vm) | [swap-vm-template](https://github.com/1inch/swap-vm-template) | ## Related - [SDKs overview](/portal/documentation/sdks/overview) - [`/llms.txt`](/llms.txt) -- machine-readable index for AI agents ## AI Integration https://business.1inch.com/portal/assets/docs-v2/ai-integration/overview.md 1inch offers two complementary ways to build with AI: the **[MCP server](/portal/documentation/ai-integration/mcp-server)** -- a live protocol your AI assistant uses to execute swaps, place limit orders, manage Aqua positions, query APIs, and search documentation -- and **[AI Skills](/portal/documentation/ai-integration/ai-skills)**, reusable instruction packages that teach agents setup patterns and exact `product_api` call recipes (spot prices, gas, portfolio, and more). Both are free to connect, work together, and share the same [authentication](/portal/documentation/ai-integration/mcp-server#authentication). For AI agents and automation that don't use MCP, the [`/llms.txt`](/llms.txt) index provides a machine-readable map of all APIs following the [llmstxt.org](https://llmstxt.org) specification. ## Where to start - [MCP server](/portal/documentation/ai-integration/mcp-server) -- what it is, capabilities, authentication, and agentic use - [Tools reference](/portal/documentation/ai-integration/tools-reference) -- full parameter reference for all nine MCP tools - [AI Skills](/portal/documentation/ai-integration/ai-skills) -- teach your agent the 1inch setup and per-domain call recipes once - [Supported clients](/portal/documentation/ai-integration/supported-clients) -- setup for Cursor, Claude, VS Code, and more ## What you can do Once connected, you work through natural conversation. Your AI assistant calls the right [tools](/portal/documentation/ai-integration/tools-reference) behind the scenes. ### Execute token swaps Describe the swap you want ("swap 100 USDC to ETH on Base"), and the assistant automatically finds the best rate across all available routes, handles token resolution, allowance checks, and transaction building. You only need to sign the transaction. Supports same-chain swaps (classic and Fusion) and cross-chain swaps. ### Place limit orders Create limit orders without installing any SDK. Tell your AI "place a limit order to sell 100 USDC for 100 USDT on Ethereum", and the server builds the order server-side, returns the data for you to sign, and submits it to the orderbook. ### Query any 1inch API The `product_api` tool gives your AI access to **all 1inch Business product APIs** -- Portfolio, Spot Price, Token, Gas Price, Web3 RPC, Balance, NFT, Charts, Transaction Gateway, and more. Ask "what's my portfolio value on Ethereum?" or "what's the current gas price on Base?" -- every API endpoint available in the 1inch Business platform is accessible through this single tool. ### Connect a non-custodial wallet The `walletconnect` tool pairs your AI session with a real wallet via [WalletConnect v2](https://docs.walletconnect.com/) so the assistant can request signatures and submit transactions for you -- without ever seeing your private key. Ask "connect my wallet" and the assistant returns a pairing QR (or, on mobile, a tap-to-open deep link for [supported wallets](/portal/documentation/ai-integration/tools-reference#supported-wallets) like MetaMask, Phantom, Coinbase Wallet, and 1inch Wallet). After you approve, follow-up requests like "now execute that swap" go straight to your wallet. Works for **EVM** and **Solana**. :::info The MCP server **never holds private keys**. Every signature and transaction request is shown to you in your own wallet app -- you approve each one. ::: ### Manage Aqua positions Makers can preview, open, and close [1inch Aqua](/portal/documentation/aqua/overview/overview) positions from the same `aqua` tool. Ask "preview a USDC/WETH position on Ethereum for my wallet" and the assistant returns market price, opening price, range, fee, and advisories. Confirm the same parameters with `confirmed: true` and your wallet signs the transaction. Closing is permanent: the position stops quoting and your tokens stay in your wallet. Writes are non-custodial. Pair WalletConnect and sign the 1inch Terms of Use; no API key is required. An API key or OAuth session is optional. Anonymous WalletConnect writes also pass a geo and sanctions check. API key and OAuth sessions skip that check. Analytics still work. Parameter reference: [`aqua` tool](/portal/documentation/ai-integration/tools-reference#aqua). ### Analyze Aqua liquidity The `aqua` tool also answers questions about Aqua in plain English. Ask "what's the TVL for maker 0x1234...?" or "show me the open positions on Base" and your assistant returns structured results: maker stats, position lists, and per-position state, activity, and volume series. Analytics need no authentication. ### Search documentation and browse SDK examples Ask any question about 1inch APIs, supported chains, error codes, or integration patterns. Your AI searches across all documentation, API references, and SDK guides -- no manual browsing required. You can also pull complete, working code examples directly into your conversation in TypeScript, Go, or Python. :::info The examples are provided in TypeScript -- but your AI assistant can translate them into any language. Just ask for "the swap example in Java" or "convert this to Rust". ::: ## Example prompts The following show the kinds of requests that work well with the 1inch MCP server and AI Skills. Adapt them to your use case. ### Executing swaps > "Swap 1000 USDC to ETH on Ethereum from my wallet 0x1234..." > "Find the best rate for swapping 0.5 ETH to USDT on Arbitrum." > "Swap 100 USDC from Base to Polygon." ### Managing limit orders > "Place a limit order to sell 1000 USDC for 1000 USDT on Ethereum, expiring in 1 hour." > "List my active limit orders on Ethereum for wallet 0x1234..." ### Querying portfolio and token data > "What's my portfolio P&L on Ethereum for wallet 0x1234...?" > "What's the current gas price on Base right now?" ### Connecting a wallet (WalletConnect) > "Connect my wallet so you can execute swaps for me." > "Pair my Phantom wallet for Solana -- I'll scan the QR." ### Managing Aqua positions > "Preview an Aqua USDC/WETH position on Ethereum for my wallet, then create it if the range looks right." > "Connect my wallet, accept the 1inch Terms of Use, and open a concentrated Aqua position." > "Close my Aqua position with strategy hash 0xaaaa... on Ethereum." ### Analyzing Aqua strategies > "What are the aggregated stats (TVL, fees) for Aqua maker 0x1234... on Ethereum and Base?" > "List the currently open Aqua strategies, then give me the daily volume series for the largest one." ### Starting a new integration > "I'm building a swap feature on Base. Show me a complete TypeScript example for swapping ERC-20 tokens using the 1inch API." > "How do I call the 1inch swap API from Python?" The assistant retrieves the relevant SDK example with full source code -- including wallet setup, token approval, swap execution, and error handling -- in the language you need. ### Prompting tips - **Just say what you want done.** "Swap 100 USDC to ETH on Base" -- the assistant handles the rest. - **Mention your chain.** "on Base", "on Arbitrum", or "cross-chain from Ethereum to Polygon" helps pick the right route. - **Combine execution with research.** Ask "what's the best rate for this swap?" then "now execute it" -- all in the same session. - **Ask for examples when building features.** The examples are tested, complete implementations that give the AI better context. https://business.1inch.com/portal/assets/docs-v2/ai-integration/mcp-server.md ## What is an MCP server? An **MCP server** exposes tools and data to AI assistants over the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) -- an open standard that lets editors and agents (Cursor, Claude, VS Code Copilot, and others) call external services through natural conversation instead of hand-written API glue. The client runs the model; the MCP server provides the capabilities. The **1inch MCP server** is an AI-native integration for DeFi: your editor or CLI agent speaks to 1inch over MCP so it can execute token swaps, place limit orders, query portfolio and price data, connect a wallet, analyze Aqua liquidity, manage Aqua maker positions, and search documentation -- all through natural language. The server is hosted at: ``` https://api.1inch.com/mcp/protocol ``` The server is listed on the [official MCP Registry](https://modelcontextprotocol.io/registry/about) as **`com.1inch.business/mcp`**, so registry-aware clients (including VS Code and GitHub Copilot) can discover and install it without manually pasting the URL. To connect it to your editor, see [Supported clients](/portal/documentation/ai-integration/supported-clients). :::tip For AI agents and automation that don't use MCP, see the [LLM-optimized documentation](/llms.txt) -- a machine-readable index of all APIs following the [llmstxt.org](https://llmstxt.org) specification. ::: ## Cost and access There is **no separate charge** to connect to the MCP server. **Swap**, **Orderbook**, **Business API access**, and the **`debug`** tool use the same authentication and API entitlements as the rest of the 1inch Business / developer stack (standard API terms apply to those calls). Aqua analytics, documentation search, the example catalog, example source, and **`walletconnect`** pairing work **without authentication**, so you can start exploring immediately. Aqua maker writes (`create_position`, `close_position`) also work without an API key: pair WalletConnect and sign the 1inch Terms of Use. ## Non-custodial by design The `walletconnect` tool relays signing requests to the user's own wallet via [WalletConnect v2](https://docs.walletconnect.com/). The MCP server **never holds private keys** -- the user always approves each signature or transaction in their own wallet app. ## Capabilities The MCP server exposes **nine tools**. `search`, `list_examples`, `get_example`, `walletconnect`, and `aqua` are public; everything else requires [authentication](#authentication). Full parameter reference: [Tools reference](/portal/documentation/ai-integration/tools-reference). | Tool | Auth | Description | | :-------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `swap` | Required | Build quotes and execution steps for Pro, Gasless Swap, and Gasless Cross-chain Swap | | `orderbook` | Required | Build, sign, submit, list, and cancel limit orders via the 1inch Orderbook API | | `product_api` | Required | Call 1inch Business product APIs (Portfolio, Spot Price, Token, Gas Price, Web3 RPC, and more) | | `walletconnect` | Public | Pair an EVM and/or Solana wallet via WalletConnect v2; sign messages and send transactions | | `debug` | Required | Debug production issues: look up request logs by correlation id or time window | | `aqua` | Public | 1inch Aqua position analytics; preview, create, and close maker positions (`create_position` / `close_position`) via WalletConnect after signing the Terms of Use (no API key) | | `search` | Public | Search documentation and API reference | | `list_examples` | Public | List available SDK code examples | | `get_example` | Public | Retrieve full example source (TypeScript, Go, Python, etc.) | ## Authentication `search`, `list_examples`, `get_example`, `walletconnect`, and `aqua` work **without authentication**. Maker writes (`create_position` / `close_position`) pair WalletConnect and sign the 1inch Terms of Use. Taker `quote` / `build_swap` need an API key or OAuth session. The other execution tools (`swap`, `orderbook`, `product_api`, `debug`) require authentication via either an API key or OAuth. ### API key Pass your 1inch API key via the `Authorization: Bearer ` header. Get a key from the [1inch Business Portal](https://business.1inch.com/portal). How to set the header depends on your MCP client -- see [Authentication setup](/portal/documentation/ai-integration/supported-clients#authentication-setup) for client-specific configuration. ### OAuth When you call an authenticated tool without an API key, MCP clients that support OAuth automatically prompt you to log in via the 1inch Business Portal. After authentication, all tools become available for the session. For all supported OAuth flows including `client_credentials` for server-to-server use, see [OAuth 2.1](/portal/documentation/apis/authentication#oauth-21). ## Agentic use "Agentic" here means your **client runs a tool loop**: the model decides which MCP tools to call, uses the results, and may call again until the task is done. You describe the outcome; the agent plans steps. ### Typical tool chains | Goal | How the agent usually proceeds | | :------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Swap or quote exploration | Swap tool for quotes / steps; combine with doc search for mode differences (Pro vs Gasless Swap vs Gasless Cross-chain Swap) | | Place a limit order | Orderbook tool to build the order server-side -> sign the returned typed data -> submit back via orderbook | | Operational checks | Business API tool for token metadata, gas, spot price, portfolio P&L, etc., then map results back into your code | | Connect a wallet | `walletconnect` `connect` to get a pairing QR / link -> user approves in their wallet -> poll `status` -> use `sign` / `send_transaction` | | Debug production issues | `debug` with a request id from API responses or a time window to retrieve logs for your account | | Manage an Aqua position | `aqua` `create_position` (preview) -> `create_position` (`confirmed: true` + `previewRef`) -> monitor with `strategy_overview` -> `close_position` (`confirmed: true`) | | Analyze Aqua liquidity | `aqua` `maker_stats` or `list_opened` to find positions -> `strategy_overview` -> `strategy_activity` / `strategy_volume` to drill in | | New feature in your repo | Documentation search -> example catalog -> example source -> apply or refactor code in your project | You do not need to name each tool: a capable client exposes them to the model; prompts like "swap 100 USDC to ETH on Base" or "find the official swap example" are enough. ### Prompt patterns that work well for agents - **Direct execution:** "Swap 100 USDC to ETH on Base from my wallet 0x1234..." - **Multi-step flows:** "Place a limit order to sell 1000 USDC for 1000 USDT on Ethereum, expiring in 1 hour." - **Manage an Aqua position:** "Preview a USDC/WETH Aqua position on Ethereum for my wallet, then create it if the range looks right." - **Outcome first:** "Implement ERC-20 -> USDC swap on Base in TypeScript; use 1inch MCP for docs and examples, then wire it into `src/swap.ts`." - **Operational queries:** "What's my portfolio P&L on Ethereum?" or "What's the current gas price on Base?" ### MCP vs `llms.txt` for agents | | MCP | [`/llms.txt`](/llms.txt) | | :--------------------- | :---------------------------------------------------------------------- | :----------------------------------------------------- | | **Best for** | Agentic execution (swaps, orders, APIs) + interactive coding assistants | Bulk context, custom RAG, CI jobs, clients without MCP | | **Freshness** | Live API access + server-side search (periodically indexed) | Snapshot index you fetch and chunk yourself | | **Authenticated APIs** | Yes -- swaps, orderbook, portfolio, gas, spot price, debugging, and more | No -- static links and summaries only | Use both if you split work: `llms.txt` in a pipeline for broad grounding, MCP in the IDE for grounded examples and tool-backed answers. ### Limits and responsibility - **Sessions:** each client window may count as a session; agents should avoid spawning unbounded parallel MCP connections. See [Too many sessions (429)](/portal/documentation/ai-integration/supported-clients#troubleshooting). - **Approvals:** your client may ask you to approve tool calls, especially OAuth or API writes. The MCP server does not bypass your org's access rules. - **Secrets:** pass API keys or headers only as documented for your client; never commit credentials. ## Legal disclaimer Use of the 1inch MCP Server is subject to the [1inch Business Portal Terms of Service](/portal/assets/legal-docs/terms_of_service_public_api_20260727.pdf) and the applicable [Software Legal Notice](/portal/assets/legal-docs/software_policy_20260218.pdf). Users are responsible for ensuring compliance with applicable regulations. https://business.1inch.com/portal/assets/docs-v2/ai-integration/tools-reference.md The 1inch MCP server provides nine tools. Five execution tools require [authentication](/portal/documentation/ai-integration/mcp-server#authentication); four public tools work without it. For the conceptual overview, see the [MCP server](/portal/documentation/ai-integration/mcp-server) page. Tool schemas describe parameters, not workflows. For single-step calls the schemas are enough, but several tools are one step of a multi-call flow (sign-then-resubmit, approvals, pairing) or accept raw paths the agent has to know. The [AI Skills](/portal/documentation/ai-integration/ai-skills) close that gap -- each tool below notes what its skill adds and when the tool alone falls short. The reverse also holds: a skill is static context and executes nothing by itself; it always needs this MCP connection, plus authentication for the execution tools. ## Authenticated tools (API key or OAuth) These tools require [authentication](/portal/documentation/ai-integration/mcp-server#authentication). Authenticate via an API key header or OAuth -- see [Authentication setup](/portal/documentation/ai-integration/supported-clients#authentication-setup). ### swap Execute token swaps via 1inch. The server automatically finds the best rate across all available routes, handles token resolution, allowance checks, and transaction building -- the agent only signs transactions where required. Supports same-chain and cross-chain swaps. **When the tool alone is enough -- and when it isn't:** a single quote (`quoteOnly: true`) or a classic swap is self-contained. Intent and cross-chain execution is a two-call flow -- sign the returned typed data, then call `swap` again with `signedOrder` -- and agents without guidance commonly stop after the first call or pick the wrong mode. [`1inch-swap`](/portal/documentation/ai-integration/ai-skills#1inch-swap) carries the per-mode signing guides and the quote-first comparison flow. | Parameter | Type | Required | Description | | :-------------- | :------ | :------- | :-------------------------------------------------------------- | | `src` | string | Yes | Source token (symbol or address) | | `dst` | string | Yes | Destination token (symbol or address) | | `amount` | string | Yes | Amount in smallest units | | `chain` | number | Yes | Chain ID | | `from` | string | Yes | Sender wallet address | | `quoteOnly` | boolean | No | If true, returns quotes without building a transaction | | `preferredType` | string | No | `"classic"`, `"fusion"`, or `"crosschain"` (omit to auto-route) | | `dstChain` | number | No | Destination chain ID (triggers cross-chain mode) | | `slippage` | number | No | Slippage tolerance in percent (default 0.5) | | `signedOrder` | string | No | Submit a previously signed Fusion/cross-chain order | | `orderHash` | string | No | Order hash for submission | **Typical flow:** 1. Call `swap` with source/destination tokens and amount -- the server finds the best rate automatically 2. Sign the returned transaction (`eth_sendTransaction`) or typed data (`eth_signTypedData_v4`) 3. For Fusion/cross-chain: call `swap` again with the `signedOrder` to submit ### orderbook Manage limit orders via the 1inch Orderbook API (v4.1). The server builds orders server-side using the Limit Order SDK -- no SDK installation needed on the client. **When the tool alone is enough -- and when it isn't:** `list` queries are self-contained. Placing an order is a sequence the schema does not convey -- `build`, then a possible approve transaction, then an `eth_signTypedData_v4` signature, then `create` -- and cancellation happens on-chain rather than through an API call. [`1inch-orderbook`](/portal/documentation/ai-integration/ai-skills#1inch-orderbook) walks the agent through that ordering and the approval handling. **Actions:** | Action | Description | | :------- | :----------------------------------------------------------------------- | | `build` | Server-side order construction. Returns EIP-712 typed data for signing | | `create` | Submit a signed order to the orderbook | | `list` | Query orders by maker address, order hash, or browse all with pagination | | `cancel` | Load order state and return on-chain cancellation guidance | **Build parameters:** | Parameter | Type | Required | Description | | :------------------ | :----- | :------- | :----------------------------------- | | `action` | string | Yes | `"build"` | | `chain` | number | Yes | Chain ID | | `makerAsset` | string | Yes | Token to sell (symbol or address) | | `takerAsset` | string | Yes | Token to receive (symbol or address) | | `makingAmount` | string | Yes | Amount to sell (smallest units) | | `takingAmount` | string | Yes | Amount to receive (smallest units) | | `maker` | string | Yes | Maker wallet address | | `expirationSeconds` | number | No | Expiration in seconds (default 600) | **Typical flow:** 1. Call `orderbook` with `action: "build"` and your order parameters 2. Approve the maker asset for the Limit Order Protocol contract (address provided in the response) 3. Sign the returned `typedData` with `eth_signTypedData_v4` 4. Call `orderbook` with `action: "create"`, passing `orderHash`, `signature`, and `orderData` from the build response ### product_api Call any 1inch Business product API endpoint using your authenticated session. This single tool provides access to **all** 1inch APIs: Portfolio, Spot Price, Token, Gas Price, Web3 RPC, Balance, NFT, Charts, Transaction Gateway, and every other endpoint available in the 1inch Business platform. For **Portfolio v5.0** through this gateway, use paths starting with `/portfolio/portfolio/v5.0/...` (the first `/portfolio` segment is stripped when routing to the Portfolio service). **When the tool alone is enough -- and when it isn't:** the tool is a raw gateway -- it accepts any path but does not tell the agent which endpoint or version answers a given question. If you already know the exact path, it is fully usable on its own; unguided agents instead tend to invent paths (404s) or misread units -- Spot Price, for example, returns native token WEI unless `currency` is set. Three skills carry the exact call recipes: [`1inch-wallet-data`](/portal/documentation/ai-integration/ai-skills#1inch-wallet-data) (portfolio, balances, history, NFTs, tokens), [`1inch-market-data`](/portal/documentation/ai-integration/ai-skills#1inch-market-data) (spot prices, gas, charts), and [`1inch-infrastructure`](/portal/documentation/ai-integration/ai-skills#1inch-infrastructure) (Web3 RPC, tx gateway, domains). When no recipe matches, the agent should confirm paths via the [`search`](#search) tool rather than guessing. | Parameter | Type | Required | Description | | :-------- | :----- | :------- | :---------------------------------------------------------------- | | `method` | string | No | `"GET"` or `"POST"` (default `"GET"`) | | `path` | string | Yes | API path (e.g. `/portfolio/portfolio/v5.0/general/current_value`) | | `query` | object | No | Query parameters as key-value pairs | | `body` | object | No | Request body for POST requests | ### walletconnect Pair a non-custodial wallet with the MCP session via [WalletConnect v2](https://docs.walletconnect.com/) so the assistant can request signatures and submit on-chain transactions for the user. Supports **EVM** and **Solana** namespaces (depending on the wallet). The MCP server **never sees private keys** -- the wallet app shows every signature / transaction request and the user approves it directly. **When to use it:** any flow that needs a real signature or `eth_sendTransaction` driven by the assistant -- for example completing a `swap`, sending an order on-chain, or confirming an Aqua `create_position` / `close_position` -- instead of pasting transaction data back into a wallet manually. **When the tool alone is enough -- and when it isn't:** the individual actions are simple, but the pairing flow has sharp edges the schema does not encode -- `pending` status is a success state to keep polling (not an error), many mobile wallets reject EVM and Solana proposed together, and the QR expires after about 10 minutes. Agents without that context restart pairings or give up mid-flow. [`1inch-walletconnect`](/portal/documentation/ai-integration/ai-skills#1inch-walletconnect) encodes those rules and the session handling. | Parameter | Type | Required | Description | | :--------------- | :----- | :------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `action` | string | Yes | One of: `"connect"`, `"status"`, `"sign"`, `"send_transaction"`, `"accept_terms"`, `"disconnect"` | | `pairingTarget` | string | No (only with `connect`) | What to advertise in the pairing proposal: `"eth"` (default, EVM only), `"solana"`, or `"both"`. Many mobile wallets fail when EVM and Solana are proposed together | | `sessionTtlDays` | number | No (only with `connect`) | How long to persist the WalletConnect session (days). Server clamps to a per-deployment maximum (default 30, max 365) | | `method` | string | Yes (only with `sign`) | `"personal_sign"`, `"eth_signTypedData_v4"`, `"solana_signMessage"`, or `"solana_signTransaction"` | | `params` | array | Yes (only with `sign`) | Exactly two items, shape depends on `method` (see below) | | `chainId` | number | No (only with `sign`/`personal_sign`) | EVM numeric chain id when signing | | `solanaChainId` | string | No (only with `sign` Solana methods) | CAIP-2 chain id, e.g. `"solana:5eykt4UsFv8P8NJdT7ipf5EZiJUfk8tXwnUS2esLgDU"`. When omitted, the first Solana chain on the session is used | | `tx` | object | Yes (only with `send_transaction`) | EVM: `{ to, chainId, data?, value?, from? }`. Solana: `{ chainId: "solana:...", serializedTransaction: "", from? }` | | `address` | string | No (only with `accept_terms`) | EVM address that signs the 1inch Terms of Use. Must belong to the active session. When omitted, the session's primary EVM account is used | **`sign` `params` by method:** | Method | `params` shape | | :----------------------- | :-------------------------------------------- | | `personal_sign` | `[message: string, signerAddress: "0x..."]` | | `eth_signTypedData_v4` | `[signerAddress: "0x...", typedDataJsonString]` | | `solana_signMessage` | `[message: string, signerPubkey: base58]` | | `solana_signTransaction` | `[serializedTransactionBase64, signerPubkey]` | **Typical flow:** 1. **`connect`** -- call with `action: "connect"` (optionally `pairingTarget: "solana"` or `"both"`). The response contains a pairing QR (PNG resource link) and a `wc:` URI. **Desktop / scan UX:** the assistant fetches the QR via MCP `resources/read` and renders or opens the PNG. **Mobile UX:** the response includes `mobileDeeplinks` for [curated wallets](#supported-wallets) -- tap one to open the wallet directly without scanning. 2. **User approves in the wallet.** The wallet shows the connection request; the user picks accounts/chains and approves. 3. **`status`** -- call with `action: "status"` until `kind: "session"` is returned (or a clear error). `kind: "pending"` is a **success** state -- keep polling. 4. **`accept_terms`** -- for anonymous Aqua writes, the connected wallet signs the 1inch Terms of Use once (`personal_sign`, no gas). Idempotent if already accepted. Required before `create_position` / `close_position` on an unauthenticated session. 5. **`sign` / `send_transaction`** -- drive the user's wallet for individual signatures or transactions. The user approves each request in their wallet app. 6. **`disconnect`** -- call with `action: "disconnect"` to end the session and clear server-side pairing state. **Idempotent connect:** calling `connect` again while a pairing is still in progress with the **same** `pairingTarget` returns the **same** `wc:` URI. Changing `pairingTarget` starts a new proposal and invalidates the previous QR. #### Supported wallets The `connect` response includes `mobileDeeplinks` for these curated wallets so users on mobile can pair with one tap. Any WalletConnect v2-compatible wallet can also pair by scanning the QR or pasting the `wc:` URI: | Wallet | Chains | | :-------------- | :---------- | | MetaMask | EVM | | 1inch Wallet | EVM, Solana | | Coinbase Wallet | EVM, Solana | | Trust Wallet | EVM, Solana | | Phantom | EVM, Solana | | Solflare | Solana | #### Error states The tool returns a structured JSON error with `kind: "error"` and a `code` so the assistant can react: | `code` | Meaning | | :------------------------------- | :----------------------------------------------------------------------------------------------------------------------------- | | `not_connected` | No active WalletConnect session -- call `connect` first | | `invalid_request` | Malformed input or wrong field for the chosen action (e.g. `chainId` on a Solana `sign`) | | `solana_namespace_not_available` | Active session does not include Solana -- reconnect with `pairingTarget: "solana"` or `"both"` and approve Solana in the wallet | | `relay_*` | Transient WalletConnect relay error -- retry, or guide the user to reconnect | ### debug Debug production issues by looking up request logs. Provide a correlation id (`x-request-id` from any API response) to trace a specific request, or specify a time window to retrieve all logs for your account. **When the tool alone is enough -- and when it isn't:** if you already have an `x-request-id`, the tool is self-contained. Without one, the agent needs to know the time-window mode's required parameters and that results are scoped to the authenticated organization and capped by your plan's log retention. [`1inch-debug`](/portal/documentation/ai-integration/ai-skills#1inch-debug) covers the mode selection and those scoping rules. | Parameter | Type | Required | Description | | :---------- | :----- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `requestId` | string | No\* | `x-request-id` (or correlation id) from API responses. When set, searches logs for this value; optional `startTime` / `endTime` narrow the window (defaults to the last 24 hours ending at `endTime` or now). | | `startTime` | string | No\* | RFC3339 start of the query window (required with `endTime` when `requestId` is omitted). | | `endTime` | string | No | RFC3339 end of the query window. | | `logLevel` | string | No | Filter by log level: `"info"`, `"warn"`, or `"error"`. Omit to return all levels. Only applies to time-window mode (ignored when `requestId` is set). | | `limit` | number | No | Max log lines (1-100, default 50). | \*Provide **`requestId`**, or both **`startTime`** and **`endTime`**, to list all logs in that window (when `requestId` is omitted). **Log retention:** the time window you can query depends on the log retention period of your subscription plan. **Response shape:** JSON with `mode` (`request_id` or `logs`), `timeRange` (`start` / `end` in RFC3339), `count`, and `logs` (array of entries). Each entry includes timestamp, message, and optional level and HTTP status code. ## Public and mixed-access tools The tools in this section are usable without authentication. The `aqua` tool is public: analytics and maker writes (`create_position`, `close_position`) work without an API key (pair WalletConnect and sign the 1inch Terms of Use). Taker actions (`quote`, `build_swap`) need an API key / OAuth session. ### aqua Analytics for [1inch Aqua](/portal/documentation/aqua/overview/overview), the programmable shared-liquidity layer, plus optional maker writes that preview, create, and close XYC AMM positions. A position is full range by default, or concentrated when you pass a price range. Query maker performance, browse positions, and pull per-position state, on-chain events, and volume series without installing the Aqua SDK. Analytics need no authentication. Writes are non-custodial: the user's wallet signs. Maker writes work without an API key: pair WalletConnect and sign the 1inch Terms of Use. An API key or OAuth session is optional. **When the tool alone is enough -- and when it isn't:** `list_opened` and `maker_stats` work with nothing more than an address. The `strategy_*` actions need the full identifier tuple (`chainId`, `maker`, `app`, `strategyHash`), which only comes from earlier calls -- agents that call them blind get 404s. Creating a position is two calls to `create_position`: the first (without `confirmed`) returns a preview and a short-lived `previewRef`; the second repeats the parameters with the returned `openingPrice`, the `previewRef`, and `confirmed: true`. Anonymous makers must pair WalletConnect and call `accept_terms` first. [`1inch-aqua`](/portal/documentation/ai-integration/ai-skills#1inch-aqua) teaches the drill-down order, the preview-then-confirm flow, and the raw-path fallback for endpoints the tool does not cover. **Actions:** | Action | Description | | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `maker_stats` | Aggregated TVL, fees, APY, and strategy counts for one maker | | `list_maker_strategies` | Paginated list of a maker's strategies, filterable by chain and lifecycle status | | `strategy_overview` | Current state, performance, and token balances of a single strategy | | `strategy_activity` | Paginated on-chain events (opens, closes, swaps) for a single strategy | | `strategy_volume` | Time-bucketed volume series for a single strategy | | `list_opened` | Cross-chain feed of currently open positions, optionally filtered by app contract | | `create_position` | Two-phase: without `confirmed` returns a preview (market price, opening price, range, fee, amounts, advisories) and a `previewRef`; with `confirmed: true` + `previewRef` encodes (and optionally sends) the position | | `close_position` | Permanently close a position (`confirmed: true`); tokens stay in the wallet | | `quote` | SwapVM `quote` simulation calldata (authenticated B2B) | | `build_swap` | Taker swap against a position (authenticated B2B only) | **Required parameters per action:** | Action | Required | | :-------------------------- | :---------------------------------------------------------------------------- | | `maker_stats` | `address` | | `list_maker_strategies` | `address` | | `strategy_overview` | `chainId`, `maker`, `app`, `strategyHash` | | `strategy_activity` | `chainId`, `maker`, `app`, `strategyHash` | | `strategy_volume` | `chainId`, `maker`, `app`, `strategyHash` | | `list_opened` | none | | `create_position` (preview) | `chainId`, `maker`, `tokens` | | `create_position` (confirm) | `chainId`, `maker`, `tokens`, `openingPrice`, `previewRef`, `confirmed: true` | | `close_position` | `chainId`, `tokens`, `confirmed`, plus `strategy` or `strategyHash` | | `quote` / `build_swap` | `chainId`, `strategy`, `tokenIn`, `tokenOut`, `amount` | **Parameters:** | Parameter | Type | Required | Description | | :-------------------- | :-------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `action` | string | Yes | One of the ten actions above | | `address` | string | No\* | Maker EVM address (`maker_stats`, `list_maker_strategies`) | | `chainId` | number or array | No\* | Single chain ID for the `strategy_*` actions and every write action. An array of chain IDs filters `maker_stats`, `list_maker_strategies`, and `list_opened` -- omit it to include all supported chains | | `maker` | string | No\* | Maker EVM address (`strategy_*` actions, `create_position`) | | `app` | string | No\* | App contract address. Required by the `strategy_*` actions; optional filter for `list_opened`; optional on `close_position` (defaults to the chain's AquaSwapVMRouter) | | `strategyHash` | string | No\* | Strategy hash, 0x-prefixed 32 bytes (`strategy_*` actions; `close_position` accepts it instead of `strategy`) | | `status` | array | No | Lifecycle filter for `list_maker_strategies`: `["open"]`, `["closed"]`, or both. Default `["open"]` | | `limit` | number | No | Page size. Max 500 for `list_maker_strategies` and `strategy_activity` (default 100), 100 for `strategy_volume` (default 10), 1000 for `list_opened` (default 100) | | `cursor` | string | No | Opaque pagination cursor returned by a previous response | | `sort` | string | No | `"asc"` or `"desc"` (default `"desc"`) for `strategy_activity`, `strategy_volume`, and `list_opened` | | `granularity` | string | No | Volume bucket size for `strategy_volume`: `"1h"`, `"1d"` (default), `"1m"`, or `"1y"` | | `tokens` | array | No\* | `create_position`: exactly two ERC-20 legs `[{ "token", "amount" }, { "token", "amount" }]`, amounts in the token's smallest units as strings; native gas tokens are rejected. `close_position`: array of token addresses to withdraw | | `openingPrice` | string | No\* | Positive decimal, token1 per token0 in whole units. Derived from the leg amounts when the preview knows both tokens' decimals; a value you pass must agree with the legs. Required on the confirm call -- echo the value the preview returned | | `priceRange` | object | No | `create_position`: `{ "rawPriceMin", "rawPriceMax" }` raw Swap VM prices (`tokenGt / tokenLt` in 1e18 fixed point) for a concentrated position. Omit for a full-range XYC AMM | | `feeBpsIn` | number | No | `create_position`: taker fee in integer basis points, 1-500 (0.01%-5%). Default 30 | | `previewRef` | string | No\* | Opaque single-use reference returned by the `create_position` preview call; valid for about two minutes. Required together with `confirmed: true` on the confirm call | | `confirmed` | boolean | No\* | `create_position`: omit to preview, `true` (with `previewRef`) to encode. `close_position`: must be `true` -- closing is permanent | | `strategy` | string | No\* | Encoded strategy bytes returned by `create_position` (`close_position` accepts `strategy` or `strategyHash`; required by `quote` and `build_swap`) | | `tokenIn`, `tokenOut` | string | No\* | Input / output token addresses for `quote` and `build_swap` | | `amount` | string | No\* | `quote` / `build_swap`: swap amount in smallest units of `tokenIn` (`exactIn`, default) or `tokenOut` | | `exactIn` | boolean | No | `quote` / `build_swap`: `true` (default) when `amount` is the exact input | | `threshold` | string | No | `quote` / `build_swap`: minimum output (`exactIn`) or maximum input, smallest units. Default `0` | | `taker`, `deadline` | string, number | No | `build_swap`: taker wallet and optional Unix deadline in seconds | | `execute` | boolean | No | Write actions with an active WalletConnect session: defaults to sending the transaction through the wallet; `false` returns calldata only (authenticated sessions only) | | `walletSessionToken` | string | No | Durable WalletConnect token from `walletconnect connect`; pass it on anonymous writes after the MCP session rotates | \*Required for the actions named in the description -- see the table above. **Typical flow:** 1. Get your bearings with `list_opened` (system-wide) or `maker_stats` (a single maker) 2. Call `list_maker_strategies` to obtain the `chainId` / `maker` / `app` / `strategyHash` combination that identifies each strategy 3. Drill into one strategy with `strategy_overview` (state), `strategy_activity` (events), or `strategy_volume` (series) 4. Page through large result sets by passing the returned `cursor` back in the next call 5. To open a position: `create_position` without `confirmed` -> review the advisories -> `create_position` again with the same parameters, the returned `openingPrice`, the `previewRef`, and `confirmed: true`. The `previewRef` is single-use and expires after about two minutes; a stale one is refused and you preview again. Closing is permanent -- pass `confirmed: true`. #### Manage a position (maker writes) 1. Pair WalletConnect (`connect` -> approve -> `status`) and call `accept_terms`. An API key or OAuth session is optional 2. Call `create_position` without `confirmed` (two ERC-20 legs, optional `priceRange` / `feeBpsIn`) 3. Show the user the preview (opening price vs market, range, fee, advisories, suggested pairs from wallet balances). Adjust and preview again if needed 4. Call `create_position` again with the same parameters, the returned `openingPrice`, `previewRef`, and `confirmed: true`. Approve tokens and sign in the wallet 5. Monitor with `strategy_overview` / `strategy_activity` using the full `chainId` / `maker` / `app` / `strategyHash` tuple 6. To exit: `close_position` with `confirmed: true` and `strategy` or `strategyHash`. Closing cannot be undone #### Availability The full Aqua tool is available for B2C without a 1inch Business Portal account. Analytics need no authentication. For `create_position` / `close_position`, pair WalletConnect and sign the 1inch Terms of Use. Anonymous writes from restricted regions are blocked. ### search Search across all 1inch documentation, API references, and SDK guides. Returns matching results with titles, snippets, and links. | Parameter | Type | Required | Description | | :------------- | :------ | :------- | :------------------------------------ | | `query` | string | Yes | Search query | | `limit` | number | No | Results per page (1-100) | | `page` | number | No | Page number (1-based) | | `include_body` | boolean | No | Include full document body in results | ### list_examples List all available SDK code examples. Returns example names that can be used with the `get_example` tool. No parameters required. Currently available examples: - **@1inch-examples/intent-swap** -- Intent/Fusion swaps for EVM and Solana - **@1inch-examples/limit-order** -- Limit orders for EVM chains - **@1inch-examples/cross-chain-swap** -- Cross-chain swaps (Fusion+) ### get_example Retrieve the full source code of an SDK example. | Parameter | Type | Required | Description | | :-------- | :----- | :------- | :----------------------------------------- | | `name` | string | Yes | Example name from `list_examples` | | `file` | string | No | Specific file path (omit to get all files) | https://business.1inch.com/portal/assets/docs-v2/ai-integration/ai-skills.md The **1inch AI Skills** package teaches AI agents how to connect to and use the [1inch MCP server](/portal/documentation/ai-integration/mcp-server) -- the production URL, authentication, and domain-specific workflows (swaps, limit orders, Aqua positions, market data, wallet data, and more). Install once; your agent loads the relevant skill on demand. ## Install ```bash npx skills add 1inch/1inch-ai ``` No npm package required -- distribution is git-based. Source and full details: [github.com/1inch/1inch-ai](https://github.com/1inch/1inch-ai). ## Skills included The package contains one hub skill and eight domain skills. Each entry links to its full description in the [skill reference](#skill-reference). | Skill | What it teaches | MCP tool used | | ----------------------------------------------- | -------------------------------------------------------------------------- | --------------- | | [`1inch-mcp-server`](#1inch-mcp-server) | Hub -- MCP URL, auth, client setup, tool overview | all | | [`1inch-swap`](#1inch-swap) | Classic / Intent / cross-chain swaps | `swap` | | [`1inch-orderbook`](#1inch-orderbook) | Limit orders: build, sign, submit, list, cancel | `orderbook` | | [`1inch-wallet-data`](#1inch-wallet-data) | Exact `product_api` recipes for portfolio, balances, history, NFTs, tokens | `product_api` | | [`1inch-market-data`](#1inch-market-data) | Exact `product_api` recipes for spot prices, gas price, charts | `product_api` | | [`1inch-infrastructure`](#1inch-infrastructure) | Exact `product_api` recipes for Web3 RPC, tx gateway, domains | `product_api` | | [`1inch-walletconnect`](#1inch-walletconnect) | Non-custodial WalletConnect pairing (EVM & Solana) | `walletconnect` | | [`1inch-aqua`](#1inch-aqua) | Aqua analytics and maker position management | `aqua` | | [`1inch-debug`](#1inch-debug) | Org-scoped request log lookup | `debug` | Skills cost almost no context until the agent needs them. Prefer domain skills for concrete tasks (for example "what's the gas price on Base?" -> `1inch-market-data` teaches `GET /gas-price/v1.6/{chainId}`). ## Skill reference ### 1inch-mcp-server The hub skill. It carries the connection knowledge every other skill builds on: the server URL (`https://api.1inch.com/mcp/protocol`), per-client setup (Cursor, VS Code Copilot, Claude Code, Codex, Gemini CLI over HTTP; Claude Desktop over stdio via supergateway), the [authentication](/portal/documentation/ai-integration/mcp-server#authentication) options, and an overview of all nine tools. It also tells the agent which domain skill to load for each kind of task, so the detailed recipes stay out of context until needed. **Load it when:** setting up the MCP connection for the first time, resolving auth questions, or when the agent needs to decide which domain skill applies. > "Connect me to the 1inch MCP server in Cursor and check which tools are available." ### 1inch-swap Teaches quoting and executing token swaps with the [`swap` tool](/portal/documentation/ai-integration/tools-reference#swap) across all three modes: - **Classic** -- on-chain swap via the aggregation router; the user pays gas (`preferredType: "classic"`) - **Intent** -- gasless swap filled by resolvers (`preferredType: "fusion"`) - **Cross-chain** -- swaps between chains without bridges (set `dstChain`) The skill covers the quote-first flow (`quoteOnly: true` to compare modes before executing), per-mode signing guides, submitting a signed Intent or cross-chain order back via `signedOrder`, and the wallet-first flow -- pairing the user's wallet with [`1inch-walletconnect`](#1inch-walletconnect) so approvals and signatures happen in the wallet app instead of manual signing steps. **Load it when:** the user wants the best rate, a gasless swap, a cross-chain swap without bridges, or a quote comparison. > "Quote swapping 100 USDC to ETH on Base, then execute the best option." ### 1inch-orderbook Teaches limit orders with the [`orderbook` tool](/portal/documentation/ai-integration/tools-reference#orderbook) (Limit Order Protocol v4.1) -- the server builds orders server-side, so no SDK installation is needed: - **build** -- server returns EIP-712 `typedData`, `orderHash`, and `orderData`; amounts in smallest units, assets as symbol or address - **sign** -- `eth_signTypedData_v4` with the maker wallet - **create** -- submit the signed order to the orderbook - **list** -- query by maker address, by order hash, or browse all with pagination - **cancel** -- load order state and on-chain cancellation guidance It also covers the approval step (when build returns an `approval` block, broadcast the approve transaction first) and the wallet-first flow via [`1inch-walletconnect`](#1inch-walletconnect). **Load it when:** the user wants to buy or sell at a target price, manage maker orders, or check open orders. > "Build a limit order to sell 1 WETH for USDC on Ethereum, expiring in 1 hour." ### 1inch-wallet-data Teaches exact [`product_api`](/portal/documentation/ai-integration/tools-reference#product_api) call recipes for wallet-centric data, so the agent calls the right endpoint with the right version instead of guessing paths: | User ask | Product | | --------------------------- | ------------- | | Portfolio value / P&L | Portfolio | | Token balances for a wallet | Balance | | Find a token by symbol/name | Token search | | Token detail / metadata | Token details | | Transaction history | History | | NFTs owned by a wallet | NFT | When a path returns 404, the skill directs the agent to the live API index or the `search` tool rather than inventing version numbers. **Load it when:** the user asks about holdings, balances, transaction history, token lookup, or NFT inventory. > "What's my portfolio value on Ethereum for 0x1234..., and list the wallet's balances on Base." ### 1inch-market-data Teaches exact [`product_api`](/portal/documentation/ai-integration/tools-reference#product_api) call recipes for market data: | User ask | Call | | ----------------- | ------------------------------------------- | | Token price (USD) | Spot Price -- with the `currency` parameter | | Current gas | Gas Price -- `GET /gas-price/v1.6/{chainId}` | | Candles / chart | Charts | It also carries the one gotcha that trips up most integrations: Spot Price returns prices in **native token WEI** unless you pass `currency` (e.g. `USD`). **Load it when:** the user asks for a token price, USD price, gas price, or candle/chart data. > "What's the USD price of WETH on Ethereum, and the current gas price on Base?" ### 1inch-infrastructure Teaches exact [`product_api`](/portal/documentation/ai-integration/tools-reference#product_api) call recipes for chain infrastructure: | User ask | Product | | -------------------------------- | --------------------------------- | | eth_call / eth_blockNumber / RPC | Web3 RPC -- `POST /web3/{chainId}` | | Broadcast a signed raw tx | Transaction Gateway | | Resolve ENS / domain lookup | Domains | **Load it when:** the user needs JSON-RPC access through 1inch, wants to broadcast a signed transaction, or needs to resolve a domain name. > "Get the latest block number on Ethereum via 1inch Web3 RPC, then resolve vitalik.eth." ### 1inch-walletconnect Teaches non-custodial wallet pairing with the [`walletconnect` tool](/portal/documentation/ai-integration/tools-reference#walletconnect) -- the server never holds private keys; the user approves every request in their own wallet app: - **connect** -- returns a pairing QR and `wc:` URI, plus mobile deep links for [curated wallets](/portal/documentation/ai-integration/tools-reference#supported-wallets); choose `pairingTarget`: `eth`, `solana`, or `both` - **status** -- poll until the session is active (`pending` is a success state while waiting) - **accept_terms** -- wallet-signed 1inch Terms of Use (once per wallet); required before anonymous Aqua writes - **sign** / **send_transaction** -- drive signatures and transactions through the connected wallet - **disconnect** -- end the session It also carries the practical pairing knowledge: many mobile wallets reject EVM and Solana proposed together (start with one namespace), the pairing QR expires after about 10 minutes, and repeating `connect` with the same `pairingTarget` returns the same URI instead of invalidating the pending one. **Load it when:** the user wants to connect a wallet, scan a QR, or have swaps, orders, or Aqua positions signed in their wallet instead of handling keys or calldata manually. > "Connect my wallet so you can execute swaps for me -- I'll scan the QR." ### 1inch-aqua Teaches [1inch Aqua](/portal/documentation/aqua/overview/overview) analytics and maker position management with the [`aqua` tool](/portal/documentation/ai-integration/tools-reference#aqua): 1. Get your bearings with `list_opened` (system-wide feed) or `maker_stats` (TVL, fees, APY for one maker) 2. Call `list_maker_strategies` to identify each position (`chainId`, `maker`, `app`, `strategyHash`) 3. Drill into one position with `strategy_overview` (state), `strategy_activity` (on-chain events), or `strategy_volume` (time series) 4. To open a position: pair a wallet with `walletconnect`. On anonymous sessions also call `accept_terms`. Call `create_position` without `confirmed` for the preview, then `create_position` again with `confirmed: true` (echo `previewRef` and `openingPrice`). Your wallet signs. To close: `close_position` with `confirmed: true` (permanent). For raw access to Aqua endpoints not covered by the tool, the skill falls back to `product_api` with paths under `/aqua/v1.0/strategies/...`. **Load it when:** the user asks about Aqua makers, positions, TVL, or liquidity activity, or wants to preview, create, or close a position. > "Preview an Aqua USDC/WETH position on Ethereum for my wallet, then create it if the range looks right." ### 1inch-debug Teaches request log lookup with the [`debug` tool](/portal/documentation/ai-integration/tools-reference#debug) in its two modes: - **By request id** -- pass the `x-request-id` (correlation id) from any API response to trace a specific request, with an optional time window (default 24-hour lookback) - **By time window** -- pass `startTime` and `endTime` (RFC3339) to retrieve all logs for the account, optionally filtered by `logLevel` (`info`, `warn`, `error`) Logs are scoped to the authenticated organization by the gateway auth context -- other orgs cannot be queried. The queryable window depends on the log retention period of your subscription plan. **Load it when:** an API call failed or returned something unexpected and the user wants to trace what happened. > "This swap request returned a 400 -- here's the x-request-id, find the logs for it." ## Skills and MCP together Use both: skills teach your agent _how_ to call tools; MCP provides the _live connection_. - The **skill** gives setup knowledge and call recipes as reusable context. - The **[MCP server](/portal/documentation/ai-integration/mcp-server)** is the live protocol the agent calls to execute swaps, place orders, query APIs, and search docs. ### When one is not enough **A skill alone executes nothing.** Skills are static instructions -- without the MCP connection there is no tool to call, and the execution tools additionally need [authentication](/portal/documentation/ai-integration/mcp-server#authentication). Installing the package without connecting the server gives your agent knowledge it cannot act on. **The MCP connection alone covers single-step calls, not flows.** Tool schemas describe parameters, not workflows. Without the skills, agents routinely stop halfway through the two-call Intent/cross-chain signing flow, skip the approve step when placing limit orders, guess `product_api` paths (404s), misread Spot Price's native-WEI default, or restart WalletConnect pairings that were still pending. The [tools reference](/portal/documentation/ai-integration/tools-reference) notes per tool where the schema alone falls short. For **stdio-only** clients (e.g. Claude Desktop), the hub skill documents the same URLs and auth patterns, but you still need the [supergateway setup](/portal/documentation/ai-integration/supported-clients#claude-desktop) for the actual MCP connection. ## Next steps - [Supported clients](/portal/documentation/ai-integration/supported-clients) -- install the skills and configure MCP per editor - [Tools reference](/portal/documentation/ai-integration/tools-reference) -- the tools your agent gains access to https://business.1inch.com/portal/assets/docs-v2/ai-integration/supported-clients.md The 1inch [MCP server](/portal/documentation/ai-integration/mcp-server) and [AI Skills](/portal/documentation/ai-integration/ai-skills) work with a wide range of AI clients -- from coding assistants to chat products such as ChatGPT and Grok. This page covers setup instructions, authentication configuration, and troubleshooting for each supported client. ## Supported clients ### Coding assistants | Client | MCP Setup | AI Skills | | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------- | :--------------------------------- | | Claude Desktop | [Instructions](#claude-desktop) | [Install](#ai-skills-installation) | | Claude Code | [Instructions](#claude-code) | [Install](#ai-skills-installation) | | OpenAI Codex | [Instructions](#openai-codex) | [Install](#ai-skills-installation) | | Gemini CLI | [Instructions](#gemini-cli) | [Install](#ai-skills-installation) | | Cursor | [Instructions](#cursor) | [Install](#ai-skills-installation) | | Windsurf | [Instructions](#windsurf) | [Install](#ai-skills-installation) | | VS Code (Copilot) | [Instructions](#vs-code-copilot) | [Install](#ai-skills-installation) | | JetBrains IDEs | [Instructions](#jetbrains-ides) | [Install](#ai-skills-installation) | | ChatGPT | [Instructions](#chatgpt) | -- | | Grok | [Instructions](#grok) | -- | ChatGPT and Grok connect as **custom connectors** in the chat product (not IDE plugins). AI Skills are aimed at coding agents and don't apply there. The following clients are also MCP-compatible and can connect using the [generic setup](#other-mcp-clients) below: - **Continue** -- open-source AI assistant for VS Code and JetBrains - **Zed** -- AI-native code editor - **Amazon Q Developer** -- AWS AI coding assistant MCP is an open standard with rapidly growing adoption. Any MCP-compatible client can connect to the 1inch MCP Server. ## MCP Server setup ### Claude Code Claude Code supports HTTP MCP servers natively: ```bash claude mcp add \ --transport http \ --scope user \ 1inch-business \ https://api.1inch.com/mcp/protocol ``` ### OpenAI Codex Codex supports MCP servers in both the CLI and the VS Code extension: ```bash codex --mcp-server "https://api.1inch.com/mcp/protocol" ``` For the VS Code extension, add the server in the Codex MCP settings panel with the same URL. ### Gemini CLI Gemini CLI supports HTTP MCP servers natively: ```bash gemini mcp add \ --transport http \ 1inch-business \ https://api.1inch.com/mcp/protocol ``` ### Claude Desktop Claude Desktop communicates via stdio, so you use [`supergateway`](https://www.npmjs.com/package/supergateway) to bridge to the HTTP server. This requires Node.js 18+. Add the following to your configuration file: - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` ```json { "mcpServers": { "1inch-business": { "command": "/absolute/path/to/node/bin/npx", "args": [ "-y", "supergateway", "--streamableHttp", "https://api.1inch.com/mcp/protocol", "--outputTransport", "stdio" ], "env": { "PATH": "/absolute/path/to/node/bin:/usr/bin:/bin" } } } } ``` Replace `/absolute/path/to/node/bin` with the path to your Node.js installation (e.g., `/usr/local/bin` or the output of `dirname $(which node)`). :::info The `command` and `PATH` fields require absolute paths because AI clients do not inherit your shell's PATH. Using `npx -y` ensures the latest version of `supergateway` is downloaded automatically. ::: ### Cursor Create or edit `.cursor/mcp.json` in your project root: ```json { "mcpServers": { "1inch-business": { "url": "https://api.1inch.com/mcp/protocol" } } } ``` ### Windsurf Create or edit `~/.codeium/windsurf/mcp_config.json`: ```json { "mcpServers": { "1inch-business": { "serverUrl": "https://api.1inch.com/mcp/protocol" } } } ``` ### VS Code (Copilot) The server is published to the [official MCP Registry](https://modelcontextprotocol.io/registry/about) as **`com.1inch.business/mcp`**, so registry-aware VS Code builds can install **1inch MCP** from the MCP server picker without manually entering the URL. Aggregators refresh on their own schedule, so if it is not listed yet, add the server manually as below. Create or edit `.vscode/mcp.json` in your project root: ```json { "servers": { "1inch-business": { "type": "http", "url": "https://api.1inch.com/mcp/protocol" } } } ``` :::info VS Code also supports configuring MCP servers through `settings.json` under the `"mcp"` key. See the [VS Code MCP documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for additional options. ::: ### JetBrains IDEs IntelliJ IDEA, WebStorm, PyCharm, and other JetBrains IDEs support MCP through the AI Assistant (2025.1+). 1. Open **Settings** -> **Tools** -> **AI Assistant** -> **Model Context Protocol (MCP)** 2. Click **Add**, select **HTTP**, and paste the following JSON snippet: ```json { "mcpServers": { "1inch-business": { "url": "https://api.1inch.com/mcp/protocol" } } } ``` The 1inch MCP tools will be available in the AI Assistant chat within your IDE. ### ChatGPT ChatGPT on **Business, Enterprise, and Edu** plans supports **custom connectors** (also surfaced as "apps") through Developer mode. A workspace admin -- or a member with connector permissions -- can add the 1inch MCP server so the tools become available in chat. 1. Open **Settings -> Connectors** (in the ChatGPT web app). If custom connectors aren't visible, enable **Advanced -> Developer mode**, or ask your workspace admin to enable it. 2. Choose **Create** (or **Add custom connector**). 3. Enter the connector details: - **Name:** `1inch Business` - **MCP Server URL:** `https://api.1inch.com/mcp/protocol` - **Authentication:** complete the OAuth consent flow against the 1inch Business Portal, or provide an API key (see [Authentication setup](#authentication-setup)). 4. Save the connector and enable it for your chats. The 1inch tools appear in the connector/tools menu. :::info The public `search` and `walletconnect` tools work without authentication. Execution tools (`swap`, `orderbook`, `product_api`, `debug`) require OAuth or an API key. ::: ### Grok Grok (xAI) supports connecting **any public MCP server** through **Custom connectors** ("Bring Your Own MCP") on paid tiers. 1. Go to [grok.com/connectors](https://grok.com/connectors). 2. Choose **New connector -> Custom**. 3. Paste the server URL: `https://api.1inch.com/mcp/protocol`. 4. Complete the OAuth flow against the 1inch Business Portal. Grok then discovers the 1inch tools and can call them. As with other clients, `search` is available without authentication, while the execution tools require your 1inch API key or OAuth session (see [Authentication setup](#authentication-setup)). ### Other MCP clients Most MCP clients support HTTP transport and can connect directly to the server URL: - **Server URL:** `https://api.1inch.com/mcp/protocol` For clients that only support stdio transport, use [`supergateway`](https://www.npmjs.com/package/supergateway) to bridge to the HTTP server (requires Node.js 18+): ```bash npx -y supergateway \ --streamableHttp https://api.1inch.com/mcp/protocol \ --outputTransport stdio ``` Configure your client to launch this as a subprocess and communicate over stdin/stdout. ## AI Skills installation Install the 1inch AI Skills so your agent knows the server URL, tools, auth patterns, and exact `product_api` call recipes (spot prices, gas, portfolio, and more): ```bash npx skills add 1inch/1inch-ai ``` No npm package required -- distribution is git-based. For more details, see [AI Skills](/portal/documentation/ai-integration/ai-skills). For **stdio-only** clients (e.g. Claude Desktop), the skill documents the same URLs and auth patterns, but you still need the [supergateway setup](#claude-desktop) above for the actual MCP connection. ## Authentication setup The public tools (`search`, `list_examples`, `get_example`, `walletconnect`, `aqua`) work without authentication. Maker writes pair WalletConnect and sign the 1inch Terms of Use. To use the other execution tools (`swap`, `orderbook`, `product_api`, `debug`), pass your API key via the `Authorization` header. Get a key from the [1inch Business Portal](https://business.1inch.com/portal). For the conceptual overview of API key vs OAuth authentication, see [Authentication](/portal/documentation/ai-integration/mcp-server#authentication). **Claude Code:** ```bash claude mcp add \ --transport http \ --header "Authorization: Bearer YOUR_API_KEY" \ --scope user \ 1inch-business \ https://api.1inch.com/mcp/protocol ``` **Cursor** (`.cursor/mcp.json`): ```json { "mcpServers": { "1inch-business": { "url": "https://api.1inch.com/mcp/protocol", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` **VS Code Copilot** (`.vscode/mcp.json`): ```json { "servers": { "1inch-business": { "type": "http", "url": "https://api.1inch.com/mcp/protocol", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` **Claude Desktop** (supergateway): ```json { "mcpServers": { "1inch-business": { "command": "/absolute/path/to/node/bin/npx", "args": [ "-y", "supergateway", "--streamableHttp", "https://api.1inch.com/mcp/protocol", "--header", "Authorization: Bearer YOUR_API_KEY", "--outputTransport", "stdio" ], "env": { "PATH": "/absolute/path/to/node/bin:/usr/bin:/bin" } } } } ``` For other clients, consult the client's MCP documentation for how to set custom HTTP headers on the transport. ## Wallet pairing (WalletConnect) The [`walletconnect`](/portal/documentation/ai-integration/tools-reference#walletconnect) tool returns the pairing QR as an MCP **resource link** (`pairingQrResourceUri`) plus a fallback `wc:` URI. How that's surfaced depends on your client: | Client | Pairing UX | | :------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Cursor / VS Code (Copilot) / Windsurf | The assistant fetches the QR via `resources/read` in the same MCP session and renders it inline in chat (or opens it from disk). If your tool host doesn't render images, ask the assistant to print the `wc:` URI in a code block. | | Claude Desktop / Claude Code | Same as above -- the assistant reads the resource and renders the PNG. On macOS / Linux it can also open the file with `open` / `xdg-open`. | | OpenAI Codex / Gemini CLI | If the CLI doesn't render images, the assistant prints the `wc:` URI; copy/paste it into your wallet app or another QR generator. The pairing URI is short-lived (~10 min). | | Mobile-first usage (any client) | The response also includes `mobileDeeplinks` for [supported wallets](/portal/documentation/ai-integration/tools-reference#supported-wallets) -- tap the wallet's HTTPS link and it opens the wallet app directly without scanning a QR. | **Tips:** - **Don't call `connect` again to refresh** -- while pairing is still in progress, calling `connect` with the same `pairingTarget` returns the same URI. Repeated `connect` calls used to invalidate the active QR; now they don't, but only as long as the `pairingTarget` matches. - **`status: "pending"` is success** -- keep polling. The wallet pairing only fails when `status` returns `kind: "error"`. - **Solana sessions:** call `connect` with `"pairingTarget": "solana"` (Solana only) or `"both"` (EVM + Solana). Many mobile wallets reject pairings that propose `eip155` and `solana` together -- start with one namespace if you only need one. - **Sticky `Mcp-Session-Id` is optional.** WalletConnect state survives restarts and replicas because it's persisted server-side, keyed by your authenticated org/app rather than the MCP session id. ## Troubleshooting | Issue | Solution | | :-------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Connection refused | Verify the `command` path is absolute. Check that `https://api.1inch.com/mcp/protocol` is reachable from your network | | Server not appearing | Restart your AI client after modifying the configuration file. Check that the JSON is valid (no trailing commas) | | `npx` not found | Ensure Node.js 18+ is installed. Use an absolute path to `npx` in the `command` field (e.g., `/usr/local/bin/npx`) | | Too many sessions (429) | Each AI client window opens a separate session. Close windows you're not using, or wait 15 minutes for idle sessions to expire. This limit is specific to MCP sessions and does not affect your API usage | | Tools not loading | Check the AI client's MCP logs for connection errors. In Cursor, use **Developer: Open MCP Log** from the command palette | | Auth prompt not appearing | If using OAuth, ensure your MCP client supports OAuth flows. Alternatively, use an API key via the `Authorization` header (see [Authentication setup](#authentication-setup)). Some clients may require a restart after configuration changes | | Stale results | Documentation indexes are updated periodically. If recently published content is missing, try again in a few minutes | | WalletConnect QR expired | The pairing URI expires after ~10 minutes. Ask the assistant to call `walletconnect` `connect` again to get a fresh QR. If your earlier `connect` succeeded but you waited too long to scan, the wallet will show "expired" -- start over with a new `connect`. | | Wallet won't approve EVM + Solana | Many mobile wallets reject pairings that propose `eip155` and `solana` together. Reconnect with `pairingTarget: "eth"` or `"solana"` only, or use a wallet listed as supporting both ([supported wallets](/portal/documentation/ai-integration/tools-reference#supported-wallets)). | | `solana_namespace_not_available` | The active WalletConnect session was approved for EVM only. Ask the assistant to `disconnect`, then `connect` again with `"pairingTarget": "solana"` or `"both"` and approve the Solana namespace in the wallet. | | QR doesn't render in chat | If the MCP host can't render the QR resource inline, ask the assistant to print the `wc:` URI in a fenced code block, or to open the PNG locally (`open` / `xdg-open` / `start`). On mobile, use the `mobileDeeplinks` from the response. | ## Trading & Liquidity ### Swap API https://business.1inch.com/portal/assets/docs-v2/apis/swap/swap.md 1inch Swap API product offers 3 different execution modes, designed to cover different liquidity sources and cross-chain requirements: - Intent Based Swaps - Classic Swaps - Cross Chain Swaps ## Supported Chains | Supported Chains | Chain ID | Classic SWAP | Intent based SWAP | Cross-chain SWAP | | :--------------- | :------- | :----------: | :---------------: | :--------------: | | Ethereum | 1 | [x] | [x] | [x] | | Solana | 501 | [ ] | [x] | [x] | | Base | 8453 | [x] | [x] | [x] | | BNB | 56 | [x] | [x] | [x] | | zkSync | 324 | [x] | [x] | [x] | | Gnosis | 100 | [x] | [x] | [x] | | Optimism | 10 | [x] | [x] | [x] | | Cronos | 25 | [x] | [x] | [x] | | Polygon | 137 | [x] | [x] | [x] | | Monad | 143 | [x] | [x] | [x] | | Linea | 59144 | [x] | [x] | [x] | | Sonic | 146 | [x] | [x] | [x] | | Unichain | 130 | [x] | [x] | [x] | | Arbitrum | 42161 | [x] | [x] | [x] | | Avalanche | 43114 | [x] | [x] | [x] | | HyperEVM | 999 | [x] | [x] | [x] | | Arc | 5042 | [x] | [x] | [x] | | Robinhood | 4663 | [x] | [x] | [x] | ## Supported Features | Supported Features | Classic SWAP | Intent based SWAP | Cross-chain SWAP | | :----------------: | :----------: | :--------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------: | | Gasless | [ ] | [x] | [x] | | RWAs | [x] | [x] | [x] | | MEV Protection | [ ] | [x] | [x] | | SDKs | [ ] | [Intent EVM SDK](https://github.com/1inch/solana-fusion-sdk), [Intent Solana SDK](https://github.com/1inch/fusion-sdk) | [Cross-chain SDK](https://github.com/1inch/cross-chain-sdk) | | AI Integration | [x] | [x] | [x] | | Integrator Fee | [x] | [x] | [x] | **RWAs** (tokenized real-world assets) are supported across Classic, Intent, and Cross-chain swap modes, and via the [Orderbook API](/portal/documentation/apis/orderbook/introduction) (limit orders). Availability differs by tokenized asset -- issuer, network, eligibility, market hours, and liquidity can vary. See [Real-world assets support](/portal/documentation/apis/swap/real-world-assets-support). **AI Integration** enables AI agents and coding assistants to discover, understand, and execute swaps through the [1inch MCP Server](/portal/documentation/ai-integration/mcp-server). Agents connected via the Model Context Protocol can search documentation, retrieve production-ready SDK examples, and build agentic swap workflows from intent submission to resolver execution, without manual research. **Integrator Fee** is a configurable fee that allows integration partners to earn a commission on transactions routed through the 1inch swap infrastructure. The integrator fee can be specified as a parameter in API requests or configured via the corresponding SDK implementations. ## Intent Based Swaps Gasless, MEV-resistant swaps executed through a competitive resolver network. Fusion mode allows users to submit swap intents that are filled by third-party resolvers. Orders are executed using a dynamic pricing mechanism that protects users from front-running and eliminates the need to pay gas fees directly. Best for: Gas-efficient swaps, MEV protection, large or price-sensitive trades. Learn more : [Intent Swap specs](./intent-swap/introduction.md) ## Classic Swaps Classic swaps provide maximum flexibility and control over swap parameters and execution behavior. This mode is equivalent to Pro mode in trading interfaces and is intended for advanced integrators who want full freedom of configuration. Best for: Advanced trading logic, fine-grained parameter control, immediate execution. Learn more : [Classic Swap specs](./classic-swap/introduction.md) ## Cross-Chain Swaps Seamless swaps across multiple blockchains with no bridging and fast execution time. Cross-chain swaps enable users to exchange assets between different networks through a unified API, abstracting away bridging complexity and cross-chain execution logic. Best for: Multi-chain applications, asset migration, Solana - EVMs cross-ecosystem liquidity access. Learn more : [Cross-chain Swap specs](./cross-chain-swap/introduction.md) https://business.1inch.com/portal/assets/docs-v2/apis/swap/cross-chain-swap/introduction.md ## 1inch Fusion+ (intent-based atomic cross-chain swaps) The 1inch Fusion+ API is a powerful solution for secure and efficient **gasless cross-chain swaps** in DeFi that uses a creative architecture of Dutch auctions and automated recovery, all without relying on a single centralized custodian. Every Fusion+ order is a gasless cross-chain swap: the user signs an intent on the source chain, and resolvers pay all gas on both the source and destination chains. ### Key benefits - Intent-based trading: you can specify your trading intentions, which can then be fulfilled by both onchain and offchain liquidity. - Inherent MEV protection: by design, you cannot lose money to front-running or "sandwich attacks" on your orders. - Gas cost abstraction: a gasless cross-chain swap lets you move assets between networks without holding native assets (like ETH) in your wallet, as the resolvers cover all gas costs on every chain involved. - Enhanced flexibility: you can set specific conditions for your trades, with the possibility of post-trade interactions, thereby providing greater control over the execution. - Native-coin support: run a gasless cross-chain swap directly from the native coin of any supported network (e.g., ETH, BNB, MATIC, AVAX, SOL) without manual wrapping and unwrapping. - no need for bridges or messaging protocols in transferring assets across networks -- a Fusion+ gasless cross-chain swap settles atomically through linked escrows instead. - Merged sources of liquidity as there are over 15 networks available: 14 EVMs and Solana ### Why a gasless cross-chain swap? Traditional bridging forces users to hold native gas tokens on **both** the source and destination chains. A Fusion+ gasless cross-chain swap removes that requirement entirely: resolvers fund the escrows and pay gas on each side, so the user only ever signs an off-chain order. This makes a gasless cross-chain swap the simplest way to move value across networks, especially for users who do not hold the destination chain's native coin. ## Supported Chains | Supported Chains | Chain ID | MEV | Gasless | | :--------------- | :------- | :-: | :-----: | | Ethereum | 1 | [x] | [x] | | Solana | 501 | [x] | [x] | | Base | 8453 | [x] | [x] | | BNB | 56 | [x] | [x] | | zkSync | 324 | [x] | [x] | | Gnosis | 100 | [x] | [x] | | Optimism | 10 | [x] | [x] | | Cronos | 25 | [x] | [x] | | Polygon | 137 | [x] | [x] | | Monad | 143 | [x] | [x] | | Linea | 59144 | [x] | [x] | | Sonic | 146 | [x] | [x] | | Unichain | 130 | [x] | [x] | | Arbitrum | 42161 | [x] | [x] | | Avalanche | 43114 | [x] | [x] | | HyperEVM | 999 | [x] | [x] | | Arc | 5042 | [x] | [x] | | Robinhood | 4663 | [x] | [x] | :::info For a comprehensive technical overview, refer to the [1inch Fusion+ whitepaper](https://1inch.io/assets/1inch-fusion-plus.pdf). ::: ## Phases of a Fusion+ swap The process typically involves two main participants: the maker, who initiates the swap, and the resolver, who completes it; and has three phases. However, if any problems arise, there is an optional 4th _recovery phase_ that can be used as a last resort. ### Phase 1: Announcement The maker initiates the process by signing a 1inch Fusion+ order and broadcasting it to 1inch. This signals their intent to execute a cross-chain swap and sets the process in motion. The order is distributed to all resolvers, triggering a [Dutch auction](../intent-swap/introduction#dutch-auction-filling-rate). Resolvers compete by offering progressively better prices as the auction continues until a resolver locks in the order by initiating an escrow on the source chain. ### Phase 2: Deposit The winning resolver deposits the maker's assets into an escrow contract on the source chain, and then deposits the corresponding assets into an escrow on the destination chain. Both escrows are linked by a secret hash, ensuring that assets can only be unlocked once the swap is completed. A small safety deposit is also assigned to each escrow, incentivizing the resolver to successfuly complete the order. ### Phase 3: Withdrawal Once both escrows are verified by the relayer, the secret is revealed, allowing the resolver to unlock the assets on the destination chain for the maker. The resolver then uses the same secret to retrieve their newly acquired assets on the source chain, finalizing the swap. ### Finality Lock Configuration The current finality lock configuration determines the lock duration for cross-chain orders based on transaction volume. Below is a table showing the configuration values for each supported chain AS IS and TO BE states. | Chain ID | Chain Name | Low Volume (USD) | Medium Volume (USD) | High Volume (USD) | Low + Medium Volume Lock | High Volume Lock | | :------- | :--------- | :--------------- | :------------------ | :---------------- | :----------------------: | :--------------: | | 1 | Ethereum | < 100 | 100-100,000 | 100,000+ | 24s | 36s | | 10 | Optimism | < 1,000 | 1,000-100,000 | 100,000+ | 10s | 36s | | 56 | BNB | < 100 | 100-100,000 | 100,000+ | 4s | 12s | | 100 | Gnosis | < 100 | 100-100,000 | 100,000+ | 36s | 1m | | 130 | Unichain | < 100 | 100-100,000 | 100,000+ | 4s | 12s | | 137 | Polygon | < 100 | 100-100,000 | 100,000+ | 10s | 12s | | 146 | Sonic | < 100 | 100-100,000 | 100,000+ | 4s | 12s | | 324 | zkSync | < 1,000 | 1,000-100,000 | 100,000+ | 4s | 12s | | 42161 | Arbitrum | < 1,000 | 1,000-100,000 | 100,000+ | 10s | 36s | | 43114 | Avalanche | < 100 | 100-100,000 | 100,000+ | 4s | 12s | | 501 | Solana | < 100 | 100-100,000 | 100,000+ | 14s | 24s | | 8453 | Base | < 1,000 | 1,000-100,000 | 100,000+ | 4s | 12s | | 59144 | Linea | < 1,000 | 1,000-100,000 | 100,000+ | 4s | 12s | ### Optional phase: Recovery In the event of a failed swap (e.g., if a party becomes unresponsive), the protocol includes a recovery mechanism. After the timelock expires, any resolver or any participating entity can cancel the swap and return the assets to their original owners. The safety deposit in each escrow is transfered to any resolver who steps in to complete the swap during this phase. swap flow #### The partial fill feature When an order is 100% filled, a single secret is used to finalize the transaction between two parties. However, when an order is only partially filled by different resolvers, revealing the secret to the public could let others claim the remainder of the order without completing their part. To solve this, a Merkle tree of secrets is implemented for partial fills, which splits the order into equal parts and generates dedicated secrets for each portion of swap. For example, if an order is divided into four parts, the first secret is used for the first 25%, the second for 50%, and so on. If a participant fills a part of the order, the next participant uses the corresponding secret based on the current progress to continue filling the order. This ensures that each participant can only fill their portion without exposing the rest of the order. In the example image below, the 1st secret is used for the initial 0-20% fill, marking the first stage of the order. Secrets 2 and 3 are not used because the order skips directly from 20% to 80%, bypassing the ranges where these secrets would apply. The 4th secret is then used to fill the order from 20% to 80%, covering this larger portion. Finally, the 5th secret is used to complete the final 80-100% of the order, ensuring that the entire order is securely and progressively filled. partial fill ## API reference For detailed information about each endpoint, refer to the Fusion+ API [OpenAPI specs](./relayer/v1.2/submit/method/post). https://business.1inch.com/portal/assets/docs-v2/apis/swap/cross-chain-swap/quick-start/evm-to-evm.md ## Introduction This guide shows how to perform a cross-chain swap between two **EVM-compatible chains** (for example Ethereum to Arbitrum, BNB Chain to Polygon, or Base to Optimism) using 1inch Fusion+. Because both source and destination chains share the same EVM account model, the entire flow stays within a single secret-revealing pattern -- no Solana keypairs, no `SvmSrcEscrowFactory` transaction, and no `announceOrder()` step. The whole swap can be driven from one EVM signer with `sdk.placeOrder()` and the resolver fills the order on the destination chain once you reveal the secret. ## Before You Start - You must have a valid **1inch API Key**. You can get one from the [1inch Business](https://business.1inch.com/portal). - Your wallet must have at least $1 of USDC on the source chain ## Integration Pattern 1. **Initialize SDK** - Create an SDK instance with your API key and blockchain provider 2. **Check Allowance** - Ensure the source token is approved for the Aggregation Router 3. **Fetch Quote** - Get cross-chain pricing for your swap 4. **Generate Secrets** - Create cryptographic secrets for escrow locking 5. **Place Order** - Submit the signed order with hashlock and secret hashes 6. **Monitor & Submit Secrets** - Poll for escrow deployment, then reveal secrets to finalize ## Interactive Code Example Explore the minimal quick-start implementation: For a more comprehensive example with balance checks and config-driven chain selection, see `src/swap-erc20-cross-chain.ts` in the same project. https://business.1inch.com/portal/assets/docs-v2/apis/swap/cross-chain-swap/quick-start/evm-to-solana.md ## Introduction This guide shows how to perform a cross-chain swap from any **EVM-compatible chain** (Ethereum, Arbitrum, Base, BNB Chain, Polygon, Optimism, etc.) **to Solana** using 1inch Fusion+. Compared to the EVM-to-EVM flow, the source chain still uses `quote.createEvmOrder()` and `sdk.submitOrder()`, but the destination is a Solana account: you pass a `SolanaAddress` for the receiver and the destination token mint, and the destination chain id is `NetworkEnum.SOLANA` (`501`). The resolver picks up the order on Solana and the swap finalizes when you reveal the secret hashes back to the relayer -- there is no Solana transaction to sign from the source side. ## Before You Start - You must have a valid **1inch API Key**. You can get one from the [1inch Business](https://business.1inch.com/portal). - Your wallet must have at least $5 worth of USDT (or the token you're swapping) on Ethereum - You need both an EVM wallet (source) and a Solana wallet address (destination) ## Key Differences from EVM-to-EVM - Uses `SolanaAddress` for the destination token and receiver address - Order is created with `quote.createEvmOrder()` and submitted via `sdk.submitOrder()` (two-step flow) - The destination chain ID is `NetworkEnum.SOLANA` (501) ## Interactive Code Example Explore the complete EVM-to-Solana implementation: https://business.1inch.com/portal/assets/docs-v2/apis/swap/cross-chain-swap/quick-start/solana-to-evm.md ## Introduction This guide shows how to perform a cross-chain swap **from Solana to any EVM-compatible chain** (Ethereum, Arbitrum, Base, BNB Chain, Polygon, Optimism, etc.) using 1inch Fusion+. Unlike the EVM-to-EVM and EVM-to-Solana flows, the source side here requires an **on-chain Solana transaction**: you build and sign a `SvmSrcEscrowFactory` instruction with `@solana/web3.js` / `@coral-xyz/anchor` to lock tokens in escrow on Solana, then announce the order to the relayer with `sdk.announceOrder()`. The destination EVM chain is filled by a resolver, and you reveal the secret hashes back to the relayer to finalize the swap. ## Before You Start - You must have a valid **1inch API Key**. You can get one from the [1inch Business](https://business.1inch.com/portal). - Your wallet must have at least $5 worth of USDT on Solana (or the token you're swapping) - Your wallet must have sufficient SOL for transaction fees - You need both a Solana wallet (source) and an EVM wallet address (destination) ## Key Differences from EVM-to-EVM - Uses `SolanaAddress` for the source token and `EvmAddress` for the destination token - Order is created with `quote.createSolanaOrder()` and announced with `sdk.announceOrder()` - Requires building and submitting a Solana transaction via `SvmSrcEscrowFactory` to lock tokens in escrow - Uses `@solana/web3.js` and `@coral-xyz/anchor` for Solana transaction handling ## Integration Pattern 1. **Initialize SDK** -- Create an SDK instance with your API key 2. **Fetch Quote** -- Get cross-chain pricing from Solana to the destination EVM chain 3. **Generate Secrets** -- Create cryptographic secrets for escrow locking 4. **Create & Announce Order** -- Build the Solana order and announce it to the relayer 5. **Submit Solana Transaction** -- Deploy the source escrow and lock tokens on-chain 6. **Monitor & Submit Secrets** -- Poll for escrow deployment, then reveal secrets to finalize ## Interactive Code Example The EVM-to-EVM example demonstrates the core cross-chain flow. The Solana variant follows the same secret submission pattern but uses Solana-specific order creation and transaction handling: https://business.1inch.com/portal/assets/docs-v2/apis/swap/cross-chain-swap/sdk/overview.md The 1inch Cross-Chain Swap SDK (Fusion+) provides high-level functionality for performing atomic cross-chain swaps between EVM chains. Fusion+ enables swaps across different blockchains without bridges or messaging protocols. ## Key Benefits - **No bridges needed** -- Atomic swaps via escrow contracts on both chains - **MEV Protection** -- Dutch auction mechanism prevents front-running and sandwich attacks - **Gas cost abstraction** -- Resolvers pay gas fees on both source and destination chains - **Native token support** -- Swap directly from ETH, AVAX, BNB, etc. without wrapping - **15+ supported chains** -- Ethereum, Base, Arbitrum, Optimism, Polygon, BSC, Avalanche, Cronos, Monad, HyperEVM, Arc, Robinhood, and more ## How Cross-Chain Fusion+ Works 1. **Sign Order** -- The SDK generates a cross-chain order specifying source/destination tokens and chains 2. **Dutch Auction** -- Resolvers compete to fill your order at the best price 3. **Escrow Deployment** -- The winning resolver deploys escrow contracts on both chains 4. **Secret Reveal** -- You reveal cryptographic secrets to unlock escrows and finalize the swap 5. **Assets Delivered** -- Tokens arrive on the destination chain atomically ## Core Methods - **getQuote** -- Fetches cross-chain pricing, including auction parameters and secret counts - **placeOrder** -- Signs and submits a cross-chain order for ERC-20 tokens (all-in-one) - **createOrder** -- Generates an order struct for native token swaps (requires separate submission) - **submitNativeOrder** -- Submits a native token order to the relayer - **getOrderStatus** -- Checks order status (pending, executed, expired, refunded) - **getReadyToAcceptSecretFills** -- Checks if escrows are deployed and ready for secret submission - **submitSecret** -- Reveals a secret to unlock escrows and finalize the swap - **WebSocketApi** -- Real-time order status updates as an alternative to polling ## Integration Pattern A typical ERC-20 cross-chain swap follows this flow: 1. **Initialize SDK** -- Create an SDK instance with your API key and blockchain provider 2. **Check Allowance** -- Ensure the source token is approved for the Aggregation Router 3. **Fetch Quote** -- Get cross-chain pricing for your swap 4. **Generate Secrets** -- Create cryptographic secrets for escrow locking 5. **Place Order** -- Submit the signed order with hashlock and secret hashes 6. **Monitor & Submit Secrets** -- Poll for escrow deployment, then reveal secrets to finalize ## Interactive Code Example Explore the complete implementation -- this example demonstrates a cross-chain ERC-20 swap: https://business.1inch.com/portal/assets/docs-v2/apis/swap/cross-chain-swap/sdk/when-and-how-to-submit-secrets.md After placing a cross-chain order, the maker must monitor for escrow deployment and reveal cryptographic secrets to finalize the swap. This page explains the secret submission lifecycle. ## Secret Submission Flow 1. **Wait for escrow creation** -- After your order is matched, escrow contracts are deployed on both source and destination chains 2. **Check readiness** -- Use `getReadyToAcceptSecretFills(orderHash)` to verify both escrows are in place and finality locks have passed 3. **Submit secrets** -- Use `submitSecret(orderHash, secret)` to reveal each secret and unlock the escrows 4. **Verify completion** -- Use `getOrderStatus(orderHash)` to confirm the order is marked as `executed` ## Key Methods - **getReadyToAcceptSecretFills(orderHash)** -- Returns which fill indices are ready for secret submission. Poll this repeatedly after order placement. - **submitSecret(orderHash, secret)** -- Reveals a secret to unlock escrows. For multi-fill orders, submit secrets one at a time as each fill becomes ready. - **getOrderStatus(orderHash)** -- Returns the current order status (`pending`, `executed`, `expired`, `refunded`). - **getPublishedSecrets(orderHash)** -- Checks which secrets have already been revealed by other resolvers. ## Partial Fill Handling For large orders split across multiple resolvers, each fill gets its own secret. The `fills` array from `getReadyToAcceptSecretFills` tells you which indices need secrets. Track submitted indices to avoid duplicates. ## Interactive Code Example See the complete secret submission loop in context: https://business.1inch.com/portal/assets/docs-v2/apis/swap/cross-chain-swap/sdk/how-to-swap-native-tokens-cross-chain.md ## Introduction With Cross-Chain Swap, you can perform a gasless cross-chain swap from a chain's native token (like ETH on Base) to an asset on another chain. ## How does native cross-chain exchange work? For cross-chain orders involving a native token, a dedicated escrow contract is deployed on the source chain. This escrow serves as a temporary vault where the following sequence takes place: 1. **Deposit**: the user deposits the source chain's native coin into the escrow. Inside, the token is automatically wrapped into its canonical ERC-20 form (e.g., ETH -> WETH). 1. **Cross-chain execution**: when a resolver fills the order, it initiates a cross-chain message that pulls the wrapped asset from the source escrow, performs the swap via Cross-Chain Swap infrastructure, bridges the taker asset to the destination chain, and delivers it directly to the user's address on that chain. 1. **Refund**: if no resolver fills the order within the allowed timeframe, the escrow can be triggered to return the user's original native token on the source chain. This design ensures that users interact only with their native assets, while all wrapping, bridging, escrow management, and swap execution are seamlessly handled by Cross-Chain Swap infrastructure and its resolvers. In the section below, you'll find an interactive example of how to swap a native coin cross-chain. ## Interactive Example Explore the complete native token cross-chain swap implementation: ## Key Differences from ERC-20 Cross-Chain Swaps | Step | ERC-20 | Native Token | | -------------------- | ----------------------------- | ----------------------------------------------- | | Approval | Required (Aggregation Router) | Not needed | | Order creation | `sdk.placeOrder()` | `sdk.createOrder()` + `sdk.submitNativeOrder()` | | Escrow deployment | Handled by resolver | Must deploy via `NativeOrdersFactory` | | On-chain transaction | Not needed | Required to fund escrow | | Secret submission | Same for both | Same for both | https://business.1inch.com/portal/assets/docs-v2/apis/swap/cross-chain-swap/sdk/auction-calculator.md The `AuctionCalculator` class computes taker amounts and auction rate bumps from a limit order struct. This is useful for understanding how the Dutch auction mechanism determines pricing over time. ## Key Methods - **AuctionCalculator.fromLimitOrderV3Struct(order)** -- Creates a calculator instance from a `LimitOrderV3Struct` - **calcRateBump(blockTimestamp)** -- Returns the auction rate bump at a given timestamp (in basis points) - **calcAuctionTakingAmount(takingAmount, rateBump)** -- Returns the adjusted taker amount after applying the rate bump ## LimitOrderV3Struct Fields | Name | Type | Description | | ------------- | ------ | ----------------------------------------------------------------------- | | salt | string | Unique value to differentiate orders with identical parameters | | makerAsset | string | Token address the maker is selling | | takerAsset | string | Token address the maker is buying | | maker | string | Address of the order creator | | receiver | string | Address to receive taker asset (zero address = maker address) | | allowedSender | string | Restricts who can fill (zero address = anyone) | | makingAmount | string | Amount of maker asset | | takingAmount | string | Amount of taker asset | | offsets | string | Byte offsets for interaction parsing | | interactions | string | Encoded Fusion-specific data (auction points, resolvers, timing, flags) | https://business.1inch.com/portal/assets/docs-v2/apis/swap/intent-swap/introduction.md ## 1inch intent-based swap (Fusion mode) 1inch Intent Swap API (Fusion mode) offers users a way to execute a **gasless swap** without spending gas or risking being front-run. To the user, a gasless swap in Fusion mode looks like an ordinary swap, but technically it is a limit order with a variable exchange rate filled by a third party called a resolver. An order's exchange rate decreases from the desired rate to the minimal return amount (Dutch auction) until it becomes profitable for resolvers to fill the order. Multiple resolvers compete for the order to ensure it is filled before the rate falls to the minimal return amount. Here are some examples of opportunities for resolvers to gain profit: - The Dutch auction constantly decreases the order rate. - Gas economy when filling matching orders. - Gas economy due to batch filling. For integrators and resolvers there is a [Fusion SDK](https://github.com/1inch/fusion-sdk) available to help with the integration. ## Why a gasless swap? A gasless swap is the core benefit of Fusion mode: the resolver pays the on-chain gas, so the user never needs to hold native tokens to trade. This makes a gasless swap ideal for onboarding new users, for stablecoin-only wallets, and for any flow where managing gas balances adds friction. Key properties of a Fusion gasless swap: - **No native balance required** -- every gasless swap settles without the user paying gas directly; the resolver covers the cost and bakes it into the auction price. - **MEV protection by design** -- a gasless swap is filled through a competitive Dutch auction, so the user is protected from front-running and sandwich attacks. - **Better execution** -- because resolvers compete to fill each gasless swap, users receive prices at or above their specified minimum return. In short, a gasless swap lets integrators offer a seamless, gas-free trading experience while resolvers profit from filling the underlying intent order. ## API reference For detailed information about each endpoint, refer to the [OPEN API specs section](./relayer/v2.0/1/order/submit/method/post). ## Supported Chains | Supported Chains | Chain ID | RWA Support | MEV | Gasless Swap | | :--------------- | :------- | :-----------------------------------: | :-: | :----------: | | Ethereum | 1 | Ondo, xStocks, Tenbin, Maple, Reserve | [x] | [x] | | Solana | 501 | [ ] | [x] | [x] | | Base | 8453 | Reserve, St0x | [x] | [x] | | BNB | 56 | Ondo, xStocks, Reserve | [x] | [x] | | zkSync | 324 | [ ] | [x] | [x] | | Gnosis | 100 | [ ] | [x] | [x] | | Optimism | 10 | [ ] | [x] | [x] | | Cronos | 25 | [ ] | [x] | [x] | | Polygon | 137 | [ ] | [x] | [x] | | Monad | 143 | [ ] | [x] | [x] | | Linea | 59144 | [ ] | [x] | [x] | | Sonic | 146 | [ ] | [x] | [x] | | Unichain | 130 | [ ] | [x] | [x] | | Arbitrum | 42161 | [ ] | [x] | [x] | | Avalanche | 43114 | [ ] | [x] | [x] | | HyperEVM | 999 | [ ] | [x] | [x] | | Arc | 5042 | [ ] | [x] | [x] | | Robinhood | 4663 | Bitstamp | [x] | [x] | ## RWAs Support The 1inch Intent Swap API supports tokenized real-world assets (RWAs) from [Ondo](https://ondo.finance), [xStocks](https://xstocks.fi), Bitstamp, Maple, Reserve, Tenbin, and St0x. RWAs are also supported across Classic Swap, Cross-chain Swap, and the Orderbook API. Availability differs by tokenized asset -- trading conditions, including market hours and eligible networks, may differ depending on the respective issuer. [Find out more](/portal/documentation/apis/swap/real-world-assets-support). ## Dutch auction filling rate Each order starts with an auction timestamp, calculated as the order's signature timestamp plus a waiting period to account for different network speeds. Before the auction begins, an order can be filled at the maximum rate. After the auction starts, the filling rate gradually decreases. The filling rate depends on several factors, including swap volume, gas costs, and chosen preset (e.g., fast, fair, auction). To minimize price impact, the source token volume is divided into parts, creating multiple price points. This benefits users with better rates and allows resolvers to profit. The partial fill functionality optimizes efficiency by allowing large swaps to be executed at better rates. Different resolvers can fill different parts of the order, making it profitable at various points. The price curve is also dynamic, and adapts to gas market conditions to manage gas price volatility. This reduces the chance of order expiration and speeds up execution by 75%. If gas prices increase, the resolver might delay fulfillment, but if they decrease, the resolver benefits from lower costs. The adjusted price curve ensures users receive more tokens when base fees decline and corrects execution costs when they rise. We recommend resolvers split orders into 6-10 parts and check if at least one part can be filled. Below is a real-life example: auction curve https://business.1inch.com/portal/assets/docs-v2/apis/swap/intent-swap/quick-start/quick-start-ethereum.md ## Introduction This guide shows how to use Intent Swap (Fusion mode) to perform a gasless swap from native ETH to USDC on Ethereum. You will use the 1inch Fusion SDK to fetch quotes, create orders, and submit them to the relayer network. Native token swaps require no approval--just deploy an escrow contract to lock your ETH. ## Before You Start - You must have a valid **1inch API Key**. You can get one from the [1inch Business](https://business.1inch.com/portal). - Your wallet must have: - ETH on Ethereum ## Install Dependencies Install the Fusion SDK and ethers: ```bash npm install @1inch/fusion-sdk ethers dotenv ``` ## Set Up the Provider Create an ethers provider using the 1inch Web3 node and your API key. The provider is used for RPC calls and wallet signing. ```typescript import { ethers } from "ethers"; const rpcUrl = "https://api.1inch.com/web3/1"; const fetchReq = new ethers.FetchRequest(rpcUrl); fetchReq.setHeader("Authorization", `Bearer ${API_KEY}`); const provider = new ethers.JsonRpcProvider(fetchReq, 1, { staticNetwork: true }); const wallet = new ethers.Wallet(PRIVATE_KEY, provider); ``` ## Initialize the Fusion SDK Create a Fusion SDK instance with your API key and a blockchain provider connector. The connector bridges ethers to the SDK's expected interface. ```typescript import { FusionSDK, NetworkEnum, PrivateKeyProviderConnector } from "@1inch/fusion-sdk"; const sdk = new FusionSDK({ url: "https://api.1inch.com/fusion", network: NetworkEnum.ETHEREUM, blockchainProvider: new PrivateKeyProviderConnector(PRIVATE_KEY, { eth: { call: (tx) => provider.call(tx) }, extend() {} }), authKey: API_KEY }); ``` ## Get a Swap Quote Call `getQuote` with the native token sentinel address, destination token, amount, and wallet address. For native ETH, use the sentinel address `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE`. ```typescript const NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; const params = { fromTokenAddress: NATIVE, toTokenAddress: USDC, amount: "1000000000000000", // 0.001 ETH (18 decimals) walletAddress: wallet.address }; const quote = await sdk.getQuote(params); ``` ## Create and Submit the Order Create the order with `createOrder`, then submit it to the relayer with `submitNativeOrder` (not `submitOrder`). Native token orders use a different submission flow. ```typescript import { Address } from "@1inch/fusion-sdk"; const order = await sdk.createOrder(params); const makerAddress = new Address(wallet.address); const result = await sdk.submitNativeOrder(order.order, makerAddress, order.quoteId); console.log("Order submitted:", result.orderHash); ``` ## Deploy the Escrow Contract Native token swaps require an on-chain escrow to hold your ETH. Use `NativeOrdersFactory` to build the deployment transaction, then send it with your wallet. The escrow wraps ETH to WETH and locks it until the order is filled. ```typescript import { NativeOrdersFactory, NetworkEnum } from "@1inch/fusion-sdk"; const factory = NativeOrdersFactory.default(NetworkEnum.ETHEREUM); const call = factory.create(makerAddress, result.order); const tx = await wallet.sendTransaction({ to: call.to.toString(), data: call.data, value: call.value }); await tx.wait(); ``` ## Full Code Example Explore the complete quick-start implementation (~85 lines) in StackBlitz: For a more comprehensive example with order tracking and error handling, see `src/evm/swap-native.ts` in the same project. https://business.1inch.com/portal/assets/docs-v2/apis/swap/intent-swap/quick-start/quick-start-solana.md ## Introduction This guide shows how to use Intent Swap (Fusion mode) to perform a gasless swap from native SOL to JUP on Solana. You will use the 1inch Solana Fusion SDK to create orders and build escrow transactions. Native SOL swaps require no approval--the escrow instruction locks your SOL on-chain until the order is filled. ## Before You Start - You must have a valid **1inch API Key**. You can get one from the [1inch Business](https://business.1inch.com/portal/applications). - Your wallet must have: - SOL on Solana (swap amount plus ~0.01 SOL for transaction fees) ## Install Dependencies Install the Solana Fusion SDK, Solana web3.js, and supporting libraries: ```bash npm install @1inch/solana-fusion-sdk @solana/web3.js axios bs58 dotenv ``` ## Set Up the Connection Create a Solana connection using the 1inch Web3 node. Pass your API key in the `httpHeaders` for authenticated RPC access. ```typescript import { Connection } from "@solana/web3.js"; const rpcUrl = "https://api.1inch.com/web3/501"; const connection = new Connection(rpcUrl, { commitment: "confirmed", httpHeaders: { Authorization: `Bearer ${API_KEY}` }, wsEndpoint: `wss://api.1inch.com/web3/501?apiKey=${API_KEY}` }); ``` ## Create the Wallet Decode your private key (base58 or base64) and create a Keypair. The SDK supports both 32-byte seeds and 64-byte full keypairs. ```typescript import { Keypair } from "@solana/web3.js"; import bs58 from "bs58"; let secretKey: Uint8Array; try { secretKey = bs58.decode(PRIVATE_KEY); } catch { secretKey = Buffer.from(PRIVATE_KEY, "base64"); } let wallet: Keypair; if (secretKey.length === 32) { wallet = Keypair.fromSeed(secretKey); } else if (secretKey.length === 64) { wallet = Keypair.fromSecretKey(secretKey); } else { throw new Error(`Invalid private key length: ${secretKey.length} bytes (expected 32 or 64)`); } ``` ## Initialize the Fusion SDK Create the Solana Fusion SDK with an HTTP provider. The provider must implement `get` and `post` methods for API calls. ```typescript import { Sdk } from "@1inch/solana-fusion-sdk"; import axios from "axios"; const sdk = new Sdk( { async get(url: string, headers: Record): Promise { return (await axios.get(url, { headers })).data; }, async post(url: string, data: unknown, headers: Record): Promise { return (await axios.post(url, data, { headers })).data; } }, { baseUrl: "https://api.1inch.com/fusion", authKey: API_KEY, version: "v2.0" } ); ``` ## Create the Order Call `createOrder` with the source token (`Address.NATIVE` for SOL), destination token address, amount in lamports, and maker address. The SDK returns an order object used to build the escrow transaction. ```typescript import { Address } from "@1inch/solana-fusion-sdk"; import { LAMPORTS_PER_SOL } from "@solana/web3.js"; const JUP = "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"; const amount = BigInt(0.01 * LAMPORTS_PER_SOL); // 0.01 SOL const makerAddress = Address.fromPublicKey(wallet.publicKey); const order = await sdk.createOrder(Address.NATIVE, new Address(JUP), amount, makerAddress); ``` ## Build and Send the Escrow Transaction Use `FusionSwapContract` to create the escrow instruction, add it to a Solana transaction, sign with your wallet, and broadcast. The escrow locks your SOL until a resolver fills the order. ```typescript import { FusionSwapContract } from "@1inch/solana-fusion-sdk"; import { Transaction, PublicKey } from "@solana/web3.js"; const contract = FusionSwapContract.default(); const instruction = contract.create(order, { maker: makerAddress, srcTokenProgram: Address.TOKEN_PROGRAM_ID }); const tx = new Transaction().add({ programId: new PublicKey(instruction.programId.toBuffer()), keys: instruction.accounts.map((a) => ({ pubkey: new PublicKey(a.pubkey.toBuffer()), isSigner: a.isSigner, isWritable: a.isWritable })), data: Buffer.from(instruction.data) }); const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(); tx.recentBlockhash = blockhash; tx.lastValidBlockHeight = lastValidBlockHeight; tx.feePayer = wallet.publicKey; tx.sign(wallet); // skipPreflight: true -- 1inch Web3 can return "Invalid Request" on simulation // while the transaction still broadcasts and confirms on-chain const signature = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true, maxRetries: 3 }); const confirmation = await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, "confirmed"); if (confirmation.value.err) { throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`); } ``` ## Full Code Example Explore the complete quick-start implementation (~120 lines) in StackBlitz: For a more comprehensive example with order tracking and error handling, see `src/solana/swap-sol-to-jup.ts` in the same project. https://business.1inch.com/portal/assets/docs-v2/apis/swap/intent-swap/sdk/solana/overview.md The 1inch Solana Fusion SDK enables token swaps on Solana with ultra-fast transactions and minimal fees. Unlike EVM chains where orders are signed off-chain, Solana orders are placed directly on-chain through escrow contracts. ## Key Benefits - **Ultra-Fast Transactions** - Solana's ~400ms block times mean near-instant order confirmation - **Minimal Fees** - Transaction costs are typically under $0.01, with no additional 1inch fees - **Deep Liquidity** - Access swaps across over 1 million Solana-native tokens - **MEV Protection** - Built-in protection against front-running and sandwich attacks ## How Solana Fusion Works The swap process on Solana differs from EVM chains: 1. **Create Order** - The SDK generates a FusionOrder with optimal auction parameters 2. **Build Escrow Transaction** - A transaction is prepared to create an on-chain escrow holding your tokens 3. **Sign and Broadcast** - You sign and submit the transaction (paying ~$0.001 in fees) 4. **Resolver Fills Order** - Resolvers compete to fill your order at the best price 5. **Receive Tokens** - The destination token is delivered directly to your wallet ## Core Methods - **getQuote** - Fetches current pricing and auction parameters. Use this to display expected output amounts before users commit to a trade. - **createOrder** - Generates a FusionOrder struct with source/destination tokens, amounts, and Dutch auction parameters. This data is used to build the on-chain escrow transaction. - **getOrderStatus** - Checks whether an order is still active or has been filled/cancelled. Use this for polling-based status tracking. - **WebSocketApi** - Real-time order status updates as an alternative to polling. Subscribe to order events for instant notifications when your order is filled or cancelled. ## Integration Pattern A typical integration follows this flow: 1. **Initialize SDK** - Create an SDK instance with your API key 2. **Fetch Quote** - Show users the expected swap rate before they confirm 3. **Create Order** - Generate the order structure when user approves 4. **Build Transaction** - Use FusionSwapContract to create the escrow instruction 5. **Submit & Track** - Broadcast the transaction and monitor for completion ## Interactive Code Example Explore the complete implementation -- this example demonstrates swapping SOL to JUP: https://business.1inch.com/portal/assets/docs-v2/apis/swap/intent-swap/sdk/solana/filling-and-canceling-solana-fusion-order.md Fusion orders on Solana have a defined lifecycle that includes filling by resolvers and cancellation options. Understanding this lifecycle helps you build robust applications that handle all possible order outcomes. ## Order Lifecycle After an escrow is created on-chain, the order enters the active state where: 1. **Resolvers Monitor** - Professional market makers watch for profitable orders to fill 2. **Dutch Auction** - The offered price decreases over time, incentivizing quick fills 3. **Fill or Expire** - Either a resolver fills the order, or it expires after the deadline ## Cancellation Options Orders can be cancelled in two scenarios: - **Maker Cancellation** - As the order creator (maker), you can cancel your own order at any time before it's filled. This returns your tokens from the escrow back to your wallet. - **Resolver Cancellation** - After an order expires, resolvers can trigger cancellation to clean up the escrow. For this service, resolvers earn a small cancellation premium (paid in lamports) as defined in the order parameters. ## Interactive Code Example Explore the complete implementation -- this example demonstrates how to cancel an existing order: https://business.1inch.com/portal/assets/docs-v2/apis/swap/intent-swap/sdk/solana/web-socket-api.md The WebSocket API provides real-time order tracking on Solana. For applications that need instant feedback on order status, WebSocket eliminates the latency and overhead of polling. ## Why Use WebSocket on Solana? Solana's fast block times (~400ms) make real-time tracking particularly valuable: - **Instant Notifications** - Know immediately when your order is filled - **Transaction Details** - Each event includes the Solana transaction signature for verification - **Efficient Monitoring** - Track multiple orders with a single connection ## Order Events The Solana WebSocket API provides three event types: ### create Emitted when your escrow transaction is confirmed on-chain. Includes the transaction signature and slot number. ### fill Emitted when a resolver fills your order. Contains the resolver's address and the amounts exchanged. ### cancel Emitted when an order is cancelled, either by the maker or by a resolver cleaning up an expired order. ## Interactive Code Example Explore the complete implementation -- this example demonstrates creating an order and tracking it via WebSocket events: https://business.1inch.com/portal/assets/docs-v2/apis/swap/intent-swap/sdk/evms/overview.md The 1inch Intent Swap SDK provides high-level functionality to work with Fusion mode on EVM-compatible chains. Fusion enables gasless swaps where resolvers compete to fill your order at the best price through a Dutch auction mechanism. ## Key Benefits - **Gasless Swaps** - Resolvers pay gas fees on your behalf, so you never need ETH for transaction costs - **MEV Protection** - Dutch auction mechanism protects against front-running and sandwich attacks - **Multi-Chain Support** - Works across Ethereum, Base, Polygon, Arbitrum, Optimism, Avalanche, BNB Chain, and more - **No Wrapping Required** - Swap native tokens (ETH, MATIC, etc.) directly without wrapping to WETH first ## How EVM Fusion Works 1. **Create Order** - The SDK generates a FusionOrder with optimal Dutch auction parameters 2. **Sign Order** - You sign the order off-chain using EIP-712 typed data (no gas required) 3. **Submit to Relayer** - The signed order is submitted to the 1inch resolver network 4. **Resolver Fills** - Resolvers compete to fill your order, driving you toward the best execution price 5. **Receive Tokens** - The destination token is delivered directly to your wallet ## Core Methods - **getQuote** - Fetches current pricing and auction parameters. Use this to display expected output amounts before users commit to a trade. - **createOrder** - Generates a FusionOrder struct with source/destination tokens, amounts, and Dutch auction parameters. Returns an order ready for signing. - **submitOrder** - Submits a signed order to the 1inch relayer network. The relayer broadcasts your order to resolvers who compete to fill it. - **getOrderStatus** - Checks whether an order is pending, filled, or cancelled. Use this for polling-based status tracking. - **WebSocketApi** - Real-time order status updates as an alternative to polling. Subscribe to order events for instant notifications when your order is filled or cancelled. ## Integration Pattern A typical integration follows this flow: 1. **Initialize SDK** - Create an SDK instance with your API key and wallet/signer 2. **Fetch Quote** - Show users the expected swap rate before they confirm 3. **Create Order** - Generate the order structure when user approves 4. **Submit Order** - Send the signed order to the relayer network 5. **Track Status** - Monitor completion via polling (`getOrderStatus`) or WebSocket subscription ## Interactive Code Example Explore the complete implementation -- this example demonstrates swapping ERC-20 tokens: https://business.1inch.com/portal/assets/docs-v2/apis/swap/intent-swap/sdk/evms/swapping-a-native-token-to-ERC-20.md With Intent Swap, you can swap a chain's native token (like ETH on Ethereum) to any ERC-20 token. This is ideal for users who hold only native tokens and want to acquire other assets without first wrapping them manually. ## How It Works Native token swaps require an additional on-chain step compared to ERC-20 swaps: 1. **Create Order** - The SDK generates a FusionOrder with optimal auction parameters 2. **Submit to Relayer** - Use `submitNativeOrder()` to register the order with the resolver network 3. **Deploy Escrow** - Use `NativeOrdersFactory` to deploy an escrow contract that holds your native token 4. **Automatic Wrapping** - The escrow wraps your native token (e.g., ETH -> WETH) and locks it 5. **Order Execution** - Resolvers compete to fill your order at the best price 6. **Direct Delivery** - The destination token is transferred directly to your wallet If no resolver fills the order before expiry, the escrow returns your native token. ## Key Differences from ERC-20 Swaps - **No approval needed** - Native tokens don't require ERC-20 approval - **Escrow deployment required** - You must send a transaction to deploy the escrow contract - **Small gas cost** - The escrow deployment costs a small amount of gas (~0.001 ETH) ## Interactive Code Example Explore the complete implementation -- this example demonstrates swapping native ETH for USDC on Ethereum mainnet: https://business.1inch.com/portal/assets/docs-v2/apis/swap/intent-swap/sdk/evms/swapping-with-permit2.md Permit2 is Uniswap's universal token approval system that enables gasless approvals after a one-time setup. Instead of approving each protocol separately, you approve Permit2 once and then use off-chain signatures for all subsequent token transfers. ## Why Use Permit2? - **Cost Savings** - After the initial approval, no gas is required for future approvals - **Better Security** - Permits are time-limited and can specify exact amounts - **Improved UX** - Users sign a message instead of submitting a transaction ## How It Works 1. **One-Time Setup** - Approve your tokens to the Permit2 contract (can be max amount) 2. **Get Quote** - Request a quote with `isPermit2: true` flag 3. **Create Order** - The SDK generates a Permit2 signature bundled with your order 4. **Submit** - The relayer verifies the signature and executes the swap ## Interactive Code Example Explore the complete implementation -- this example demonstrates swapping USDC to WETH using Permit2: https://business.1inch.com/portal/assets/docs-v2/apis/swap/intent-swap/sdk/evms/websocket-api.md The WebSocket API enables real-time order tracking for production applications. Instead of repeatedly polling the API for status updates, your application receives instant notifications when order events occur. ## Why Use WebSocket? For applications that need responsive user interfaces or automated trading systems, WebSocket provides significant advantages: - **Instant Updates** - Receive order events within milliseconds instead of waiting for poll intervals - **Reduced API Load** - A single persistent connection replaces hundreds of status requests - **Complete Event History** - Track all state changes including partial fills, not just final status ## Order Events The WebSocket API notifies you of these order lifecycle events: - **order_created** - Your order has been submitted and is visible to resolvers - **order_filled** - The order was completely filled by a resolver - **order_filled_partially** - Part of your order was filled (for large orders) - **order_invalid** - The order became invalid due to cancellation, expiry, or insufficient balance ## Interactive Code Example Explore the complete implementation -- this example demonstrates creating a swap order and tracking its progress via WebSocket events: https://business.1inch.com/portal/assets/docs-v2/apis/swap/classic-swap/introduction.md The 1inch classic swap API (Pathfinder v6.1) is a cutting-edge discovery and routing algorithm that offers asset exchanges at the best rates on the market. Pathfinder finds the most efficient paths for a token swap and is able to split between different protocols and even different market depths within one protocol in the shortest possible time. ## Supported Chains | Supported Chains | Chain ID | | :--------------- | :------- | | Ethereum | 1 | | Solana | 501 | | Base | 8453 | | BNB | 56 | | zkSync | 324 | | Gnosis | 100 | | Optimism | 10 | | Cronos | 25 | | Polygon | 137 | | Monad | 143 | | Linea | 59144 | | Sonic | 146 | | Unichain | 130 | | Arbitrum | 42161 | | Avalanche | 43114 | | HyperEVM | 999 | | Arc | 5042 | | Robinhood | 4663 | ## Eficiency The latest version (V 6.1) of Pathfinder algorithm pushes DeFi trading forward by unlocking smarter, more efficient swaps, and bringing a number of concrete advantages to 1inch Swap users: - Maximized returns: multiple swap paths are combined to secure the best rates, delivering more value from every trade. - Superior liquidity efficiency: by splitting volumes into finer chunks and blending paths, Pathfinder taps into pools more effectively, skimming the cream from concentrated liquidity for optimal exchanges. - Unrivaled access: the new Pathfinder facilitates a deeper dive into diverse liquidity pools, distributing volume to capture better rates where it matters most. - Gas efficiency: by merging paths to achieve better gas cost efficiency, every swap becomes more cost-effective. ## Before V 6.1 old routing ## After V 6.1 new pathfinder routing ## API reference For detailed information about each endpoint, refer to the classic swap API [OpenAPI specs](./methods/v6.1/1/quote/method/get). ## Quickstart In order to quickly understand how to make an exchange using the protocol, we suggest you use the [Quickstart guide](./quick-start.md). :::info For workloads that require higher RPS, expanded quotas, or sustained performance, explore the available paid plans on the [**pricing page**](/pricing). For organizations with significant trading volumes or advanced requirements, [**complete this application**](/get-enterprise) to explore tailored Enterprise options. ::: https://business.1inch.com/portal/assets/docs-v2/apis/swap/classic-swap/quick-start.md Let's go over an example of how to perform a token swap using the **1inch Classic Swap API**. --- ## Before You Start - You must have a valid **1inch API Key**. You can get one from the [1inch Business](https://business.1inch.com/portal). - Your wallet must have: - USDC (or the token you're swapping) on Base - ETH on Base to pay gas fees ## Typescript Program 1. Loads configuration variables (private key, wallet address, RPC URL, and 1inch Business Portal API key) from the environment or a local `.env` file 2. Connects to the Base chain using viem 3. Checks the allowance of the token being swapped and increases the approval if necessary 4. Uses the `/swap` endpoint to construct the swap transaction 5. It signs and broadcasts the transaction ### Dependencies - node v18.17.1 - npm v10.9.2 ```bash npm install dotenv ethers ``` ### Example ```typescript import dotenv from "dotenv"; import { Wallet, JsonRpcProvider, Contract } from "ethers"; dotenv.config(); const requiredEnvVars = ["PRIVATE_KEY", "API_KEY", "RPC_URL"]; for (const key of requiredEnvVars) { if (!process.env[key]) { console.error(`Missing required environment variable: ${key}`); process.exit(1); } } const AGGREGATION_ROUTER_V6 = "0x111111125421ca6dc452d289314280a0f8842a65"; const usdcBase = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; const wethBase = "0x4200000000000000000000000000000000000006"; const BASE_CHAIN_ID = 8453; const baseUrl = `https://api.1inch.com/swap/v6.1/${BASE_CHAIN_ID}`; const config = { aggregationRouter: AGGREGATION_ROUTER_V6, privateKey: process.env.PRIVATE_KEY!, apiKey: process.env.API_KEY!, rpcUrl: process.env.RPC_URL!, srcTokenAddress: usdcBase, dstTokenAddress: wethBase, amountToSwap: "100000", slippage: 1 // 1% }; const erc20ABI = [ { constant: true, inputs: [ { name: "owner", type: "address" }, { name: "spender", type: "address" } ], name: "allowance", outputs: [{ name: "", type: "uint256" }], payable: false, stateMutability: "view", type: "function" }, { constant: false, inputs: [ { name: "spender", type: "address" }, { name: "amount", type: "uint256" } ], name: "approve", outputs: [{ name: "", type: "bool" }], payable: false, stateMutability: "nonpayable", type: "function" } ]; type TransactionPayload = { to: string; data: string; value: bigint; }; type TxResponse = { tx: TransactionPayload; }; const provider = new JsonRpcProvider(config.rpcUrl); const wallet = new Wallet(config.privateKey, provider); function buildQueryURL(path: string, params: Record): string { const url = new URL(baseUrl + path); url.search = new URLSearchParams(params).toString(); return url.toString(); } async function call1inchAPI(endpointPath: string, queryParams: Record): Promise { const url = buildQueryURL(endpointPath, queryParams); const response = await fetch(url, { method: "GET", headers: { Accept: "application/json", Authorization: `Bearer ${config.apiKey}` } }); if (!response.ok) { const body = await response.text(); throw new Error(`1inch API returned status ${response.status}: ${body}`); } return (await response.json()) as T; } async function signAndSendTransaction(tx: TransactionPayload): Promise { try { const txResponse = await wallet.sendTransaction({ to: tx.to, data: tx.data, value: tx.value }); return txResponse.hash; } catch (err) { console.error("Transaction signing or broadcasting failed"); console.error("Transaction data:", tx); throw err; } } async function checkAllowance(tokenAddress: string, spenderAddress: string): Promise { console.log("Checking token allowance..."); const tokenContract = new Contract(tokenAddress, erc20ABI, provider); const allowance = await tokenContract.allowance(wallet.address, spenderAddress); console.log("Allowance:", allowance.toString()); return allowance; } async function approveIfNeeded(tokenAddress: string, spenderAddress: string, requiredAmount: bigint): Promise { const allowance = await checkAllowance(tokenAddress, spenderAddress); if (allowance >= requiredAmount) { console.log("Allowance is sufficient for the swap."); return; } console.log("Insufficient allowance. Approving exact amount needed..."); const tokenContract = new Contract(tokenAddress, erc20ABI, wallet); const tx = await tokenContract.approve(spenderAddress, requiredAmount); console.log("Approval transaction sent. Hash:", tx.hash); console.log("Waiting for confirmation..."); await tx.wait(); console.log("Approval confirmed!"); } async function performSwap(): Promise { const swapParams = { src: config.srcTokenAddress, dst: config.dstTokenAddress, amount: config.amountToSwap, from: wallet.address.toLowerCase(), slippage: config.slippage.toString(), disableEstimate: "false", allowPartialFill: "false" }; console.log("Fetching swap transaction..."); const swapTx = await call1inchAPI("/swap", swapParams); console.log("Swap transaction:", swapTx.tx); const txHash = await signAndSendTransaction(swapTx.tx); console.log("Swap transaction sent. Hash:", txHash); } async function main() { try { await approveIfNeeded(config.srcTokenAddress, config.aggregationRouter, BigInt(config.amountToSwap)); await performSwap(); } catch (err) { console.error("Error:", (err as any)?.response?.data || err); } } main().catch((err) => { console.error("Unhandled error in main:", err); process.exit(1); }); ``` ## Go Program Uses the [official 1inch Go SDK](https://github.com/1inch/1inch-sdk-go/) to perform a token swap on the Base chain: 1. Loads configuration variables (private key, RPC URL, and 1inch Business Portal API key) from the environment 2. Connects to the Base chain using the SDK 3. Uses the `GetApproveAllowance` function in the SDK to call the `/approve/allowance` REST endpoint to check if your wallet has granted the proper USDC allowance to the 1inch router 4. If the allowance is **insufficient**, it uses the `GetApproveTransaction` function in the SDK to call the `/approve/transaction` REST endpoint to construct and send an approval transaction 5. Then, it uses the `GetSwap` function in the SDK to call the `/swap` REST endpoint to construct the swap transaction 6. It signs and broadcasts these transactions using the SDK client ### Dependencies - go v1.24.5 ```bash go get github.com/1inch/1inch-sdk-go/sdk-clients/aggregation go get github.com/ethereum/go-ethereum ``` ### Code ```go package main import ( "context" "fmt" "log" "math/big" "os" "time" "github.com/1inch/1inch-sdk-go/constants" "github.com/1inch/1inch-sdk-go/sdk-clients/aggregation" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ) var ( privateKey = os.Getenv("PRIVATE_KEY") nodeUrl = os.Getenv("RPC_URL") devPortalToken = os.Getenv("DEV_PORTAL_API_KEY") ) const ( UsdcBase = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" WethBase = "0x4200000000000000000000000000000000000006" amountUsdc = "100000" // 0.1 USDC (6 decimals) ) func main() { config, err := aggregation.NewConfiguration(aggregation.ConfigurationParams{ NodeUrl: nodeUrl, PrivateKey: privateKey, ChainId: constants.BaseChainId, ApiUrl: "https://api.1inch.com", ApiKey: devPortalToken, }) if err != nil { log.Fatalf("Failed to create configuration: %v\n", err) } client, err := aggregation.NewClient(config) if err != nil { log.Fatalf("Failed to create client: %v\n", err) } ctx := context.Background() walletAddr := client.Wallet.Address().Hex() // Step 1: Check Allowance allowanceData, err := client.GetApproveAllowance(ctx, aggregation.GetAllowanceParams{ TokenAddress: UsdcBase, WalletAddress: walletAddr, }) if err != nil { log.Fatalf("Failed to get allowance: %v\n", err) } allowance := new(big.Int) allowance.SetString(allowanceData.Allowance, 10) amountToSwap := new(big.Int) amountToSwap.SetString(amountUsdc, 10) // Step 2: Approve if needed if allowance.Cmp(amountToSwap) < 0 { fmt.Println("Insufficient allowance. Approving...") approveData, err := client.GetApproveTransaction(ctx, aggregation.GetApproveParams{ TokenAddress: UsdcBase, Amount: amountUsdc, }) if err != nil { log.Fatalf("Failed to get approve data: %v\n", err) } data, err := hexutil.Decode(approveData.Data) if err != nil { log.Fatalf("Failed to decode approve data: %v\n", err) } to := common.HexToAddress(approveData.To) tx, err := client.TxBuilder.New().SetData(data).SetTo(&to).Build(ctx) if err != nil { log.Fatalf("Failed to build approve transaction: %v\n", err) } signedTx, err := client.Wallet.Sign(tx) if err != nil { log.Fatalf("Failed to sign approve transaction: %v\n", err) } err = client.Wallet.BroadcastTransaction(ctx, signedTx) if err != nil { log.Fatalf("Failed to broadcast approve transaction: %v\n", err) } fmt.Printf("Approve transaction sent: https://basescan.org/tx/%s\n", signedTx.Hash().Hex()) // Wait for approval to be mined for { receipt, _ := client.Wallet.TransactionReceipt(ctx, signedTx.Hash()) if receipt != nil { fmt.Println("Approve transaction confirmed.") break } time.Sleep(2 * time.Second) } } else { fmt.Println("Sufficient allowance already present.") } // Step 3: Perform Swap swapData, err := client.GetSwap(ctx, aggregation.GetSwapParams{ Src: UsdcBase, Dst: WethBase, Amount: amountUsdc, From: walletAddr, Slippage: 1, // 1% slippage }) if err != nil { log.Fatalf("Failed to get swap data: %v\n", err) } tx, err := client.TxBuilder.New(). SetData(swapData.TxNormalized.Data). SetTo(&swapData.TxNormalized.To). SetGas(swapData.TxNormalized.Gas). SetValue(swapData.TxNormalized.Value). Build(ctx) if err != nil { log.Fatalf("Failed to build transaction: %v\n", err) } signedTx, err := client.Wallet.Sign(tx) if err != nil { log.Fatalf("Failed to sign transaction: %v\n", err) } err = client.Wallet.BroadcastTransaction(ctx, signedTx) if err != nil { log.Fatalf("Failed to broadcast transaction: %v\n", err) } fmt.Printf("Swap transaction sent: https://basescan.org/tx/%s\n", signedTx.Hash().Hex()) // Wait for swap transaction to be mined for { receipt, _ := client.Wallet.TransactionReceipt(ctx, signedTx.Hash()) if receipt != nil { fmt.Println("Swap transaction confirmed!") break } time.Sleep(2 * time.Second) } } ``` https://business.1inch.com/portal/assets/docs-v2/apis/swap/real-world-assets-support.md ## What are RWAs? Real-world assets are tokenized representations of traditional financial instruments made accessible on-chain. In the case of Ondo-backed, xStocks, Bitstamp, Maple, Reserve, Tenbin, and St0x assets, the tokens are designed to follow the total economic performance of the underlying instrument, aiming to mirror its value behavior rather than operate as derivatives. By bringing these instruments to blockchain ecosystems, RWAs allow users and developers to interact with traditional market exposures without directly accessing traditional brokerage or settlement infrastructure. ## Why can RWAs be useful? - _Expanded exposure set_: RWAs introduce equity, fixed-income, and commodity-linked exposures alongside crypto assets. - _Portfolio diversification_: RWAs can complement crypto-native assets with different risk and return characteristics. - _Composable on-chain usage_: once tokenized, RWAs can be integrated into smart contracts, swap execution modes, limit orders, and automated strategies. - _Gasless option_: when trading via Intent (Fusion) or Cross-chain modes, users can gain gasless exposure to traditional markets without holding native tokens to cover fees. ## How do RWAs work in 1inch? 1inch supports RWAs across **all Swap products** -- Classic Swap, Intent Swap (Fusion), and Cross-chain Swap (Fusion+) -- and via the **Orderbook API** (limit orders), subject to issuer and network availability. **Availability differs by tokenized asset.** Which products, networks, trading hours, and jurisdictions apply depends on the specific RWA (issuer rules, liquidity, and eligibility). Integrators should treat support as per-asset rather than universal for every tokenized instrument. Supported RWA tokens can be traded through the execution mode that fits the use case: - **Classic Swap** -- transaction-based quotes and swaps with full parameter control. - **Intent Swap (Fusion)** -- gasless, MEV-protected intents fulfilled by competing resolvers; the user signs an intent and resolvers pay on-chain gas. - **Cross-chain Swap (Fusion+)** -- bridgeless cross-chain execution where supported. - **Orderbook** -- fixed-price limit orders for RWA tokens where available. Supported RWA issuers (providers) include: - [Ondo Finance](https://ondo.finance/) - [xStocks](https://xstocks.fi/) (Backed Finance AG) - Bitstamp - Maple - Reserve - Tenbin - St0x Availability differs by tokenized asset, network, and jurisdiction. This integration is powered through collaboration with these issuers, which helps ensure reliable pricing and smooth handling of RWA-linked tokens within the execution process. ## xStocks access and trading conditions ### Supported networks - Ethereum - BNB Smart Chain ### Geographic availability and time of operation Eligibility and jurisdictional availability are determined by the issuer (Backed Finance AG). Refer to xStocks documentation for current eligibility criteria: [FAQ](https://docs.xstocks.fi/docs/frequently-asked-questions). xStocks RWAs are available for trading 24/7 and provide continuous access without time restrictions. ### Minimum order size Note that orders below $20 are not filled. ## Ondo access and trading conditions ### Supported networks - Ethereum - BNB Smart Chain ### Geographic availability Eligibility and jurisdictional availability of Ondo-backed RWAs are determined by the issuer (Ondo Finance). Refer to Ondo's eligibility documentation for the current list of supported and excluded jurisdictions: https://docs.ondo.finance/ondo-global-markets/eligibility. ## Bitstamp access and trading conditions ### Supported networks - Robinhood Eligibility and jurisdictional availability for Bitstamp RWAs are determined by the issuer. For the latest details on supported tokens, trading hours, and geographic restrictions, refer to the issuer's documentation. ## Maple access and trading conditions ### Geographic availability Eligibility and jurisdictional availability for Maple RWAs are determined by the issuer. Maple RWA tokens are geo-restricted: requests originating from restricted jurisdictions are blocked (HTTP 451) at the API level. For the latest details on supported tokens, trading hours, and geographic restrictions, refer to the issuer's documentation. ## Reserve access and trading conditions ### Supported networks - BNB Smart Chain - Ethereum - Base ### Geographic availability Eligibility and jurisdictional availability for Reserve RWAs are determined by the issuer. Reserve RWA tokens are geo-restricted: requests originating from restricted jurisdictions are blocked (HTTP 451) at the API level. For the latest details on supported tokens, trading hours, and geographic restrictions, refer to the issuer's documentation. ## Tenbin access and trading conditions ### Supported networks - Ethereum ### Geographic availability Eligibility and jurisdictional availability for Tenbin RWAs are determined by the issuer. Tenbin RWA tokens are geo-restricted: requests originating from restricted jurisdictions are blocked (HTTP 451) at the API level. For the latest details on supported tokens, trading hours, and geographic restrictions, refer to the issuer's documentation. ## St0x access and trading conditions ### Geographic availability Eligibility and jurisdictional availability for St0x RWAs are determined by the issuer. St0x RWA tokens are geo-restricted: requests originating from restricted jurisdictions are blocked (HTTP 451) at the API level. For the latest details on supported tokens, trading hours, and geographic restrictions, refer to the issuer's documentation. ## Time of operation Primary pricing and liquidity for RWAs follow traditional market hours and are generally available 24 hours a day, 5 days a week, to stay aligned with underlying markets. :::info Outside of market hours (e.g., weekends or holidays), secondary transfers may still occur, but pricing may be wider or less representative due to underlying markets being closed. ::: ## Quick FAQ - **Which RWA tokens are available via 1inch Swap and Orderbook APIs?** Only whitelisted, issuer-approved RWA tokens that meet 1inch's technical, liquidity, and compliance integration criteria, as well as issuers' requirements. Availability differs by tokenized asset and may not be the same across Classic, Intent, Cross-chain, and Orderbook. - **Which RWA providers does 1inch support?** Ondo, xStocks, Bitstamp, Maple, Reserve, Tenbin, and St0x. Availability differs by tokenized asset, network, and jurisdiction. - **What token standards are supported?** Currently, ERC-20 tokens are supported for RWA trading via the 1inch Swap and Orderbook APIs. - **Do users pay gas to trade RWAs?** It depends on the product. Intent (Fusion) and Cross-chain (Fusion+) can execute as gasless swaps -- resolvers cover gas costs. Classic Swap and Orderbook (limit order) flows generally require on-chain gas as usual for those products. - **Does 1inch perform KYC or AML on users before purchasing the RWAs?** No. 1inch does not perform KYC/AML. Compliance requirements are enforced by the RWA issuer at the token or contract level. - **What is the integrator responsible for?** Integrators are responsible for: user eligibility checks, UI/UX warnings, regulatory disclosures and jurisdictional compliance, and compliance with the relevant RWA issuer's terms and eligibility criteria. --- ## Legal disclaimer _Information relating to real-world assets backed by Ondo Finance (Ondo), Backed Finance AG (xStocks), Bitstamp, Maple, Reserve, Tenbin, and St0x, including their structure, backing, regulatory positioning, eligibility, and availability, is provided by Ondo, xStocks, Bitstamp, Maple, Reserve, Tenbin, and/or St0x, or with reference thereto. Developers and users should review own terms of Ondo, xStocks, Bitstamp, Maple, Reserve, Tenbin, and/or St0x, disclosures, and documentation and monitor updates provided directly by Ondo, xStocks, Bitstamp, Maple, Reserve, Tenbin, and/or St0x._ _1inch does not issue, sponsor, endorse, manage, or provide investment advice in relation to any RWA or underlying asset. 1inch acts solely as an infrastructure and technology provider, enabling access to supported tokens via its APIs, where available._ _This material is provided for informational purposes only and does not constitute investment advice, an offer, solicitation, or recommendation to acquire or dispose of any asset._ _Availability of RWAs may vary by jurisdiction, eligibility criteria, network, market conditions, and third-party restrictions, and may change over time. 1inch may apply different availability across its own products and at levels accessible to it. 1inch does not monitor, verify, or take responsibility for the integrator's enforcement of end-user eligibility, screening, disclosures, or jurisdictional compliance._ _Nothing in this documentation creates any partnership, agency, joint venture, or fiduciary relationship between 1inch and any RWA issuer._ ### Orderbook API https://business.1inch.com/portal/assets/docs-v2/apis/orderbook/introduction.md 1inch Orderbook API is using the 1inch [Limit Order Protocol](https://github.com/1inch/limit-order-protocol), which is a set of smart contracts that can work on any EVM-based blockchains. Key features of the protocol are extreme flexibility and high gas efficiency. ## Orderbook An orderbook is a trading mechanism where users create fixed-price orders for tokens, specifying both the execution price and an expiration condition that limits how long the order remains valid. For example, you can put up 2 WBTC tokens for sale at the price of 82,415 DAI tokens valid for 7 days. The 1inch Limit Orders Protocol provides a rich set of tools for flexible and professional trade management: - Partial fill Orders can be filled in multiple steps without requiring full execution at once. - Predicates Conditional execution logic (e.g. price bounds, time constraints, external state checks). - Single cancellation Cancel a specific order by its unique hash. - Batch (bunch) cancellation Invalidate multiple orders efficiently in a single action. - Fullness check Ensure the order is filled exactly as specified, preventing over- or under-fills when required. - Validation Custom on-chain validation logic to control whether an order can be executed. To enable efficient market making, RFQ orders are used, benefiting from a special optimization that avoids high gas costs during execution. ## Limit order execution ### 1. Order placement Users place limit orders. Each order is cryptographically signed and remains off-chain until executed. ### 2. Order discovery Anyone can fetch these signed orders using the [1inch Orderbook API](./methods) to execute trades by submitting them on-chain. ### 3. Order execution To execute an order, the signed order is submitted to the smart contract's `fillOrder` method. See the latest contract addresses in [Limit Order SDK](https://github.com/1inch/limit-order-sdk/blob/master/src/constants.ts). :::info Both the buyer and seller must approve the 1inch Limit Order Protocol to transfer their tokens. ::: ## Supported tokens standards - ERC-20 - ERC-721 - ERC-1155 Other token standards can be supported via external extensions. ## Real-world assets (RWAs) The Orderbook API supports limit orders for tokenized real-world assets (RWAs), alongside the Swap API products (Classic, Intent, and Cross-chain). **Availability differs by tokenized asset** -- issuer rules, supported networks, eligibility, market hours, and liquidity can vary. For issuers, networks, and trading conditions, see [Real-world assets support](/portal/documentation/apis/swap/real-world-assets-support). :::info For workloads that require higher RPS, expanded quotas, or sustained performance, explore the available paid plans on the [**pricing page**](/pricing). For organizations with significant trading volumes or advanced requirements, [**complete this application**](/get-enterprise) to explore tailored Enterprise options. ::: ## Supported Chains | Supported Chains | Chain ID | | :--------------- | :------- | | Ethereum | 1 | | Base | 8453 | | BNB | 56 | | zkSync | 324 | | Gnosis | 100 | | Optimism | 10 | | Cronos | 25 | | Polygon | 137 | | Monad | 143 | | Linea | 59144 | | Sonic | 146 | | Unichain | 130 | | Arbitrum | 42161 | | Avalanche | 43114 | | HyperEVM | 999 | | Arc | 5042 | | Robinhood | 4663 | ## More resources - To quickly understand how to create, sign, and submit a limit order, see the [Quickstart guide](./quick-start.md). - For detailed information about each endpoint, refer to the [Orderbook API Swagger section](./methods). - Try directly at [1inch dApp](https://1inch.com/pro?mode=limit&pair=1:0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2-0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48). - Explore the [Limit Order Protocol](https://github.com/1inch/limit-order-protocol/). - For integration purposes, check out [Limit Order SDK](./limit-order-sdk/introduction.md). https://business.1inch.com/portal/assets/docs-v2/apis/orderbook/quick-start.md ## Introduction This guide shows how to create and submit a Limit Order v4 using the [@1inch/limit-order-sdk](https://github.com/1inch/limit-order-sdk) on Ethereum. ## Before You Start - You must have a valid **1inch API Key**. You can get one from the [1inch Business](https://business.1inch.com/portal). - Your wallet must have: - USDC on Ethereum (for the maker asset) - ETH for the token approval transaction ## Integration Pattern 1. **Approve Tokens** - Grant the Limit Order Protocol contract permission to spend your tokens 2. **Initialize SDK** - Create an SDK instance with your API key and network 3. **Configure Traits** - Set expiration and nonce via `MakerTraits` 4. **Create Order** - Build a properly formatted order with the SDK 5. **Sign Order** - Sign with EIP-712 typed data (no gas required) 6. **Submit Order** - Send the signed order to the 1inch Orderbook API ## Interactive Code Example Explore the minimal quick-start implementation (~90 lines): For a more comprehensive example with provider-agnostic design and balance checks, see `src/create-limit-order.ts` in the same project. https://business.1inch.com/portal/assets/docs-v2/apis/orderbook/sdk/introduction.md The [limit-order-sdk](https://github.com/1inch/limit-order-sdk) is a TypeScript library developed by 1inch that enables developers to build, sign, and manage off-chain limit orders, fully compatible with the 1inch Limit Order Protocol. ## Key Benefits - **Gasless Order Creation** - Sign orders off-chain with EIP-712, only pay gas for token approval - **Orderbook Discovery** - Submit signed orders to the 1inch Orderbook for resolvers to fill - **Multi-Token Support** - Trade ERC-20, ERC-721, and ERC-1155 tokens - **Permit Support** - Use permits for gasless approvals ## How It Works 1. **Approve Tokens** - Grant the Limit Order Protocol contract permission to spend your tokens (one-time gas cost) 2. **Create Order** - Use the SDK to build a properly formatted order with assets, amounts, and expiration 3. **Sign Order** - Sign the order off-chain using EIP-712 typed data (no gas required) 4. **Submit to Orderbook** - Send the signed order to the 1inch Orderbook API (free) 5. **Resolver Fills** - Resolvers discover and fill your order on-chain ## Interactive Code Example Explore the complete implementation -- this example demonstrates creating, signing, and submitting a limit order: ## Advanced usage In addition to publishing limit orders to the 1inch Orderbook, the Limit Order SDK can also be used for more advanced and customized workflows that operate independently of the Orderbook API. To learn more about this use case, see [Advanced usage for custom protocol integration](./advanced-usage.md). https://business.1inch.com/portal/assets/docs-v2/apis/orderbook/sdk/querying-the-orderbook.md The Limit Order SDK provides a built-in `Api` class for querying orders from the 1inch Orderbook. You can retrieve orders by hash, by maker address, or browse all active orders with pagination support. ## Prerequisites You need: - A 1inch API key (get one at [1inch Business](https://business.1inch.com/portal)) - The `@1inch/limit-order-sdk` package installed No wallet or private key is required for read-only queries. ## Setting up the API client ```javascript import { Api, FetchProviderConnector, CursorPager } from "@1inch/limit-order-sdk"; const api = new Api({ networkId: 1, // Ethereum mainnet authKey: "your-api-key", httpConnector: new FetchProviderConnector() }); ``` ## Query by order hash If you have an order hash from a previous submission, you can retrieve its full details: ```javascript const order = await api.getOrderByHash(orderHash); console.log("Maker Asset:", order.data.makerAsset); console.log("Taker Asset:", order.data.takerAsset); console.log("Making Amount:", order.data.makingAmount); console.log("Taking Amount:", order.data.takingAmount); ``` ## Query by maker address To retrieve all orders created by a specific wallet address, use `getOrdersByMaker` with pagination: ```javascript const response = await api.getOrdersByMaker(makerAddress, { pager: new CursorPager({ limit: 10 }), statuses: [1] // 1 = active orders only }); for (const order of response.items) { console.log("Maker Asset:", order.data.makerAsset); console.log("Taking Amount:", order.data.takingAmount); } ``` ### Status codes | Status | Meaning | | ------ | --------- | | 1 | Active | | 2 | Filled | | 3 | Expired | | 4 | Cancelled | ## Query all active orders To browse the full orderbook, use `getAllOrders` with pagination: ```javascript const response = await api.getAllOrders({ pager: new CursorPager({ limit: 5 }), statuses: [1] }); console.log(`Found ${response.items.length} active orders`); ``` ## Full working example https://business.1inch.com/portal/assets/docs-v2/apis/orderbook/sdk/advanced-usage.md The [limit-order-sdk](https://github.com/1inch/limit-order-sdk) can be used not only to integrate with the 1inch Orderbook, but also as a toolkit for building custom trading infrastructure based on the 1inch Limit Order Protocol. This approach is intended for teams who want full control over how orders are constructed, signed, and filled--independently of the public 1inch backend. With this scenario, you can benefit from using the following advanced features: - Manually construct limit and RFQ orders - Asset evaluation helpers - Custom execution predicates ## Difference between Orderbook API and custom protocol | Feature | Orderbook API | Custom Protocol Integration | | :------------------ | :-------------------------- | :-------------------------- | | Limit orders | [x] Supported | [x] Supported | | `permit` field | [x] Supported | [x] Supported | | RFQ orders | [ ] Not supported | [x] Fully supported | | Custom predicates | [ ] Ignored | [x] Executed on-chain | | `interaction` field | [ ] Not accepted | [x] Fully usable | | Fee customization | [ ] Fixed (protocol default) | [x] Fully configurable | ## About RFQ orders A Request for Quotation (RFQ) is a business process where a trader requests a price quote from a supplier for the purchase of specific tokens. RFQ orders are gas-optimized and purpose-built for the needs of professional market makers. They offer lightweight execution with specific constraints: - Supports expiration time - Can be canceled by order ID - Fillable only once - Allows partial fill (only once) These characteristics make RFQ orders ideal for real-time, short-lived liquidity provisioning. ## Start your custom integration Explore internals and primitives in the Limit Order SDK repository: - [Limit order](https://github.com/1inch/limit-order-sdk/blob/master/src/limit-order/) - [RFQ order](https://github.com/1inch/limit-order-sdk/tree/master/src/rfq-order) ## Wallet & Data ### History API https://business.1inch.com/portal/assets/docs-v2/apis/history/introduction.md History API allows you to access accurate transaction history data across several major networks. With it, you can get such information as: - Transaction type, hash, status, fee, timestamps - Participants addresses - Token transfers, and more ## Supported Chains | Supported Chains | Chain ID | | :--------------- | :------- | | Ethereum | 1 | | Solana | 501 | | Base | 8453 | | BNB | 56 | | zkSync | 324 | | Gnosis | 100 | | Optimism | 10 | | Cronos | 25 | | Polygon | 137 | | Monad | 143 | | Linea | 59144 | | Sonic | 146 | | Unichain | 130 | | Arbitrum | 42161 | | Avalanche | 43114 | | HyperEVM | 999 | | Arc | 5042 | | Robinhood | 4663 | ## Use cases The History API can be applied across various industries and scenarios that require access to accurate on-chain transaction data. Below are some common use cases: ### Compliance and auditing The History API enables companies to access transaction details, including participants, amounts, and timestamps, helping them meet auditing requirements and adhere to Anti-Money Laundering (AML) regulations. ### Portfolio management Investors and portfolio managers can use the History API to analyze transaction histories across multiple blockchain networks. It helps track token movements, swaps, staking activities, and liquidity provisions, allowing your users to generate reports and optimize investment strategies. ### DeFi protocol analytics Decentralized Finance (DeFi) protocols rely on transaction data to monitor user behavior, liquidity, and protocol usage. The History API provides detailed information on liquidity additions, removals, borrowing, lending, and staking activities, making it essential for DeFi analytics platforms. ### NFT marketplaces NFT marketplaces can integrate the History API to display detailed transaction histories for NFTs, including purchases, bids, sales, and transfers. This enables buyers and sellers to review historical data before making decisions. ### Security and risk management Security platforms can use the History API to track transaction histories and identify suspicious activities, such as unauthorized approvals or large token transfers. The API's detailed breakdown of transaction types helps detect patterns indicative of potential security threats, protecting users from fraud and malicious actions. ### Enterprise resource management Enterprises engaging in blockchain transactions can integrate the History API to manage internal operations and resources more effectively. It helps track employee or system-triggered transactions, manage smart contract interactions, and ensure transparency in decentralized workflows. ### Tax reporting Individuals and businesses dealing with cryptocurrencies must track all transactions for accurate tax reporting. The History API allows users to retrieve transaction histories to calculate gains, losses, and taxable events such as token swaps and sales. :::info For workloads that require higher RPS, expanded quotas, or sustained performance, explore the available paid plans on the [**pricing page**](/pricing). For organizations with significant trading volumes or advanced requirements, [**complete this application**](/get-enterprise) to explore tailored Enterprise options. ::: ## Transaction types We define the following transaction types: | Transaction type | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------- | | `Unknown` | A transaction type that hasn't been identified or categorized. | | `Approve` | Granting permission for a smart contract to spend your tokens. | | `Wrap` | Converting a cryptocurrency into its wrapped version. | | `Unwrap` | Converting a wrapped token back to its original cryptocurrency. | | `Swap` | Exchanging one type of token for another. | | `Transfer` | Sending tokens from one address to another. | | `LimitOrderFill` | Executing a limit order by buying or selling tokens at the specified price or better. | | `LimitOrderCancel` | Canceling an existing limit order that is expired or hasn't been filled fully. | | `LimitOrderCancelAll` | Canceling an active limit order. | | `AddLiquidity` | Providing tokens to a liquidity pool to facilitate trading and earn rewards. | | `RemoveLiquidity` | Withdrawing tokens from a liquidity pool. | | `Borrow` | Taking out a loan in the form of tokens from a lending protocol. | | `Repay` | Returning borrowed tokens to a lending protocol. | | `Stake` | Locking tokens in a protocol to earn rewards or participate in governance. | | `Unstake` | Withdrawing staked tokens from a protocol. | | `Vote` | Casting a vote on a proposal in a decentralized governance system. | | `DelegateVotePower` | Assigning your voting power to another address to vote on your behalf. | | `UnDelegateVotePower` | Revoking the assignment of your voting power from a delegate. | | `DiscardVote` | Invalidating or withdrawing a previously cast vote. | | `Claim` | Retrieving rewards or tokens that are due to you. | | `Action` | Performing a specific function or task within a smart contract. | | `Bridge` | Transferring tokens across different blockchains. | | `BuyNft` | Purchasing an NFT from the marketplace. | | `BidNft` | Placing a bid on an NFT in an auction. | | `OfferSellNft` | Making an offer to sell an NFT you own. | | `Burn` | Permanently removing tokens from circulation. | | `WrappedTx` | A [Gnosis Safe](https://app.safe.global/welcome) transaction which can include one or more multisigned transactions. | | `RegisterENSDomain` | Registering a human-readable name on the Ethereum Name Service (ENS) to associate with your address. | | `Revoke` | Withdrawing previously granted permissions or approvals. | | `CreateSafe` | Creating a multisig Gnosis Safe Wallet. | | `AddOwner` | Adding a new owner to a multisig Gnosis Safe Wallet. | | `MultiStage` | A transaction that involves multiple steps or stages, often used in complex operations. | ## Supported networks - [Ethereum Mainnet](https://ethereum.org/) - [Arbitrum](https://arbitrum.io/) - [Avalanche](https://www.avax.network/) - [BNB Chain](https://www.binance.com/en/support/announcement/854415cf3d214371a7b60cf01ead0918) - [Gnosis](https://www.xdaichain.com/) - [Sonic](https://www.soniclabs.com/) - [Optimism](https://www.optimism.io/) - [Cronos](https://cronos.org/) - [Polygon](https://polygon.technology/) - [Monad](https://www.monad.xyz/) - [zkSync Era](https://docs.zksync.io/build) - [Base](https://docs.base.org/) - [Unichain](https://www.unichain.org/) - [HyperEVM](https://hyperliquid.xyz/) - Arc - Robinhood ## Quickstart To start using History API swiftly, refer to the [Quickstart guide](./quick-start). ## API reference For detailed information about each endpoint, refer to the History API [Open API section](./methods). https://business.1inch.com/portal/assets/docs-v2/apis/history/quick-start.md This guide will walk you through on how you can seamlessly retrieve and display wallet transactions by using the wallet history API. ## Prerequisites - Node.js and npm installed - Basic knowledge of JavaScript, React, and Express.js ## Step-by-step guide ### Step 1: Initialization 1. Create a new directory for the project: ```bash mkdir wallet-history && cd wallet-history ``` 2. Initialize a new Node.js project: ```bash npm init -y ``` 3. Install Express, CORS, and Axios: ```bash npm install express cors axios ``` 4. Install dotenv: ```bash npm install dotenv ``` Then create a new file called .env and add your api key: ```bash API_KEY=YOUR_1INCH_API_KEY ``` 5. Create a new file `api.js` and set up a basic Express server by pasting into it the following: ```javascript // Import required packages const express = require("express"); const axios = require("axios"); const dotenv = require("dotenv"); const cors = require("cors"); const path = require("path"); // Initialize the app and load environment variables from .env dotenv.config({ path: path.resolve(__dirname, ".env") }); const app = express(); // Enable CORS for all routes app.use(cors()); const BASE_URL = "https://api.1inch.com/history/v2.0/history"; // Endpoint to fetch wallet transaction history app.get("/api/:address/history", async (req, res) => { const address = req.params.address; const limit = req.query.limit || 10; try { const constructedUrl = `${BASE_URL}/${address}/events?chainId=${1}&limit=${limit}`; const response = await axios.get(constructedUrl, { headers: { Authorization: `Bearer ${process.env.API_KEY}` // Use API key from .env } }); // Send the response data back to the client res.json(response.data); } catch (error) { console.error("Error fetching wallet transactions:", error); res.status(500).json({ error: "An error occurred while fetching data" }); } }); // Start the server const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); }); ``` 1. Add an endpoint to fetch wallet transactions and don't forget to replace API_KEY with your 1inch Business API key: ```javascript const BASE_URL = "https://api.1inch.com/history/v2.0/history"; app.get("/api/:address/history", async (req, res) => { const address = req.params.address; const limit = req.query.limit || 10; try { const constructedUrl = `${BASE_URL}/${address}/events?chainId=${1}&limit=${limit}`; const response = await axios.get(constructedUrl, { headers: { Authorization: `Bearer ${process.env.API_KEY}` } }); // Send the data from the API back to the client res.json(response.data.items); } catch (error) { console.error("Axios Error: ", error.response); res.status(500).json({ error: "Failed to fetch wallet transactions" }); } }); ``` ### Step 2: Setting up React frontend 1. Create a new React application: ```bash npx create-react-app client ``` 2. Navigate to the React application directory: ```bash cd client ``` 3. Create a `History.js` component inside the `src` directory with the following content: ```javascript import React, { useState, useEffect } from "react"; const History = ({ address }) => { const [transactions, setTransactions] = useState([]); const [isLoading, setLoading] = useState(false); useEffect(() => { const fetchData = async () => { setLoading(true); const response = await fetch(`http://localhost:5001/api/${address}/history`); if (!response.ok) { console.log("Fetch history error", response); return; } const transactions = await response.json(); setTransactions(transactions); setLoading(false); }; if (isLoading) { return; } fetchData(); }, [address]); if (isLoading) { return
Loading...
; } return ( ); }; export default History; ``` 4. Import and use `History` in `src/App.js`: ```javascript import React, { useState } from "react"; import "./App.css"; import History from "./History"; function App() { const [inputValue, setSearchTerm] = useState(""); const [address, setAddress] = useState(); const handleChange = (event) => { setSearchTerm(event.target.value); }; const handleSubmit = (event) => { event.preventDefault(); setAddress(inputValue); }; return (
{address &&

Ethereum history of {address}

} {address && }
); } export default App; ``` ### Step 3: Running the project 1. Start the Express server: ```bash node api.js ``` 2. In a new terminal, navigate to the client directory and start the React app: ```bash cd client ``` Now, you can view your wallet transaction history at http://localhost:3000. Described above is a basic setup and you can expand upon this by adding more features, error handling, and styling to get to production.
### Traces API https://business.1inch.com/portal/assets/docs-v2/apis/traces/introduction.md The 1inch Trace API provides comprehensive, step-by-step data on every operation within on-chain transactions. This empowers developers and analysts to gain precise insights into transaction behavior. This API supports many networks ensuring compatibility across diverse ecosystems. With fast and organized transaction traces, you can debug, audit, and optimize with ease, whether you're working on smart contracts or analyzing historical transaction data. ## Supported Chains | Supported Chains | Chain ID | | :--------------- | :------- | | Ethereum | 1 | | Base | 8453 | | BNB | 56 | | zkSync | 324 | | Gnosis | 100 | | Optimism | 10 | | Cronos | 25 | | Polygon | 137 | | Monad | 143 | | Linea | 59144 | | Sonic | 146 | | Unichain | 130 | | Arbitrum | 42161 | | Avalanche | 43114 | | HyperEVM | 999 | | Arc | 5042 | | Robinhood | 4663 | ## What are transaction traces? A transaction trace is essentially a step-by-step representation of every operation that occurs during the execution of a transaction on the blockchain. For Ethereum, these operations can be quite varied, from basic transfers of ETH to complex contract interactions. ## Why are transaction traces useful? - **Debugging smart contracts**: developers can use transaction traces to troubleshoot their smart contracts, especially when something goes wrong. They can identify which specific step of the contract resulted in an error. - **Optimization**: developers can analyze how gas is consumed during different operations in the transaction. This helps in optimizing smart contracts to be more gas efficient. - **Security audits**: transaction traces can be instrumental in security audits. They allow auditors to see the exact flow of operations, ensuring that the contract behaves as expected. - **Understanding contract behavior**: for those trying to understand how a particular smart contract operates, tracing provides a clear breakdown of its inner workings. - **Historical analysis**: for platforms or researchers analyzing blockchain data, transaction traces offer rich insights into the actual execution of historical transactions. :::info For workloads that require higher RPS, expanded quotas, or sustained performance, explore the available paid plans on the [**pricing page**](/pricing). For organizations with significant trading volumes or advanced requirements, [**complete this application**](/get-enterprise) to explore tailored Enterprise options. ::: ## API reference For detailed information about each endpoint, refer to the Traces API [Swagger section](./methods). https://business.1inch.com/portal/assets/docs-v2/apis/traces/quick-start.md ## 1inch Transaction Traces API: 1inch's Transaction Traces API provides users with an efficient way to access transaction traces for different chains. ### 1. Check Synced Interval: Before fetching any transaction traces, it's useful to know the synced interval. It gives the range of block numbers that have been synced and are available for query. **Endpoint**: ```text GET /v1.0/chain/{chain}/synced-interval ``` **Parameters**: - `chain` (required): Chain ID. For Ethereum mainnet, it would be `1`. **Sample Request**: ```text https://api.1inch.com/traces/v1.0/chain/1/synced-interval ``` ### 2. Get Block Trace by Number: To retrieve the transaction traces of all transactions in a specific block. **Endpoint**: ```text GET /v1.0/chain/{chain}/block-trace/{blockNumber} ``` **Parameters**: - `chain` (required): Chain ID. - `blockNumber` (required): The block number you wish to retrieve traces from. **Sample Request**: ```text https://api.1inch.com/traces/v1.0/chain/1/block-trace/15000000 ``` ### 3. Get Block Trace by Transaction Hash: If you are specifically interested in the trace of a particular transaction within a block, you can fetch it using the transaction hash. **Endpoint**: ```text GET /v1.0/chain/{chain}/block-trace/{blockNumber}/tx-hash/{txHash} ``` **Parameters**: - `chain` (required): Chain ID. - `blockNumber` (required): The block number containing the transaction. - `txHash` (required): The hash of the specific transaction you want to trace. **Sample Request**: ```text https://api.1inch.com/traces/v1.0/chain/1/block-trace/17378177/tx-hash/0x16897e492b2e023d8f07be9e925f2c15a91000ef11a01fc71e70f75050f1e03c ``` ### Prerequisites: - Node.js and npm installed. - A text editor or IDE. - API key for the Transaction Traces API. ### Steps: 1. **Initialize a New Node.js Project**: ```bash mkdir traces_api_tutorial cd traces_api_tutorial npm init -y ``` 2. **Install the Required Packages**: We'll be using `axios` to make the HTTP requests: ```bash npm install axios ``` 3. **Set Up Your API Key**: For the sake of security, store your API key in a `.env` file: ```bash echo "API_KEY=YOUR-API-KEY" > .env ``` Then, install `dotenv` to use this file: ```bash npm install dotenv ``` 4. **Write the Code**: Create a new file named `index.js` and add the following: ```javascript require("dotenv").config(); const axios = require("axios"); const BASE_URL = "https://api.1inch.com/traces/v1.0/chain"; async function getSyncedInterval(chain) { const url = `${BASE_URL}/${chain}/synced-interval`; const response = await axios.get(url, { headers: { Authorization: `Bearer ${process.env.API_KEY}` } }); return response.data; } async function getBlockTraceByNumber(chain, blockNumber) { const url = `${BASE_URL}/${chain}/block-trace/${blockNumber}`; const response = await axios.get(url, { headers: { Authorization: `Bearer ${process.env.API_KEY}` } }); return response.data; } async function getBlockTraceByNumberAndTxHash(chain, blockNumber, txHash) { const url = `${BASE_URL}/${chain}/block-trace/${blockNumber}/tx-hash/${txHash}`; const response = await axios.get(url, { headers: { Authorization: `Bearer ${process.env.API_KEY}` } }); return response.data; } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } (async function () { try { const chain = 1; // Example chain const blockNumber = 15000000; // Example block number const txHash = "0x16897e492b2e023d8f07be9e925f2c15a91000ef11a01fc71e70f75050f1e03c"; // Example transaction hash const syncedInterval = await getSyncedInterval(chain); console.log("Synced Interval:", syncedInterval); await sleep(1000); // Sleep for 1 second to avoid rate limit const blockTrace = await getBlockTraceByNumber(chain, blockNumber); console.log("Block Trace:", blockTrace); await sleep(1000); // Sleep for 1 second to avoid rate limit const txTrace = await getBlockTraceByNumberAndTxHash(chain, blockNumber, txHash); console.log("Transaction Trace:", txTrace); } catch (error) { console.error("Error fetching data:", error.response ? error.response.data : error.message); } })(); ``` 5. **Run Your Script**: ```bash node index.js ``` Transaction traces are a powerful tool for anyone interacting with or developing on the blockchain. They provide invaluable insights into the workings of smart contracts, helping improve efficiency, security, and understanding. With 1inch's Transaction Traces API, accessing these insights becomes a seamless process, enabling deeper interaction with blockchain data. With the steps above, you'll be able to interact with the Transaction Traces API using Node.js. Make sure you replace `'API-KEY'` in the `.env` file with your actual API key. ### Portfolio API https://business.1inch.com/portal/assets/docs-v2/apis/portfolio/introduction.md The 1inch Portfolio API aggregates and presents detailed information for web3 assets across multiple wallets and chains. By leveraging this API, you can gain a holistic view into any crypto portfolio and track performance metrics. This allows you or your users to make informed investment decisions without the hassle of juggling multiple platforms. ## Key features - **Multi-wallet support**: add and monitor multiple wallets to get a complete picture of your crypto holdings across various addresses. - **Multi-chain integration**: view assets and transactions across multiple supported blockchains on a single screen. - **Detailed analytics**: access advanced DeFi analytics, including metrics from top protocols like Uniswap, Curve, Aave, and more. - **Profit and loss tracking**: calculate and track your portfolio's profit and loss and return on investment across different protocols and tokens. - **Current value monitoring**: obtain real-time data on the current value of your assets, organized by chains and addresses. - **Comprehensive protocol data**: get detailed information on supported protocols, including underlying tokens, APR, unclaimed fees, and more. - **Investment opportunity discovery**: identify and explore new investment opportunities based on any given portfolio's performance and market trends. ## Supported Chains | Supported Chains | Chain ID | | :--------------- | :------- | | Ethereum | 1 | | Solana | 501 | | Base | 8453 | | BNB | 56 | | zkSync | 324 | | Gnosis | 100 | | Optimism | 10 | | Cronos | 25 | | Polygon | 137 | | Monad | 143 | | Linea | 59144 | | Sonic | 146 | | Unichain | 130 | | Arbitrum | 42161 | | Avalanche | 43114 | | HyperEVM | 999 | | Arc | 5042 | | Robinhood | 4663 | ## Getting started To begin using the 1inch Portfolio API, generate an API key and set up your development environment. Then you check out the [Quickstart guide](./quick-start), or dive right into any of the endpoints listed below. :::info For workloads that require higher RPS, expanded quotas, or sustained performance, explore the available paid plans on the [**pricing page**](/pricing). For organizations with significant trading volumes or advanced requirements, [**complete this application**](/get-enterprise) to explore tailored Enterprise options. ::: ## Core Concepts | Term | Meaning | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Address** | Wallet address. EVM chains use `0x...` format, Solana uses base58. | | **Chain ID** | Numeric blockchain identifier. `1` = Ethereum mainnet, `137` = Polygon, etc. See [supported chains](./methods/portfolio/v5.0/general/supported_chains/method/get). | | **Timerange** | Period for historical calculations. Values: `1day`, `1week`, `1month`, `1year`, `3years`. Counted backwards from current time. | | **Snapshot** | Point-in-time view of assets. By default returns current state; optional `timestamp` parameter returns historical state. | | **Metrics** | Aggregated historical data for a timerange: profit/loss, ROI, volume of inflows and outflows, etc. | | **Protocol** | DeFi application where user has positions (Aave, Uniswap, Compound, etc.). See [supported protocols](./methods/portfolio/v5.0/general/supported_protocols/method/get). Does not include simple token holdings. | | **Native token** | Chain's base currency (ETH on Ethereum, MATIC on Polygon). Represented as `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee` in API responses. | ## API reference For detailed information about each endpoint, refer to the Portfolio API [OpenAPI section](./methods). https://business.1inch.com/portal/assets/docs-v2/apis/portfolio/quick-start.md Base URL (unified gateway at `https://api.1inch.com`): `https://api.1inch.com/portfolio/portfolio/v5.0` The OpenAPI paths for the Portfolio service are `/portfolio/v5.0/...`. On the unified gateway, use an extra `/portfolio` segment so requests are routed correctly (the gateway strips the first `/portfolio` before forwarding). The same applies to the MCP `product_api` tool: use paths starting with `/portfolio/portfolio/v5.0`. Portfolio API provides real-time and historical data about wallet holdings across multiple blockchains. Track token balances, DeFi positions, profit and loss, and portfolio value over time. This guide covers the most common use cases to get you started quickly. For full API reference, see the [Open API specifications](./methods). ## Response Format All endpoints return responses wrapped in an envelope: ```json { "result": { ... }, "meta": { ... } } ``` Your data is always in `result`. The `meta` field contains optional metadata (pagination, warnings, etc.). ## Multiple Wallets Pass multiple `addresses` parameters to aggregate data across wallets: ```bash ?addresses=0xabc...&addresses=0xdef...&addresses=0x123... ``` The API will return combined results. Individual wallet breakdowns are available in `by_address` field where supported. ## Getting Started ### 1. Get Total Portfolio Value Returns USD value of all assets (tokens + DeFi positions) broken down by category, chain, and protocol. ```bash curl 'https://api.1inch.com/portfolio/portfolio/v5.0/general/current_value?addresses=0xd470055c6189b921c4d44b3d277ad868f79c0f75' \ -H 'Authorization: Bearer YOUR_API_KEY' ``` Response: ```json { "result": { "total": 12543.87, "by_category": [ { "category_id": "tokens", "category_name": "Tokens", "value_usd": 8234.12 }, { "category_id": "protocols", "category_name": "Protocols", "value_usd": 3106.3 }, { "category_id": "native", "category_name": "Native", "value_usd": 1203.45 } ], "by_chain": [ { "chain_id": 1, "chain_name": "Ethereum", "value_usd": 9500.0 }, { "chain_id": 137, "chain_name": "Polygon", "value_usd": 3043.87 } ], "by_protocol_group": [{ "protocol_group_id": "aave", "protocol_group_name": "Aave", "value_usd": 2500.0 }] } } ``` **Notes:** - `tokens` = ERC-20 tokens and SPL tokens - `native` = ETH, MATIC, BNB, etc. - `protocols` = value locked in DeFi (lending, LPs, staking, etc.) ### 2. Get Profit and Loss Returns realized + unrealized PnL for a given timerange, grouped by chain. ```bash curl 'https://api.1inch.com/portfolio/portfolio/v5.0/general/profit_and_loss?addresses=0xd470055c6189b921c4d44b3d277ad868f79c0f75&timerange=1year' \ -H 'Authorization: Bearer YOUR_API_KEY' ``` Response: ```json { "result": [ { "chain_id": 1, "abs_profit_usd": 2341.56 }, { "chain_id": 137, "abs_profit_usd": -128.3 }, { "chain_id": null, "abs_profit_usd": 2213.26 } ] } ``` **Notes:** - `chain_id: null` = aggregated total across all chains - Negative values indicate a loss ### 3. Get Token Holdings and DeFi Positions **Token balances:** ```bash curl 'https://api.1inch.com/portfolio/portfolio/v5.0/tokens/snapshot?addresses=0xd470055c6189b921c4d44b3d277ad868f79c0f75' \ -H 'Authorization: Bearer YOUR_API_KEY' ``` **DeFi positions:** ```bash curl 'https://api.1inch.com/portfolio/portfolio/v5.0/protocols/snapshot?addresses=0xd470055c6189b921c4d44b3d277ad868f79c0f75' \ -H 'Authorization: Bearer YOUR_API_KEY' ``` **Notes:** - Add `chain_id` parameter to filter by specific chain - Add `timestamp` (unix seconds) to get historical snapshot ### 4. Get Token and Protocol Metrics Returns PnL, ROI, inflows/outflows for each token or protocol position. **Token metrics:** ```bash curl 'https://api.1inch.com/portfolio/portfolio/v5.0/tokens/metrics?addresses=0xd470055c6189b921c4d44b3d277ad868f79c0f75&timerange=1year' \ -H 'Authorization: Bearer YOUR_API_KEY' ``` **Protocol metrics:** ```bash curl 'https://api.1inch.com/portfolio/portfolio/v5.0/protocols/metrics?addresses=0xd470055c6189b921c4d44b3d277ad868f79c0f75&timerange=1year' \ -H 'Authorization: Bearer YOUR_API_KEY' ``` **Notes:** - Returns a list of metrics per token/protocol position - Each item includes `profit_abs_usd`, `roi`, `inflow_usd`, `outflow_usd`, etc. ### 5. Get Single Token Details Returns detailed analytics for a specific token: average buy price, inflows, outflows, PnL. ```bash curl 'https://api.1inch.com/portfolio/portfolio/v5.0/tokens/additional/details?addresses=0xd470055c6189b921c4d44b3d277ad868f79c0f75&chain_id=1&contract_address=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&timerange=1year' \ -H 'Authorization: Bearer YOUR_API_KEY' ``` Response: ```json { "result": { "chain": 1, "contract_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "symbol": "USDC", "name": "USD Coin", "amount": 5000.0, "price_to_usd": 1.0, "value_usd": 5000.0, "abs_profit_usd": 127.45, "roi": 0.026, "average_price_usd": 0.998, "average_price_usd_period": 0.999, "value_inflow_usd": 4800.0, "value_outflow_usd": 320.0, "value_inflow_usd_period": 2400.0, "value_outflow_usd_period": 150.0 } } ``` **Notes:** - `average_price_usd` = all-time average purchase price - `average_price_usd_period` = average price within requested timerange - `roi` = return on investment as decimal (0.026 = 2.6%) - `*_period` fields = values calculated only for the requested timerange ### 6. Get Portfolio Value Chart Returns time series data for plotting portfolio value over time. ```bash curl 'https://api.1inch.com/portfolio/portfolio/v5.0/general/chart?addresses=0xd470055c6189b921c4d44b3d277ad868f79c0f75&timerange=1month' \ -H 'Authorization: Bearer YOUR_API_KEY' ``` Response: ```json { "result": [ { "timestamp": 1704067200, "value_usd": 10234.50 }, { "timestamp": 1704153600, "value_usd": 10456.78 }, ... ] } ``` **Notes:** - Timestamps are Unix epoch seconds (UTC) - Data points interval: 5min for `1day`, 30min for `1week`, 2h for `1month`, daily for `1year`, weekly for `3years` ## Common Use Cases | Use case | Endpoint | Required parameters | | ------------------------- | --------------------------------- | ------------------------------------------- | | Portfolio total value | `/general/current_value` | `addresses` | | PnL by chain | `/general/profit_and_loss` | `addresses`, `timerange` | | Token balances | `/tokens/snapshot` | `addresses` | | DeFi positions | `/protocols/snapshot` | `addresses` | | Token metrics | `/tokens/metrics` | `addresses`, `timerange` | | Protocol metrics | `/protocols/metrics` | `addresses`, `timerange` | | Single token analytics | `/tokens/additional/details` | `addresses`, `chain_id`, `contract_address` | | Token transaction history | `/tokens/additional/transactions` | `addresses`, `chain_id`, `contract_address` | | Portfolio value chart | `/general/chart` | `addresses`, `timerange` | | Single token chart | `/tokens/additional/charts` | `addresses`, `chain_id`, `contract_address` | ### Balance API https://business.1inch.com/portal/assets/docs-v2/apis/balance/introduction.md The 1inch Balance API provides real-time, accurate information about token balances and allowances across multiple chains. This API is essential for applications that need to track or display the assets held in web3 wallets and offers a simple integration into wallets, exchanges, and other dApps. By leveraging the Balance API, you can ensure that your users always have up-to-date details on their token holdings, allowances, and more. ## Supported Chains The 1inch Balance API supports a broad range of chains: | Supported Chains | Chain ID | | :--------------- | :------- | | Ethereum | 1 | | Solana | 501 | | Base | 8453 | | BNB | 56 | | zkSync | 324 | | Gnosis | 100 | | Optimism | 10 | | Cronos | 25 | | Polygon | 137 | | Monad | 143 | | Linea | 59144 | | Sonic | 146 | | Unichain | 130 | | Arbitrum | 42161 | | Avalanche | 43114 | | HyperEVM | 999 | | Arc | 5042 | | Robinhood | 4663 | ## Why use the 1inch Balance API? The 1inch Balance API offers several significant benefits: - **Wide selection of tokens**: supports any token on the most popular blockchain networks. - **Fast response time**: with response times under 400 ms, the Balance API is among the fastest on the market, providing nearly instant access to wallet balances and allowances. ## Integration possibilities The 1inch Balance API can be integrated into a variety of applications, making it versatile for many web3 use cases. It is especially useful in: - Wallets - Portfolio and token trackers - Exchanges and meta-aggregators - Majority of web3 applications :::info For workloads that require higher RPS, expanded quotas, or sustained performance, explore the available paid plans on the [**pricing page**](/pricing). For organizations with significant trading volumes or advanced requirements, [**complete this application**](/get-enterprise) to explore tailored Enterprise options. ::: ## API reference For detailed information about each endpoint, refer to the Balance API [OpenAPI section](./methods). https://business.1inch.com/portal/assets/docs-v2/apis/balance/quick-start.md ## Building a Token Balance Checker using 1inch Balance API In this tutorial, we will create a simple Python script to check the token balances of a given wallet address using the 1inch Balance API. We'll use the popular `requests` library to make API calls. ### Prerequisites: 1. Python (3.6 or higher) installed on your machine. 2. `requests` library. Install it using `pip install requests`. ### Step 1: Understanding the API Before we begin, let's briefly understand the 1inch Balance API endpoint we'll be using: - **Endpoint**: `https://api.1inch.com/balance` - **Method**: `GET` - **Parameters**: - `walletAddress`: The Ethereum wallet address for which we want to fetch token balances. ### Step 2: Implementing the Token Balance Checker Create a new Python file named `token_balance_checker.py` and write the following code: ```python # token_balance_checker.py import requests def get_token_balances(wallet_address): endpoint = f'https://api.1inch.com/balance/v1.2/1/balances/{wallet_address}' response = requests.get(endpoint, headers={'Authorization': f'Bearer YOUR-API-KEY'}) if response.status_code == 200: return response.json() else: print(f"Failed to fetch token balances. Error code: {response.status_code}") return None def main(): # Replace '0xYourWalletAddress' with the Ethereum wallet address you want to check wallet_address = '0xYourWalletAddress' token_balances = get_token_balances(wallet_address) if token_balances: print(f"Token balances for wallet address {wallet_address}:") for token, balance in token_balances.items(): print(f"{token}: {balance}") else: print("Token balance fetch failed. Please check your wallet address.") if __name__ == '__main__': main() ``` ### Step 3: Running the Token Balance Checker Save the script and run it using the command: ```bash python token_balance_checker.py ``` Replace `'0xYourWalletAddress'` in the script with the Ethereum wallet address you want to check. The script will then fetch and display the token balances for that wallet address. That's it! You have now built a simple Token Balance Checker using the 1inch Balance API. You can further expand this project to include more functionality, such as calculating the total value of tokens based on token prices, filtering out zero-balance tokens, and more. Remember to handle errors and implement security measures, such as input validation and user authentication, when building real-world applications. ### Token API https://business.1inch.com/portal/assets/docs-v2/apis/tokens/introduction.md The Token API retrieves comprehensive token information and allows users to search for token data using a token's name, address, or symbol. This feature ensures that your application or platform can access precise token details across various blockchain networks. Whether you're developing a new dApp, a data aggregation tool, or a financial platform, the Token API provides up-to-date and essential token information whenever required. This API is indispensable for wallets, explorers, portfolio trackers, and any application that needs reliable token data. ## Supported Chains | Supported Chains | Chain ID | | :--------------- | :------- | | Ethereum | 1 | | Solana | 501 | | Base | 8453 | | BNB | 56 | | zkSync | 324 | | Gnosis | 100 | | Optimism | 10 | | Cronos | 25 | | Polygon | 137 | | Monad | 143 | | Linea | 59144 | | Sonic | 146 | | Unichain | 130 | | Arbitrum | 42161 | | Avalanche | 43114 | | HyperEVM | 999 | | Arc | 5042 | | Robinhood | 4663 | ## Why use the Token API? The Token API offers several key advantages: - **Extensive token search capabilities**: supports searches by token name, address, or symbol, covering a broad range of tokens across popular blockchain networks. - **Reliable data accuracy**: ensures accurate and up-to-date token information to support a seamless user experience. - **High availability**: designed for robustness, the Token API performs efficiently, even under heavy query loads. ## Integration possibilities The Token API can be integrated into a variety of applications, making it a flexible solution for diverse use cases. It is especially beneficial for: - Price trackers - Block explorers - Wallets - Portfolio and token trackers - Exchanges and meta-aggregators :::info For workloads that require higher RPS, expanded quotas, or sustained performance, explore the available paid plans on the [**pricing page**](/pricing). For organizations with significant trading volumes or advanced requirements, [**complete this application**](/get-enterprise) to explore tailored Enterprise options. ::: ## API reference For detailed information about each endpoint, refer to the Token API [OpenAPI section](./methods). https://business.1inch.com/portal/assets/docs-v2/apis/tokens/quick-start.md In this tutorial, we will walk you through the process of using the 1inch Token API with Python and the `requests` library. We'll cover how to search for tokens, get detailed information about individual tokens, and obtain a list of supported tokens from the 1inch API. Now, let's get started! ## Step 1: Prerequisites ### Python Make sure you have Python installed on your system. If you don't, follow the [official instructions](https://www.python.org/). ### Requests library This script uses the `requests` library to make HTTP requests to the 1inch Token API. Install it by running: ```bash pip install requests ``` ### Set up environment variables To securely manage your API key, store it as an environment variable. For macOS: 1. Open the Terminal. 1. Add `API_KEY` variable to your shell configuration file. Replace `your_api_key` with your DevPortal API key: ```bash export API_KEY=your_api_key ``` 1. Save and reload your configuration file: ```bash source ~/.zshrc # For zsh (default for macOS) ``` ### Prepare for rate limits The script includes mechanisms to handle API rate limits. It introduces small delays between API calls using the time library. You don't need to install `time`, as it's part of Python's standard library. The `time.sleep(1)` function pauses the script for one second between calls, ensuring compliance with API restrictions. ### Organize imports Now, the opening part of your script should look something like this: ```python import requests # For making API requests import time # For introducing delays between requests (to respect API rate limits) import os # For accessing environment variables ``` ## Step 2: Define the API base URL and headers Next, we'll define the base URL and headers for the 1inch Token API. We'll use this URL to construct our API requests. Additionally, we make sure `API_KEY` is retrieved from the environment variable. ```python API_KEY = os.getenv("API_KEY") BASE_URL = "https://api.1inch.com/token" HEADERS = { "Authorization": f"Bearer {API_KEY}", "accept": "application/json" } ``` ## Step 3: Search for tokens To search for tokens, we'll use the `/v1.4/{chain_id}/search` endpoint. We'll provide a query in the name, symbol, or description of the token to search for matches. ```python def search_tokens(query, chain_id, limit=10, ignore_listed="false"): endpoint = f"{BASE_URL}/v1.4/{chain_id}/search" params = { "query": query, "limit": limit, "ignore_listed": ignore_listed } response = requests.get(endpoint, headers=HEADERS, params=params) if response.status_code == 200: return response.json() else: print(f"Failed to search tokens. Status code: {response.status_code}") return None ``` ## Step 4: Get detailed information about specific tokens To get detailed information about specific tokens, we'll use the `/v1.4/{chain_id}/custom/{addresses}` endpoint. We'll provide a list of token addresses for which we want to obtain information. ```python def get_tokens_info(chain_id, addresses): endpoint = f"{BASE_URL}/v1.4/{chain_id}/custom/{','.join(addresses)}" response = requests.get(endpoint, headers=HEADERS ) if response.status_code == 200: return response.json() else: print(f"Failed to get tokens info. Status code: {response.status_code}") return None ``` ## Step 5: Get all tokens info To get information about all tokens supported by 1inch on a specific network, we'll use the `/v1.4/{chain_id}` endpoint. ```python def get_all_tokens_info(chain_id, provider="1inch"): endpoint = f"{BASE_URL}/v1.4/{chain_id}" params = { "provider": provider, } response = requests.get(endpoint, headers=HEADERS, params=params) if response.status_code == 200: return response.json() else: print(f"Failed to get all tokens info. Status code: {response.status_code}") return None ``` ## Step 6: Get 1inch token list To get the list of 1inch tokens, we'll use the `/v1.4/{chain_id}/token-list` endpoint. ```python def get_1inch_token_list(chain_id, provider="1inch"): endpoint = f"{BASE_URL}/v1.4/{chain_id}/token-list" params = { "provider": provider, } response = requests.get(endpoint, headers=HEADERS, params=params) if response.status_code == 200: return response.json() else: print(f"Failed to get 1inch token list. Status code: {response.status_code}") return None ``` ## Step 7: Putting it all together Now, let's put it all together and use the functions to interact with the 1inch Token API. ```python if __name__ == "__main__": # Step 3: Search for tokens search_query = "1inch" chain_id = 1 # Replace with the chain ID you want to search on search_results = search_tokens(search_query, chain_id) print("Search Results:") print(search_results) # sleep one second because of RPS limit time.sleep(1) # Step 4: Get detailed information about specific tokens token_addresses = ["0x111111111117dc0aa78b770fa6a738034120c302"] # Replace with token addresses you want to query tokens_info = get_tokens_info(chain_id, token_addresses) print("Tokens Info:") print(tokens_info) # sleep one second because of RPS limit time.sleep(1) # Step 5: Get information about all tokens on a token list all_tokens_info = get_all_tokens_info(chain_id) print("All Tokens Info:") print(all_tokens_info) # sleep one second because of RPS limit time.sleep(1) # Step 6: Get 1inch token list token_list = get_1inch_token_list(chain_id) print("1inch Token List:") print(token_list) ``` That's it! You've successfully used the 1inch Token API with Python and the `requests` library to search for tokens, get detailed token information, and obtain a list of supported tokens from 1inch. Happy coding! ## Full script Below you can find the full script with consideration of the default RPS limit: ```python import requests import time import os API_KEY = os.getenv("API_KEY") BASE_URL = "https://api.1inch.com/token" HEADERS = { "Authorization": f"Bearer {API_KEY}", "accept": "application/json" } # for a list of providers check https://tokenlists.org/ def search_tokens(query, chain_id, limit=10, ignore_listed="false"): endpoint = f"{BASE_URL}/v1.4/{chain_id}/search" params = { "query": query, "limit": limit, "ignore_listed": ignore_listed } response = requests.get(endpoint, headers=HEADERS, params=params) if response.status_code == 200: return response.json() else: print(f"Failed to search tokens. Status code: {response.status_code}") return None def get_tokens_info(chain_id, addresses): endpoint = f"{BASE_URL}/v1.4/{chain_id}/custom/{','.join(addresses)}" response = requests.get(endpoint, headers=HEADERS ) if response.status_code == 200: return response.json() else: print(f"Failed to get tokens info. Status code: {response.status_code}") return None def get_all_tokens_info(chain_id, provider="1inch"): endpoint = f"{BASE_URL}/v1.4/{chain_id}" params = { "provider": provider, } response = requests.get(endpoint, headers=HEADERS, params=params) if response.status_code == 200: return response.json() else: print(f"Failed to get all tokens info. Status code: {response.status_code}") return None def get_1inch_token_list(chain_id, provider="1inch"): endpoint = f"{BASE_URL}/v1.4/{chain_id}/token-list" params = { "provider": provider, } response = requests.get(endpoint, headers=HEADERS, params=params) if response.status_code == 200: return response.json() else: print(f"Failed to get 1inch token list. Status code: {response.status_code}") return None if __name__ == "__main__": # Step 3: Search for tokens search_query = "1inch" chain_id = 1 # Replace with the chain ID you want to search on search_results = search_tokens(search_query, chain_id) print("Search Results:") print(search_results) # sleep one second because of RPS limit time.sleep(1) # Step 4: Get detailed information about specific tokens token_addresses = ["0x111111111117dc0aa78b770fa6a738034120c302"] # Replace with token addresses you want to query tokens_info = get_tokens_info(chain_id, token_addresses) print("Tokens Info:") print(tokens_info) # sleep one second because of RPS limit time.sleep(1) # Step 5: Get information about all tokens on a token list all_tokens_info = get_all_tokens_info(chain_id) print("All Tokens Info:") print(all_tokens_info) # sleep one second because of RPS limit time.sleep(1) # Step 6: Get 1inch token list token_list = get_1inch_token_list(chain_id) print("1inch Token List:") print(token_list) ``` ### NFT API https://business.1inch.com/portal/assets/docs-v2/apis/nft/introduction.md Get expanded data on users' NFTs. The NFT API is designed to meet the needs of NFT platform developers and provides direct streamlined access to on-chain vital NFT data. With a simple GET request, retrieve comprehensive information about any NFT--whether it's pixel art, a meme, or a unique collectible. Using just an NFT owner's address, developers can access detailed attributes of their assets, including image URLs, descriptions, associated contract details, and links to platforms like OpenSea. Queries can be customized with parameters like 'limit' and 'offset' to fit a variety of use cases, from lightweight data visualization to in-depth analytics. As NFTs continue to gain prominence as digital art, collectibles, and even financial assets, having an efficient way to interact with and retrieve data about them is crucial. Whether you're building an NFT marketplace, a personal collection tracker, or a digital art gallery, our NFT API is the ideal foundation to start. ## Supported Chains | Supported Chains | Chain ID | | :--------------- | :------- | | Ethereum | 1 | | Base | 8453 | | Gnosis | 100 | | Optimism | 10 | | Polygon | 137 | | Arbitrum | 42161 | | Avalanche | 43114 | ## Why use the NFT API? The NFT API offers several key advantages: - **Comprehensive NFT data**: retrieve a wide array of NFT attributes, including image URLs, descriptions, metadata, and contract details, all from a single API. - **Efficient querying**: customize requests with parameters like `limit` and `offset` to ensure seamless data retrieval, whether for simple lists or complex analyses. - **Cross-platform compatibility**: direct links to platforms like OpenSea make it easy to integrate NFT data across different marketplaces and applications. - **Optimized for large datasets**: designed to handle significant query volumes, ensuring stable performance even for high-traffic applications. ## Integration possibilities The NFT API can be integrated into various applications, making it highly versatile for different NFT-centric use cases. It is especially valuable for: - NFT marketplaces - Personal NFT collection trackers - Digital art galleries and virtual galleries - Wallets with NFT support - DeFi platforms leveraging NFTs as collateral - NFT data aggregators and analytics platforms :::info For workloads that require higher RPS, expanded quotas, or sustained performance, explore the available paid plans on the [**pricing page**](/pricing). For organizations with significant trading volumes or advanced requirements, [**complete this application**](/get-enterprise) to explore tailored Enterprise options. ::: ## API reference For detailed information about each endpoint, refer to the NFT API [OpenAPI section](./methods). https://business.1inch.com/portal/assets/docs-v2/apis/nft/quick-start.md ## Getting Started with the NFT API with React and Node.js The 'List NFTs' API demo will walk you through on how users can seamlessly retrieve and display a curated selection of non-fungible tokens (NFTs) from a specified collection or platform. ### Prerequisites: 1. Node.js and npm installed. 2. Basic knowledge of JavaScript, React, and Express.js. ### Steps: ### 1. Setting Up Node Backend 1.1. Create a new directory for the project: ```bash mkdir nft-collection && cd nft-collection ``` 1.2. Initialize a new Node.js project: ```bash npm init -y ``` 1.3. Install Express and Axios: ```bash npm install express axios ``` 1.4. Create a new file `api.js` and set up a basic Express server: ```javascript const express = require("express"); const axios = require("axios"); const cors = require("cors"); const path = require("path"); const app = express(); const PORT = 5000; const BASE_URL = "https://api.1inch.com/nft/v2/byaddress"; app.use(cors()); // To handle CORS issues when making requests to the front end // Serve static files from the React app app.use(express.static(path.join(__dirname, "nft-collection/build"))); // We will route all other requests to the nft-collection build app.get("*", (req, res) => { res.sendFile(path.join(__dirname, "nft-collection/build", "index.html")); }); app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); }); ``` 1.5. Add an endpoint to fetch NFTs (replace `API_KEY`): ```javascript const BASE_URL = "https://api.1inch.com/nft/v2/byaddress"; app.get("/fetchNfts", async (req, res) => { const address = req.query.address || "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; const limit = req.query.limit || 50; const offset = req.query.offset || 0; const chainIds = req.query.chainIds || 1; try { const constructedUrl = `${BASE_URL}?address=${address}&chainIds=${chainIds}&limit=${limit}&offset=${offset}`; const response = await axios.get(constructedUrl, { headers: { Authorization: `Bearer ${process.env.API_KEY}` } }); // Send the data from the API back to the client res.json(response.data); } catch (error) { console.error("Axios Error: ", error.response); res.status(500).json({ error: "Failed to fetch NFTs" }); } }); ``` ### 2. Setting Up React Frontend 2.1. Create a new React app: ```bash npx create-react-app client ``` 2.2. Navigate to the React app directory: ```bash cd client ``` 2.3. Install Axios: ```bash npm install axios ``` 2.4. Create a component `NFTList.js` inside the `src` directory: ```jsx import React, { useState, useEffect } from "react"; import { fetchNFTs } from "./api"; const NFTList = ({ address }) => { const [nfts, setNfts] = useState([]); useEffect(() => { const fetchData = async () => { try { const response = await fetchNFTs(address); setNfts(response.data.assets); } catch (error) { console.error("Error fetching NFTs:", error); } }; fetchData(); }, [address]); return (
{nfts.map((nft) => (
{nft.name}

{nft.name}

{nft.description}

))}
); }; export default NFTList; ``` 2.5. Import and use `NFTList` in `src/App.js`: ```jsx import React from "react"; import "./App.css"; import NFTList from "./NFTList"; function App() { return (

My NFT Collection

); } export default App; ``` ### 3. Running the Project 3.1. Start the Express server: ```bash node api.js ``` 3.2. In a new terminal, navigate to the `client` directory and start the React app: ```bash cd client npm start ``` Now, you can view your NFT collection visualization at `http://localhost:3000`. That's it! This is a basic setup and you can expand upon this by adding more features, error handling, and styling to get to production.
### Token Details API https://business.1inch.com/portal/assets/docs-v2/apis/token-details/introduction.md The Token Details API is a versatile set of endpoints designed to provide developers with in-depth insights about tokens and native tokens. This API empowers developers to access critical token data, historical price trends, and market performance metrics, streamlining the integration of token-related functionality into applications. Whether you're building a DeFi application, analytics dashboard, or wallet service, the Token Details API provides the tools you need to deliver a rich, data-driven user experience. ## Supported Chains The Token Details API provides support for the following chains: | Supported Chains | Chain ID | | :--------------- | :------- | | Ethereum | 1 | | Solana | 501 | | Base | 8453 | | BNB | 56 | | zkSync | 324 | | Gnosis | 100 | | Optimism | 10 | | Cronos | 25 | | Polygon | 137 | | Monad | 143 | | Linea | 59144 | | Sonic | 146 | | Unichain | 130 | | Arbitrum | 42161 | | Avalanche | 43114 | | HyperEVM | 999 | | Arc | 5042 | | Robinhood | 4663 | ## Why use the Token Details API? Token data and price history are essential for applications in the DeFi ecosystem to deliver accurate and insightful functionality. The Token Details API simplifies this by providing: - Comprehensive token metadata and performance metrics. - Historical price data to support analytics and trend visualization. - Reduced development effort by offering ready-to-use endpoints for token-related data. ## Use cases The Token Details API can be leveraged for a variety of use cases, including: - **DeFi dashboards**: display token details and price trends on analytics platforms. - **Wallet applications**: show detailed token information, including links to explorers and social media. - **Market analytics**: visualize historical price trends and analyze market performance. - **Token research**: provide links to white papers, research materials, and source code for deeper insights. - **Portfolio tracking**: enable users to monitor token performance and price changes. - **Custom charts**: create visualizations of historical token price movements. - **Token listings**: dynamically display token details, including descriptions and market data. ## Quickstart To start using Token Details API swiftly, refer to the [Quickstart guide](./quick-start). ## API reference For detailed information about each endpoint, refer to the Token Details API [OpenAPI section](./methods). https://business.1inch.com/portal/assets/docs-v2/apis/token-details/quick-start.md The Token Details API demo will walk you through how users can seamlessly retrieve and display token price changes from the past week. ## Prerequisites - Node.js and npm installed - Basic knowledge of JavaScript, React, and Express.js ### Set up environment variables To securely manage your API key, store it as an environment variable. For macOS: 1. Open the Terminal. 1. Add `API_KEY` variable to your shell configuration file. Replace `your_api_key` with your DevPortal API key: ```bash export API_KEY=your_api_key ``` 1. Save and reload your configuration file: ```bash source ~/.zshrc # For zsh (default for macOS) ``` ## Step 1: Initialization 1. Create a new directory for the project: ```bash mkdir token-details && cd token-details ``` 1. Initialize a new Node.js project: ```bash npm init -y ``` 1. Install Express, CORS, and Axios: ```bash npm install express cors axios ``` 1. Create a new file `api.js` and set up a basic Express server by pasting into it the following: ```javascript const express = require("express"); const axios = require("axios"); const cors = require("cors"); const path = require("path"); const app = express(); const PORT = 5001; app.use(cors()); // To handle CORS issues when making requests to the front end // Serve static files from the React app app.use(express.static(path.join(__dirname, "client/build"))); // Add endpoint handler here // We will route all other requests to the client build app.get("*", (req, res) => { res.sendFile(path.join(__dirname, "client/build", "index.html")); }); app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); }); ``` 1. Add an endpoint to fetch token prices: ```javascript const BASE_URL = "https://api.1inch.com/token-details/v1.0/charts/interval"; const CHAIN_ID = 1; // Eth app.get("/api/:tokeAddress/prices/:interval", async (req, res) => { const { tokeAddress, interval } = req.params; try { const constructedUrl = `${BASE_URL}/${CHAIN_ID}/${tokeAddress}?interval=${interval}`; const response = await axios.get(constructedUrl, { headers: { Authorization: `Bearer ${process.env.API_KEY}` } }); // Send the data from the API back to the client res.json(response.data.d); } catch (error) { console.error("Axios Error: ", error.response); res.status(500).json({ error: "Failed to fetch token price by interval" }); } }); ``` ## Step 2: Setting up React frontend 1. Create a new React application: ```bash npx create-react-app client ``` 2. Navigate to the React application directory and install React Charts: ```bash cd client npm i react-charts ``` 3. Create a component `TokenPrice.js` inside the `src` directory with the following content: ```javascript import React, { useState, useEffect } from "react"; import { Chart } from "react-charts"; const TokenPrice = ({ address, interval }) => { const [tokenPrices, setTokenPrices] = useState([]); const [isLoading, setLoading] = useState(false); const primaryAxis = React.useMemo(() => ({ getValue: (datum) => datum.date }), []); const secondaryAxes = React.useMemo(() => [{ getValue: (datum) => datum.price }], []); useEffect(() => { const fetchData = async () => { setLoading(true); const response = await fetch(`http://localhost:5001/api/${address}/prices/${interval}`); if (!response.ok) { console.log("Fetch token prices error", response); return; } const prices = await response.json(); setTokenPrices(prices); setLoading(false); }; if (isLoading) { return; } fetchData(); }, [address, interval]); if (isLoading) { return
Loading...
; } if (tokenPrices.length === 0) { return
No data available
; } return ( ({ date: new Date(d.t * 1000), price: d.v })) } ], primaryAxis, secondaryAxes }} /> ); }; export default TokenPrice; ``` 4. Import and use `TokenPrice` in `src/App.js`: ```javascript import "./App.css"; import TokenPrice from "./TokenPrice"; function App() { return (

Token Price

); } export default App; ``` ## Step 3: Running the project 1. Start the Express server: ```bash node api.js ``` 2. In a new terminal, navigate to the client directory and start the React app: ```bash cd client npm start ``` Now, you can view your 1inch token price over last week at http://localhost:3000. Described above is a basic setup and you can expand upon this by adding more features, error handling, and styling to get to production.
## Market Data ### Gas Price API https://business.1inch.com/portal/assets/docs-v2/apis/gas-price/introduction.md The 1inch Gas Price API provides real-time, accurate gas price data across various blockchains, enabling developers and users to make informed decisions on transaction costs. With ultimate interoperability, the API allows seamless integration into applications that require up-to-date gas prices. ## Supported Chains The Gas Price API provides support for the following chains: | Supported Chains | Chain ID | | :--------------- | :------- | | Ethereum | 1 | | Solana | 501 | | Base | 8453 | | BNB | 56 | | zkSync | 324 | | Gnosis | 100 | | Optimism | 10 | | Cronos | 25 | | Polygon | 137 | | Monad | 143 | | Linea | 59144 | | Sonic | 146 | | Unichain | 130 | | Arbitrum | 42161 | | Avalanche | 43114 | | HyperEVM | 999 | | Arc | 5042 | | Robinhood | 4663 | ## Why use the Gas Price API? The Gas Price API offers several key benefits: - **Up-to-date information**: provides the most current gas price data on various blockchains for your users. - **Fast response**: With a response time of less than 200 ms, the Gas Price API is one of the fastest solutions available. ## Integration possibilities The Gas Price API can be integrated into various applications, such as: - Wallets - Exchanges and meta-aggregators - Portfolio trackers - Trading and arbitrage bots Any user interface that needs to display gas prices can benefit from integrating the Gas Price API. :::info For workloads that require higher RPS, expanded quotas, or sustained performance, explore the available paid plans on the [**pricing page**](/pricing). For organizations with significant trading volumes or advanced requirements, [**complete this application**](/get-enterprise) to explore tailored Enterprise options. ::: :::tip For the canonical, machine-readable index of API versions (recommended for AI agents and automation), see [llms.txt](/llms.txt). ::: ## API reference For detailed information about each endpoint, refer to the Gas Price API [OpenAPI section](./methods). https://business.1inch.com/portal/assets/docs-v2/apis/gas-price/quick-start.md Here we're going to run through a full stack tutorial on the Gas Price API. As a background here is an short brief into the three parameters we will be getting as a response `baseFee`, `maxPriorityFeePerGas`, and `maxFeePerGas`. 1. **Base Fee**: - Introduced by EIP-1559, the base fee is an amount of ether that is burned (removed from circulation) for every transaction or smart contract execution. - The base fee is adjusted by the protocol with every block. Its purpose is to ensure that the Ethereum block (space for transactions) is neither too full nor too empty. If a block is more than 50% full, the base fee increases. If it's less than 50% full, the base fee decreases. - The base fee is burned, meaning it's not given to the miners but removed from the total ether supply. 2. **Max Priority Fee Per Gas** (often called Tip): - While the base fee is burned, miners still need an incentive to include your transaction in the block, especially when the network is busy. - This is where the "max priority fee" comes in. It's a tip that goes directly to the miner. If you provide a higher tip, miners are more likely to prioritize your transaction. - Unlike the base fee, the priority fee isn't algorithmically adjusted. It's set by the sender, and it represents how much they're willing to pay to prioritize their transaction. 3. **Max Fee Per Gas**: - This is the total maximum amount (in wei, the smallest unit of ether) a user is willing to pay per unit of gas. - It includes both the base fee and the priority fee (tip). - When a transaction is mined, the difference between the max fee per gas and the actual cost (base fee + tip) is refunded to the user. For more info on these fees and how they're calculated you can read this article [here](https://ethereum.org/en/developers/docs/gas/). To kick off the tutorial we're going to set up the backend with express js for the web server, and axios as an HTTP client to get the information. ## Backend 1. **Setup** ```bash mkdir gas-price-app cd gas-price-app npm init -y npm install express axios ``` 1. **server.js** ```javascript const express = require("express"); const axios = require("axios"); const path = require("path"); const app = express(); const PORT = 3000; // Serve static files from the React frontend app -> coming in the next part app.use(express.static(path.join(__dirname, "client/build"))); // Get the front end file by going to the root domain app.get("/", (req, res) => { res.sendFile(path.join(__dirname + "/client/build/index.html")); }); app.get("/gas-price", async (req, res) => { try { const response = await axios.get(""); res.json(response.data); } catch (error) { res.status(500).json({ message: "Error fetching gas prices" }); } }); app.listen(PORT, () => { console.log(`Server running on :${PORT}`); }); ``` ## Frontend For the frontend, we'll use React and we'll also install the HTTP client axios to get the endpoint we just created: 1. **Setup** ```bash npx create-react-app client cd client npm install axios, react ``` 2. **App.js** Replace the contents of `src/App.js` with: ```jsx import React, { useState, useEffect } from "react"; import axios from "axios"; function App() { const [gasPrice, setGasPrice] = useState({}); useEffect(() => { const fetchGasPrice = async () => { const response = await axios.get("/gas-price"); setGasPrice(response.data); }; fetchGasPrice(); }, []); return (

Ethereum Gas Prices

Base Fee: {gasPrice.baseFee}
{gasPrice.low && ( <>
Low: {gasPrice.low.maxPriorityFeePerGas}
Medium: {gasPrice.medium.maxPriorityFeePerGas}
High: {gasPrice.high.maxPriorityFeePerGas}
Instant: {gasPrice.instant.maxPriorityFeePerGas}
)}
); } export default App; ``` 3. Add proxy to `client/package.json`: ```json "proxy": "" ``` 4. Run both the backend and frontend: - In the `gas-price-app` directory, run: ```bash node server.js ``` - In the `gas-price-app/client` directory, run: ```bash npm run build npm start ``` When you navigate to `http://localhost:3000`, the frontend should now display the gas prices fetched from the 1inch API through our backend server. And that's it! With just a few lines of code, you've created a full stack application that displays Ethereum gas prices from the 1inch API.
### Spot Price API https://business.1inch.com/portal/assets/docs-v2/apis/spot-price/introduction.md The Spot Price API provides real-time token price data across multiple blockchains. This provides your users with accurate pricing information in a given chain's native currency. Whether you're building a new dApp, financial platform, or web3-based tool, this API ensures you have up-to-date and reliable price information when you need it. This API is an essential tool for wallets, exchanges, portfolio trackers, and any application that requires real-time data. ## Supported Chains The Spot Price API supports a wide range of chains: | Supported Chains | Chain ID | | :--------------- | :------- | | Ethereum | 1 | | Solana | 501 | | Base | 8453 | | BNB | 56 | | zkSync | 324 | | Gnosis | 100 | | Optimism | 10 | | Cronos | 25 | | Polygon | 137 | | Monad | 143 | | Linea | 59144 | | Sonic | 146 | | Unichain | 130 | | Arbitrum | 42161 | | Avalanche | 43114 | | HyperEVM | 999 | | Arc | 5042 | | Robinhood | 4663 | ## Why use the Spot Price API? The Spot Price API offers several key advantages: - **Wide selection of tokens**: supports any token across the most popular blockchain networks. - **Fastest response time**: with a response time under 300 ms, the Spot Price API is one of the fastest solutions on the market. ## Integration possibilities The Spot Price API can be integrated into a wide range of applications, making it a versatile tool for different use cases. It is particularly useful in: - Wallets - Portfolio and token trackers - Exchanges and meta-aggregators :::info For workloads that require higher RPS, expanded quotas, or sustained performance, explore the available paid plans on the [**pricing page**](/pricing). For organizations with significant trading volumes or advanced requirements, [**complete this application**](/get-enterprise) to explore tailored Enterprise options. ::: ## API reference For detailed information about each endpoint, refer to the Spot Price API [OpenAPI section](./methods). https://business.1inch.com/portal/assets/docs-v2/apis/spot-price/quick-start.md ## Getting Started the 1inch Spot Price API The 1inch Spot Price API allows you to retrieve on-chain prices of tradable tokens on the Ethereum network. All prices are presented in the native currency WEI. In this tutorial, we will cover how to interact with the API to get token prices using various endpoints. ### Prerequisites Before you begin, make sure you have the following: 1. A code editor to write and run the code (e.g., Visual Studio Code, PyCharm). 2. Python installed on your system. 3. Basic knowledge of Python programming and REST APIs. ### Step 1: Import Required Libraries To start, create a new Python script (e.g., `token_prices.py`) and import the necessary libraries, If you don't have requests make sure you install it by running `pip install requests` ```python import requests ``` ### Step 2: Get Prices for Whitelisted Tokens The first endpoint allows you to get prices for whitelisted tokens. These tokens are pre-defined and can be accessed without specifying any parameters. Let's implement a function to fetch these prices: Replace your API key with the one found [here](/applications) ```python def get_whitelisted_token_prices(): url = "https://api.1inch.com/price/v1.1/1" response = requests.get(url, headers={'Authorization': f'Bearer YOUR_API_KEY'}) if response.status_code == 200: prices = response.json() print("Prices for whitelisted tokens:") for token_address, price in prices.items(): print(f"{token_address}: {price}") else: print("Failed to fetch token prices.") ``` ### Step 3: Get Prices for Requested Tokens The second endpoint allows you to request prices for specific tokens. To do this, you need to pass an array of token addresses in the request body. Let's implement a function to get prices for requested tokens: ```python def get_requested_token_prices(tokens): url = "https://api.1inch.com/price/v1.1/1" payload = { "tokens": tokens } response = requests.post(url, headers={'Authorization': f'Bearer YOUR_API_KEY'}, json=payload) if response.status_code == 200: prices = response.json() print("Prices for requested tokens:") for token_address, price in prices.items(): print(f"{token_address}: {price}") else: print("Failed to fetch token prices.") ``` ### Step 4: Get Prices for Multiple Addresses The third endpoint allows you to get prices for multiple tokens at once. You need to pass multiple token addresses separated by commas in the URL. Let's implement a function to fetch prices for multiple addresses: ```python def get_prices_for_addresses(addresses): url = f"https://api.1inch.com/price/v1.1/1/{','.join(addresses)}" response = requests.get(url, headers={'Authorization': f'Bearer YOUR_API_KEY'}) if response.status_code == 200: prices = response.json() print("Prices for requested tokens:") for token_address, price in prices.items(): print(f"{token_address}: {price}") else: print("Failed to fetch token prices.") ``` ### Step 5: Test the Functions Now that we have implemented the functions, let's test them by calling each one: ```python if __name__ == "__main__": # Test get_whitelisted_token_prices get_whitelisted_token_prices() # Test get_requested_token_prices tokens_to_request = ["0x111111111117dc0aa78b770fa6a738034120c302"] get_requested_token_prices(tokens_to_request) # Test get_prices_for_addresses addresses_to_fetch = ["0x111111111117dc0aa78b770fa6a738034120c302", "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"] get_prices_for_addresses(addresses_to_fetch) ``` ### Step 6: Run the Script Save the script and run it using Python. You should see the prices for whitelisted tokens, prices for the requested tokens, and prices for the specified addresses displayed in the console. That's it! You have successfully used the 1inch Spot Price API to fetch on-chain token prices in Python. You can further integrate this functionality into your applications to get real-time token prices and make informed decisions while trading. ## Full script Here you can find the full script with consideration of the default RPS limit ```python import requests import time def get_whitelisted_token_prices(): url = "https://api.1inch.com/price/v1.1/1" response = requests.get(url, headers={'Authorization': f'Bearer YOUR_API_KEY'}) if response.status_code == 200: prices = response.json() print("Prices for whitelisted tokens:") for token_address, price in prices.items(): print(f"{token_address}: {price}") else: print("Failed to fetch token prices.") def get_requested_token_prices(tokens): url = "https://api.1inch.com/price/v1.1/1" payload = { "tokens": tokens } response = requests.post(url, headers={'Authorization': f'Bearer YOUR_API_KEY'}, json=payload) if response.status_code == 200: prices = response.json() print("Prices for requested tokens:") for token_address, price in prices.items(): print(f"{token_address}: {price}") else: print("Failed to fetch token prices.") def get_prices_for_addresses(addresses): url = f"https://api.1inch.com/price/v1.1/1/{','.join(addresses)}" response = requests.get(url, headers={'Authorization': f'Bearer YOUR_API_KEY'}) if response.status_code == 200: prices = response.json() print("Prices for requested tokens:") for token_address, price in prices.items(): print(f"{token_address}: {price}") else: print("Failed to fetch token prices.") if __name__ == "__main__": # Test get_whitelisted_token_prices get_whitelisted_token_prices() # sleep one second because of RPS limit time.sleep(1) # Test get_requested_token_prices tokens_to_request = ["0x111111111117dc0aa78b770fa6a738034120c302"] get_requested_token_prices(tokens_to_request) # sleep one second because of RPS limit time.sleep(1) # Test get_prices_for_addresses addresses_to_fetch = ["0x111111111117dc0aa78b770fa6a738034120c302", "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"] get_prices_for_addresses(addresses_to_fetch) ``` https://business.1inch.com/portal/assets/docs-v2/apis/spot-price/oracle-deployment-guide.md This section provides a comprehensive guide on deploying an oracle, including the nuances of script parameters and additional setup steps. Follow these steps to ensure accurate and reliable oracle deployment for price data retrieval. ### Step 1: Surveying DEX Liquidity 1. **Identify DEXes with Sufficient Liquidity:** Begin by surveying the network for Decentralized Exchanges (DEXes) that offer sufficient liquidity. This ensures the oracle can retrieve reliable and accurate price data. ### Step 2: Selection of DEXes 2. **Select Supported DEXes:** Choose DEXes that are supported by `SpotPriceAggregator` or are forks of supported protocols. Supported DEXes can be found in the `contracts/oracles/` directory of the project. ### Step 3: Network Configuration 3. **Configure the Network Settings:** - Skip this step if your network is supported. This can be checked by observing whether the network is mentioned (registered or not) during a test run, visible in the console output. This verification can be done also by reviewing the `registerAll` method in the [`Networks` class](https://github.com/1inch/solidity-utils/blob/master/hardhat-setup/networks.ts#L108-L128). If your network is listed there, it's considered supported, and no further action is required for registration in this step. - Update the [Hardhat settings file](https://github.com/1inch/spot-price-aggregator/blob/master/hardhat.config.js) to configure the network. - Utilize the `Networks` class from [solidity-utils](https://github.com/1inch/solidity-utils/blob/master/hardhat-setup/networks.ts) for network registration. - Example configuration snippet: ```javascript ... const { Networks } = require('@1inch/solidity-utils/hardhat-setup'); const net = new Networks(true, 'mainnet', true); net.register(your_network_name, networkId, process.env.YOURNETWORK_RPC_URL, process.env.YOURNETWORK_PRIVATE_KEY, etherscan_network_name, process.env.YOURNETWORK_ETHERSCAN_KEY); const networks = net.networks; const etherscan = net.etherscan; ... ``` ### Step 4: Environment Variables 4. **Set Environment Variables:** Define necessary environment variables in the `.env` file located at the project root. Include variables such as `YOURNETWORK_RPC_URL`, `YOURNETWORK_PRIVATE_KEY`, and `YOURNETWORK_ETHERSCAN_KEY` with appropriate values: - `YOURNETWORK_RPC_URL`: The RPC URL for accessing your network's node. This URL can support the HTTP header 'auth-key'. To use this header, append the header value to the URL using the `|` symbol. For example: `http://localhost:8545|HeaderValue`. This format allows you to authenticate requests to your node. - `YOURNETWORK_PRIVATE_KEY`: Your account's private key, which should be entered without the `0x` prefix. This key is used for deploying contracts and executing transactions on the network. - `YOURNETWORK_ETHERSCAN_KEY`: The API key for an Etherscan-like blockchain explorer that supports your network. This key is necessary for verifying and publishing your contract's source code. Ensure you register for an API key with a compatible explorer service for your network. ### Step 5: Deploying Oracles 5. **Deploy Oracles:** - Use the deploy script located at `deploy/commands/simple-deploy.js`. You can find a description of the script and how to use it in the [scripts description](https://github.com/1inch/spot-price-aggregator/blob/master/deploy/README.md). - Configure the `PARAMS` object for each protocol you wish to deploy an oracle for. The parameters include: - **contractName**: Name of the contract from the `contracts/oracles/` directory. - **args**: Arguments required by the contract (See contract's constructor). - **deploymentName**: A name for your deployment, which will be used to create a file in the `deployments/` directory. - Ensure the `skip` [flag](https://github.com/1inch/spot-price-aggregator/blob/master/deploy/commands/simple-deploy.js#L25) is set to `false` to proceed with deployment. - Example command for deployment: `yarn && yarn deploy `. ### Step 6: Deploying Wrappers 6. **Deploy Wrappers:** - Follow similar steps as step 5 to deploy necessary wrappers and `MultiWrapper`. You can find different wrappers in the `contracts/wrappers/` directory. After `MultiWrapper` is deployed, it will be possible to edit these lists of wrappers. ### Step 7: Deploying OffchainOracle 7. **Deploy OffchainOracle:** - Follow similar steps as step 5 to deploy the `OffchainOracle`. Make sure to include the deployed oracles (from step 5), `MultiWrapper` with wrappers (from step 6) and specifying the tokens you wish to use as connectors for price discovery. After `OffchainOracle` is deployed, it will be possible to edit these lists of oracles and connectors. ### Charts API https://business.1inch.com/portal/assets/docs-v2/apis/charts/introduction.md The 1inch Charts API is designed to provide easy retrieval and display of chart data for onchain assets, including ERC20 tokens, NFTs, and more. It fetches and visualizes historical market data, giving you the tools needed to make data-driven decisions. ## Supported Chains | Supported Chains | Chain ID | | :--------------- | :------- | | Ethereum | 1 | | Base | 8453 | | BNB | 56 | | Gnosis | 100 | | Optimism | 10 | | Cronos | 25 | | Polygon | 137 | | Monad | 143 | | Linea | 59144 | | Sonic | 146 | | Unichain | 130 | | Arbitrum | 42161 | | HyperEVM | 999 | | Arc | 5042 | ## Key features - **Retrieval of chart data**: enables fetching of historical and real-time chart data. - **Versatile data types**: supports data for ERC20 token pairs, NFTs, and more. - **Easy integration**: designed to be easily integrated into backend systems (Node.js) and frontend applications (React). - **Support for multiple networks**: currently available on Ethereum, Binance Smart Chain, and Polygon. ## Use cases in dApps ### NFT marketplaces - **Price history charts:** display historical price trends for NFTs - **Volume charts:** show trading volume over time, gain insight into the sentiment and liquidity of specific NFTs. ### Decentralized exchanges (DEXes) - **Token price charts:** provide users with historical and real-time price charts for any supported ERC20 token. - **Trading volume charts:** visualize trading volumes to help users understand market activity. ### Portfolio management tools - **Asset performance tracking:** allow users to track the historical performance of their assets--ideal for wallet applications as well! - **Portfolio analysis:** provide insights into the value changes and trends in a user's portfolio over time. ### Yield farming platforms - **Yield tracking:** show historical yields of multiple farming positions to help users make informed decisions. - **APY trends:** visualize the APY trends of an individual farm to help users assess its performance. ### DeFi analytics - **Market analysis:** provide comprehensive market analysis tools for users to study trends and make data-driven decisions. - **Liquidity pool charts:** show data for various liquidity pools, including volume, fees, and pool composition. :::info If you are an enterprise with significant API request volumes, [**complete this application**](/get-enterprise) to get in touch with us to explore Enterprise options tailored to your needs. ::: ## API reference For detailed information about each endpoint, refer to the Charts API [OpenAPI section](./methods). https://business.1inch.com/portal/assets/docs-v2/apis/charts/quick-start.md This guide will help you set up a project to retrieve and display chart data using the 1inch Charts API. You'll create a Node.js backend to handle API requests and serve data, and a React frontend to display this data in a user-friendly way. Once finished, you'll have built a simple yet powerful NFT collection viewer that you can expand upon and customize! ## Prerequisites - You'll need Node.js and npm installed. - Basic knowledge of JavaScript, React, and Express.js. ## Step 1: Set Up the Node Backend This backend will handle API requests, fetch data from the Charts API, and serve your frontend React application. ### 1.1. Create a new project directory ```bash mkdir nft-collection && cd nft-collection ``` ### 1.2. Initialize a new Node.js project This will create the `package.json' file. ```bash npm init -y ``` ### 1.3. Install necessary packages - **Express:** A web framework for Node.js, used to build the backend server. - **Axios:** A HTTP client, used to make API requests. - **CORS:** Middleware to handle Cross-Origin Resource Sharing, essential when the frontend and backend are on different ports. - **Path:** A Node.js module for working with file and directory paths. ```bash npm install express axios cors path ``` ### 1.4. Set up a basic Express server Create a file named `api.js` and add the following code. This will initialize an Express server to handle API requests and serve the React app (see Step 2 below). ```javascript const express = require("express"); const axios = require("axios"); const cors = require("cors"); const path = require("path"); const app = express(); const PORT = 3000; const BASE_URL = "https://api.1inch.com/charts/v1/..."; app.use(cors()); // Handle CORS issues // Serves static files from the React app app.use(express.static(path.join(__dirname, "client/build"))); // Endpoint to fetch chart data app.get("/charts", async (req, res) => { try { const response = await axios.get(BASE_URL, { headers: { Authorization: `Bearer ${process.env.API_KEY}` } }); res.send(response.data); } catch (error) { console.error("Error fetching charts data:", error); res.status(500).json({ error: "Failed to fetch charts data" }); } }); // Serve the React app for other routes app.get("*", (req, res) => { res.sendFile(path.join(__dirname, "client/build", "index.html")); }); app.listen(PORT, () => { console.log(`Server is running on http://localhost:${PORT}`); }); ``` ## Step 2: Setting Up React Frontend The frontend will display the data fetched from the backend API. ### 2.1. Create a new React app This creates a new React application for your frontend code. ```bash npx create-react-app client ``` ### 2.2. Navigate to the React app directory ```bash cd client ``` ### 2.3. Install Axios This will be used to make HTTP requests from the React app to the backend. ```bash npm install axios ``` ### 2.4. Create an NFT list component This component will handle fetching and displaying NFT data. Create a file with this directory and name: `src/NFTList.js`. Then add the following code: ```javascript import React, { useState, useEffect } from "react"; import axios from "axios"; const NFTList = ({ address }) => { // Initialize state to store NFT data const [nfts, setNfts] = useState([]); useEffect(() => { // Fetch NFT data from the server const fetchData = async () => { try { // Fetch NFTs for the given address const response = await axios.get(`/fetchNfts?address=${address}`); // Update the state with the fetched NFT data setNfts(response.data.assets); } catch (error) { console.error("Error fetching NFTs:", error); } }; fetchData(); }, [address]); // Refetch if the address changes return (
{/* Render the list of NFTs */} {nfts.map((nft) => (
{nft.name}

{nft.name}

{nft.description}

))}
); }; export default NFTList; ``` ### 2.5. Update the main App component Integrate the NFT list component into the main application. Add this to `src/App.js`: ```javascript import React from "react"; import "./App.css"; import NFTList from "./NFTList"; function App() { return (

My NFT Collection

); } export default App; ``` ## Step 3: Running the Project ### 3.1. Start the Express server ```bash node api.js ``` ### 3.2. Start the React app Navigate to the client directory and run the React development server: ```bash cd client npm start ``` You should now have a functional application for viewing NFT collections! While primitive, this framework can easily be built upon with features such as web3 wallet connections, auction mechanics, and more.
## Infrastructure ### Authentication https://business.1inch.com/portal/assets/docs-v2/apis/authentication.md The 1inch Business API supports two authentication methods: 1. **API keys** - Simple bearer tokens for direct integrations 2. **OAuth 2.1** - Standard protocol for user consent flows and server-to-server access Your credentials carry many privileges, so keep them secure. Do not share API keys or OAuth client secrets in publicly accessible areas such as GitHub or client-side code. --- ## API Keys You can view and manage your API keys under each of your [Applications](/applications). ### Usage - **Header**: `Authorization: Bearer {YOUR_API_KEY}` - **Query parameter**: `apiKey={YOUR_API_KEY}` ### REST API Call ```bash # Header authentication curl -X 'GET' \ 'https://api.1inch.com/swap/v5.2/1/tokens' \ -H 'accept: application/json' \ -H 'Authorization: Bearer YOUR_API_KEY' # Query parameter authentication curl -X 'GET' \ 'https://api.1inch.com/swap/v5.2/1/tokens?apiKey=YOUR_API_KEY' \ -H 'accept: application/json' ``` ### WebSocket ```bash # Header authentication wscat -c 'wss://api.1inch.com/web3/1' -H 'Authorization:Bearer YOUR_API_KEY' # Query parameter authentication wscat -c 'wss://api.1inch.com/web3/1?apiKey=YOUR_API_KEY' ``` ### Response Headers Every API response includes the following header: | Header | Description | | :------------- | :------------------------------------------------------------------------------------------------------ | | `X-Request-Id` | Unique request identifier. Save this value when troubleshooting -- it correlates with server-side logs. | **Tip:** When reporting an issue to support or using the MCP [`debug`](/portal/documentation/ai-integration/tools-reference#debug) tool, include the `X-Request-Id` from the failing response. It allows exact log lookup without time-range guessing. Note that logs are only available within the retention period defined by your subscription plan. --- ## OAuth 2.1 OAuth 2.1 provides an alternative to API keys, suitable for: - **Client Credentials** - Server-to-server integrations without user interaction - **Authorization Code + PKCE** - User-facing apps where the end user grants access OAuth access tokens are used the same way as API keys: `Authorization: Bearer {ACCESS_TOKEN}`. ### Obtaining OAuth Clients **Option A: Application Details (recommended)** 1. Go to [Applications](/applications) -> select an application -> **Details** 2. In the **OAuth Clients** section, click **Create OAuth client** 3. Choose grant type (Authorization Code or Client Credentials), set redirect URIs if needed, and create 4. Copy `client_id` and `client_secret` from the success dialog **Option B: Dynamic Client Registration (DCR)** Register a client via API: ```bash # Client Credentials - no application_id required curl -X POST 'https://api.1inch.com/oauth/register' \ -H 'Authorization: Bearer YOUR_PORTAL_ACCESS_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "client_name": "My Server Client", "redirect_uris": ["https://example.com"], "grant_types": ["client_credentials"] }' # Authorization Code - application_id required curl -X POST 'https://api.1inch.com/oauth/register' \ -H 'Authorization: Bearer YOUR_PORTAL_ACCESS_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "client_name": "My Web App Client", "redirect_uris": ["https://myapp.com/callback"], "grant_types": ["authorization_code", "refresh_token"], "application_id": "YOUR_APPLICATION_ID" }' ``` ### Discovery Fetch OAuth server metadata (RFC 8414). Replace the base URL with your auth server URL if it differs from the API gateway: ```bash curl 'https://api.1inch.com/.well-known/oauth-authorization-server' ``` Response includes `issuer`, `authorization_endpoint`, `token_endpoint`, `registration_endpoint`, `jwks_uri`, and supported grant types. Use these URLs for OAuth flows. ### Client Credentials Flow For server-to-server calls without user interaction: ```bash # 1. Exchange client credentials for access token curl -X POST 'https://api.1inch.com/oauth/token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET' # 2. Use access_token in API calls curl -X 'GET' 'https://api.1inch.com/swap/v5.2/1/tokens' \ -H 'Authorization: Bearer ACCESS_TOKEN' ``` ### Authorization Code Flow (with PKCE) For user-facing apps where the end user grants access: ```bash # 1. Generate PKCE code_verifier and code_challenge (S256) # code_verifier = 32 random bytes, base64url # code_challenge = base64url(SHA256(code_verifier)) # 2. Redirect user to authorization endpoint # GET /oauth/authorize?response_type=code&client_id=...&redirect_uri=...&state=...&code_challenge=...&code_challenge_method=S256 # 3. User approves on consent page, gets redirected to redirect_uri?code=...&state=... # 4. Exchange authorization code for tokens curl -X POST 'https://api.1inch.com/oauth/token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'grant_type=authorization_code&code=AUTH_CODE&redirect_uri=...&code_verifier=...&client_id=...&client_secret=...' # 5. Use access_token; use refresh_token when access_token expires curl -X POST 'https://api.1inch.com/oauth/token' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'grant_type=refresh_token&refresh_token=...&client_id=...&client_secret=...' ``` ### Supported Grant Types | Grant Type | Use Case | | -------------------- | ---------------------------- | | `client_credentials` | Server-to-server, no user | | `authorization_code` | User-facing apps with PKCE | | `refresh_token` | Refresh expired access token | ### Transaction Gateway API https://business.1inch.com/portal/assets/docs-v2/apis/transaction/introduction.md The Transaction Gateway API provides reliable on-chain transaction broadcasting, supporting both public and private modes. This enables developers to choose the most appropriate broadcasting method for their use case, whether for general public transactions or for sensitive, protected transactions in a private mempool. Ideal for applications focused on security and efficiency, the Transaction Gateway API is a powerful tool for wallets, exchanges, and virtually any on-chain application. ## Supported Chains | Supported Chains | Chain ID | | :--------------- | :------- | | Ethereum | 1 | | Solana | 501 | | Base | 8453 | | BNB | 56 | | zkSync | 324 | | Gnosis | 100 | | Optimism | 10 | | Cronos | 25 | | Polygon | 137 | | Monad | 143 | | Linea | 59144 | | Sonic | 146 | | Unichain | 130 | | Arbitrum | 42161 | | Avalanche | 43114 | | HyperEVM | 999 | | Arc | 5042 | | Robinhood | 4663 | ## Why use the Transaction Gateway API? The Transaction Gateway API offers several key advantages: - **Private broadcasting**: provides a secure way to broadcast transactions to a private mempool, offering protection against front-running and ensuring increased transaction privacy. - **Flexible broadcasting modes**: supports both public and private transaction broadcasting, allowing users to choose the method that best fits their needs. - **Reliable performance**: ensures robust transaction delivery, even under high-volume scenarios, making it suitable for fast-paced trading environments. ## Integration possibilities The Transaction Gateway API can be integrated into a wide range of applications, including: - Wallets - Exchanges and meta-aggregators - Traders and arbitrage bots - DeFi platforms and lending protocols - NFT marketplaces - Payment processors and merchant tools :::info For workloads that require higher RPS, expanded quotas, or sustained performance, explore the available paid plans on the [**pricing page**](/pricing). For organizations with significant trading volumes or advanced requirements, [**complete this application**](/get-enterprise) to explore tailored Enterprise options. ::: ## API reference For detailed information about each endpoint, refer to the Transaction Gateway API [OpenAPI section](./methods). https://business.1inch.com/portal/assets/docs-v2/apis/transaction/quick-start.md ## How to Use the 1inch Transaction Gateway API with Python and Request In this tutorial, we will guide you through the process of using the 1inch Transaction Gateway API with Python and the `requests` library. The Transaction Gateway API allows you to broadcast public and private transactions on the Ethereum network using raw transaction data. Before we begin, ensure you have Python installed on your system. If you don't have the `requests` library installed, you can install it using `pip` by running the following command: ```bash pip install requests ``` Now, let's get started with step-by-step instructions! ### Step 1: Import the `requests` Library First, we need to import the `requests` library, which allows us to make HTTP requests to the 1inch Transaction Gateway API. ```python import requests ``` ### Step 2: Define the API Base URL Next, we'll define the base URL for the 1inch Transaction Gateway API. We'll use this URL to construct our API requests. ```python base_url = "https://api.1inch.com/tx-gateway/v1.1/1" headers = { "Authorization": "Bearer [YOUR_API_KEY]", "accept": "application/json" } ``` ### Step 3: Broadcast Public Transaction To broadcast a public transaction, we'll use the `/broadcast` endpoint. We'll provide the raw transaction data as the request body. ```python def broadcast_public_transaction(raw_transaction): endpoint = "/broadcast" data = { "rawTransaction": raw_transaction } response = requests.post(base_url + endpoint, headers=headers, json=data) if response.status_code == 200: return response.json()["transactionHash"] else: print(f"Failed to broadcast public transaction. Status code: {response.status_code}") return None ``` ### Step 4: Broadcast Private Transaction To broadcast a private transaction, we'll use the `/flashbots` endpoint. Again, we'll provide the raw transaction data as the request body. ```python def broadcast_private_transaction(raw_transaction): endpoint = "/flashbots" data = { "rawTransaction": raw_transaction } response = requests.post(base_url + endpoint, headers=headers, json=data) if response.status_code == 200: return response.json()["transactionHash"] else: print(f"Failed to broadcast private transaction. Status code: {response.status_code}") return None ``` ### Step 5: Broadcast a Raw Transaction via RPC If you prefer to broadcast a raw transaction via RPC, we'll use the `/rpc` endpoint. Similar to the previous steps, we'll provide the JSON-RPC request as the request body. ```python def broadcast_raw_transaction_via_rpc(json_rpc_request): endpoint = "/rpc" data = json_rpc_request response = requests.post(base_url + endpoint, headers=headers, json=data) if response.status_code == 200: return response.json()["result"] else: print(f"Failed to broadcast transaction via RPC. Status code: {response.status_code}") return None ``` ### Step 6: Putting It All Together Now, let's put everything together and use the functions to broadcast transactions on the Ethereum network. ```python if __name__ == "__main__": # Replace with the raw transaction data you want to broadcast raw_transaction_data = "0xf86..." # Step 3: Broadcast public transaction transaction_hash_public = broadcast_public_transaction(raw_transaction_data) print("Transaction Hash (Public):", transaction_hash_public) # Step 4: Broadcast private transaction transaction_hash_private = broadcast_private_transaction(raw_transaction_data) print("Transaction Hash (Private):", transaction_hash_private) # Step 5: Broadcast transaction via RPC # Replace with the JSON-RPC request you want to use json_rpc_request_data = { "jsonrpc": "2.0", "id": "string", "method": "eth_sendRawTransaction", "params": [raw_transaction_data] } transaction_hash_rpc = broadcast_raw_transaction_via_rpc(json_rpc_request_data) print("Transaction Hash (RPC):", transaction_hash_rpc) ``` That's it! You have successfully used the 1inch Transaction Gateway API with Python and the `requests` library to broadcast public and private transactions, as well as transactions via RPC on the Ethereum network. Happy coding! ## Full Script Here you can find the full script with consideration of the default RPS limit ```python import requests import time base_url = "https://api.1inch.com/tx-gateway/v1.1/1" headers = { "Authorization": "Bearer [YOUR_API_KEY]", "accept": "application/json" } def broadcast_public_transaction(raw_transaction): endpoint = "/broadcast" data = { "rawTransaction": raw_transaction } response = requests.post(base_url + endpoint, headers=headers, json=data) if response.status_code == 200: return response.json()["transactionHash"] else: print(f"Failed to broadcast public transaction. Status code: {response.status_code}") return None def broadcast_private_transaction(raw_transaction): endpoint = "/flashbots" data = { "rawTransaction": raw_transaction } response = requests.post(base_url + endpoint, headers=headers, json=data) if response.status_code == 200: return response.json()["transactionHash"] else: print(f"Failed to broadcast private transaction. Status code: {response.status_code}") return None def broadcast_raw_transaction_via_rpc(json_rpc_request): endpoint = "/rpc" data = json_rpc_request response = requests.post(base_url + endpoint, headers=headers, json=data) if response.status_code == 200: print(response.json()) return response.json()["result"] else: print(f"Failed to broadcast transaction via RPC. Status code: {response.status_code}") return None if __name__ == "__main__": # Replace with the raw transaction data you want to broadcast raw_transaction_data = "0xf86..." # Step 3: Broadcast public transaction transaction_hash_public = broadcast_public_transaction(raw_transaction_data) print("Transaction Hash (Public):", transaction_hash_public) # sleep one second because of RPS limit time.sleep(1) # Step 4: Broadcast private transaction transaction_hash_private = broadcast_private_transaction(raw_transaction_data) print("Transaction Hash (Private):", transaction_hash_private) # sleep one second because of RPS limit time.sleep(1) # Step 5: Broadcast transaction via RPC # Replace with the JSON-RPC request you want to use json_rpc_request_data = { "jsonrpc": "2.0", "id": "string", "method": "eth_sendRawTransaction", "params": [raw_transaction_data] } transaction_hash_rpc = broadcast_raw_transaction_via_rpc(json_rpc_request_data) print("Transaction Hash (RPC):", transaction_hash_rpc) ``` ### Domains API https://business.1inch.com/portal/assets/docs-v2/apis/domains/introduction.md The Domains API enables seamless interaction with blockchain-based domain services, providing tools to resolve and retrieve domain information for addresses, as well as access provider data. Whether you're building a wallet, an identity service, or any web3 application, the Domains API offers essential functionality to enhance user experiences with blockchain domains. ## Supported Chains | Supported Chains | Chain ID | | :--------------- | :------- | | Ethereum | 1 | | Polygon | 137 | ## Use cases The Domains API can be applied for the following use cases: - **Wallets and identity applications**: enable users to interact with human-readable domain names instead of long blockchain addresses. - **Batch domain resolution**: efficiently resolve multiple addresses to domains for applications handling large datasets. - **Provider information**: integrate provider avatars and branding for enhanced visual experiences. ## Quickstart To start using Domains API swiftly, refer to the [Quickstart guide](./quick-start). ## API reference For detailed information about each endpoint, refer to the Domains API [OpenAPI section](./methods). https://business.1inch.com/portal/assets/docs-v2/apis/domains/quick-start.md --- sidebar_position: 2 --- ## Introduction This guide will walk you through retrieving data from providers such as ENS, LENS, and UD using the Domains API. ## Prerequisites - Node.js and npm installed on your machine - Basic knowledge of JavaScript, React, and Express.js ## Step-by-step guide ### Step 1: Initialization 1. Create a new directory for your project: ```bash mkdir domains && cd domains ``` 2. Initialize a new Node.js project: ```bash npm init -y ``` 3. Install Express, CORS, and Axios: ```bash npm install express cors axios ``` 4. Install dotenv for securely storing environment variables: ```bash npm install dotenv ``` Then create a new file called `.env` and add your DevPortal API key to it: ```bash API_KEY=YOUR_1INCH_API_KEY ``` ### Step 2: Import required libraries 1. Create a new file named `api.js` in your project directory. 1. Add the following code to import necessary packages and initialize your Express application: ```javascript const express = require("express"); const axios = require("axios"); const dotenv = require("dotenv"); const cors = require("cors"); const path = require("path"); // Load environment variables dotenv.config({ path: path.resolve(__dirname, ".env") }); const app = express(); app.use(cors()); ``` ### Step 3: Define API endpoints Add the following code to define your API endpoints: - Retrieve domain information ```javascript const BASE_URL = "https://api.1inch.com/domains/v2.0"; app.get("/api/:domain/info", async (req, res) => { const domain = req.params.domain; try { const constructedUrl = `${BASE_URL}/${domain}/lookup`; const response = await axios.get(constructedUrl, { headers: { Authorization: `Bearer ${process.env.API_KEY}` } }); res.json(response.data); } catch (error) { res.status(500).json({ error: "API error" }); } }); ``` - Reverse lookup for a domain: ```javascript app.get("/api/:domain/reverseinfo", async (req, res) => { const domain = req.params.domain; try { const constructedUrl = `${BASE_URL}/${domain}/reverse-lookup`; const response = await axios.get(constructedUrl, { headers: { Authorization: `Bearer ${process.env.API_KEY}` } }); res.json(response.data); } catch (error) { res.status(500).json({ error: "API error" }); } }); ``` - Retrieve provider data with avatars: ```javascript app.get("/api/:domain/get-providers-data-with-avatar", async (req, res) => { const domain = req.params.domain; try { const constructedUrl = `${BASE_URL}/get-providers-data-with-avatar`; const response = await axios.get(constructedUrl, { headers: { Authorization: `Bearer ${process.env.API_KEY}` } }); res.json(response.data); } catch (error) { res.status(500).json({ error: "API error" }); } }); ``` ### Step 4: Start the server 1. Add the following code to start your Express server and save the file: ```javascript const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); }); ``` 1. Run the server by using in your Terminal: ```bash node api.js ``` ## Response models The API returns structured JSON responses, which you can use to integrate with your application. - Response model: Provider data with avatar ```json { "result": { "protocol": "string", "domain": "string", "address": "string", "avatar": {} } } ``` - Response model: Domain or address information ```json { "result": { "protocol": "string", "address": "string", "checkUrl": "string" } } ``` ### Web3 RPC API https://business.1inch.com/portal/assets/docs-v2/apis/web3/overview.md The Web3 RPC API provides you with an easy way to maintain a constant, secure connection to the blockchain. These nodes provide reliable, real-time data access and interaction capabilities for executing transactions, monitoring block events, and querying network data efficiently. ## Supported features | Supported Chains | Chain ID | JSON-RPC | Websockets/Subscriptions | gRPC | full-node | archive-node | | :--------------- | :------- | :------: | :----------------------: | :--: | :-------: | :----------: | | Ethereum | 1 | [x] | [x] | [ ] | [x] | [x] | | Solana | 501 | [x] | [x] | [x] | [x] | [x] | | Base | 8453 | [x] | [x] | [ ] | [x] | [x] | | BNB | 56 | [x] | [x] | [ ] | [x] | [x] | | Monad | 143 | [x] | [x] | [ ] | [x] | [x] | | zkSync | 324 | [x] | [x] | [ ] | [x] | [x] | | Gnosis | 100 | [x] | [x] | [ ] | [x] | [x] | | Optimism | 10 | [x] | [x] | [ ] | [x] | [x] | | Polygon | 137 | [x] | [x] | [ ] | [x] | [x] | | Linea | 59144 | [x] | [x] | [ ] | [x] | [x] | | Sonic | 146 | [x] | [x] | [ ] | [x] | [x] | | Unichain | 130 | [x] | [x] | [ ] | [x] | [x] | | Arbitrum | 42161 | [x] | [x] | [ ] | [x] | [x] | | Robinhood | 4663 | [x] | [x] | [ ] | [x] | [x] | | Avalanche | 43114 | [x] | [x] | [ ] | [x] | [x] | | Cronos | 25 | [x] | [x] | [ ] | [x] | [x] | | HyperEVM | 999 | [x] | [x] | [ ] | [x] | [x] | | Arc | 5042 | [x] | [x] | [ ] | [x] | [x] | ## Supported API Interaction Methods - **JSON-RPC** -- Standard request-response protocol used for blockchain node interactions. - **WebSockets (Subscriptions)** -- Persistent, event-driven connections for receiving real-time updates. - **gRPC (Solana only)** -- High-performance, streaming-capable remote procedure calls for low-latency and real-time interactions on the Solana blockchain. :::info Solana gRPC Support For real-time Solana blockchain data streaming, check out our [Solana gRPC](./solana/solana-grpc.md) documentation using the Yellowstone protocol. ::: ## Getting Started Select a chain from the navigation to view available JSON-RPC methods for that network. Each chain has its own set of methods that can be called using JSON-RPC requests. ## Authentication For authentication details, see the [Authentication documentation](../authentication.md). ## Base URL The base URL for all Web3 RPC requests is:`https://api.1inch.com/web3/{chainId}` Where `{chainId}` is the chain ID of the network you want to interact with. ## Benefits - **Maximal uptime**: we have developed the most advanced internal systems for node health check/load balancing, which enable us to offer the maximum possible uptime. - **High load resistance**: thanks to our significant expertise, we can support high network load without sacrificing uptime. - **Support for multiple chains**: the Web3 RPC API supports different EVM networks, allowing you to interact with various blockchains through a single provider. - **Seamless integration**: easily integrate with your existing web3 projects using standard Web3 libraries. - **Included in all plans**: the Web3 RPC API, is included in all plans - **Full and archive node support**: choose between low-latency full nodes for real-time execution and archive nodes with complete historical blockchain state for analytics, auditing, and backtesting. :::info Enhanced API services are available as part of our paid plans. They are designed for enterprises with high-demand blockchain interactions or substantial operational scale, ensuring superior performance in terms of connectivity, throughput, and response times. ::: ## Example ### JSON-RPC over HTTP Here's a simple example using viem to make a JSON-RPC call: ```typescript import { createPublicClient, http } from "viem"; import { mainnet } from "viem/chains"; const client = createPublicClient({ chain: mainnet, transport: http("https://api.1inch.com/web3/1", { fetchOptions: { headers: { Authorization: `Bearer ${process.env.API_KEY}` } } }) }); // Get the current block number const blockNumber = await client.getBlockNumber(); console.log("Current block number:", blockNumber); ``` ### WebSocket subscriptions WebSocket connections stay open so you can subscribe to real-time blockchain events instead of polling. Connect to `wss://api.1inch.com/web3/{chainId}` and use the `eth_subscribe` method -- for example, the `newHeads` subscription pushes a notification for every new block header. Authenticate over WebSocket with the `apiKey` query parameter; browsers cannot set request headers on WebSocket connections. See the [Authentication documentation](../authentication.md) for the header alternative. #### Raw WebSocket Using [`wscat`](https://github.com/websockets/wscat) to subscribe to new block headers: ```bash wscat -c 'wss://api.1inch.com/web3/1?apiKey=YOUR_API_KEY' # Send a subscription request: > {"jsonrpc":"2.0","id":1,"method":"eth_subscribe","params":["newHeads"]} # The node replies with a subscription id: < {"jsonrpc":"2.0","id":1,"result":"0x9ce59a13059e417087c02d3236a0b1cc"} # Then streams a notification for every new block: < {"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0x9ce59a13059e417087c02d3236a0b1cc","result":{"number":"0x1b4","hash":"0xabc...","parentHash":"0xdef...","timestamp":"0x64e3f1a0"}}} ``` #### viem WebSocket client Swap the `http` transport for `webSocket` to open a persistent connection. The idiomatic way to follow new blocks is `watchBlocks`, which uses `eth_subscribe("newHeads")` under the hood when connected over WebSocket: ```typescript import { createPublicClient, webSocket } from "viem"; import { mainnet } from "viem/chains"; const client = createPublicClient({ chain: mainnet, transport: webSocket(`wss://api.1inch.com/web3/1?apiKey=${process.env.API_KEY}`) }); // watchBlocks subscribes via eth_subscribe("newHeads") over the WebSocket const unwatch = client.watchBlocks({ onBlock: (block) => { console.log("New block:", block.number, block.hash); } }); // Stop the subscription when you're done: // unwatch(); ``` To call `eth_subscribe` directly, use the transport's `subscribe` method. `onData` receives the raw JSON-RPC notification (block header fields are hex-encoded): ```typescript import { createPublicClient, webSocket } from "viem"; import { mainnet } from "viem/chains"; const client = createPublicClient({ chain: mainnet, transport: webSocket(`wss://api.1inch.com/web3/1?apiKey=${process.env.API_KEY}`) }); const { unsubscribe } = await client.transport.subscribe({ params: ["newHeads"], onData: (data) => { console.log("New block header:", data.result); }, onError: (error) => console.error(error) }); // Stop the subscription when you're done: // await unsubscribe(); ``` ## Potential use cases - **Decentralized applications (dApps)**: ensure your dApp has a stable and secure connection to the blockchain for tasks like reading smart contract data, sending transactions, and querying blockchain state. - **Decentralized exchanges (DEXes)**: maintain high uptime and reliable data feeds for trading operations, ensuring users can trade assets seamlessly. - **Trading bots**: achieve fast and reliable access to blockchain data to make timely trading decisions and execute trades. - **Wallets**: provide users with real-time balance updates, transaction statuses, and other important blockchain data. - **Analytics platforms**: collect and analyze blockchain data for insights and trends without worrying about node maintenance. ## API Reference For detailed information about each endpoint, refer to the Web3 RPC API [section](./ethereum/methods). https://business.1inch.com/portal/assets/docs-v2/apis/web3/solana/solana-grpc.md Yellowstone gRPC enables real-time Solana blockchain data streaming through gRPC interfaces. Stream accounts, transactions, blocks, and slots with low latency and efficient bidirectional communication. ## Getting Started ### Endpoint - `api.1inch.com:443` ### Authentication All requests must include an API key in the authorization header: ```bash Authorization: Bearer YOUR_API_KEY ``` Get your API key [here](/applications). ## Protocol Reference The protocol uses Protocol Buffers. View the [geyser.proto definition](https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/geyser.proto) for complete specifications. **Key Methods:** - `Subscribe` - Bidirectional stream for real-time updates - `GetLatestBlockhash`, `GetBlockHeight`, `GetSlot` - `IsBlockhashValid`, `Ping`, `GetVersion` ### Setup Download [geyser.proto](https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/geyser.proto) and [solana-storage.proto](https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/solana-storage.proto) to your project. ## Typescript Usage Examples #### Install dependencies ```bash npm install @grpc/grpc-js @grpc/proto-loader ``` ### Example 1: Simple GetSlot call ```typescript import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; // Load proto file const packageDefinition = protoLoader.loadSync("path/to/geyser.proto", { longs: String // Convert uint64 to strings for easier handling }); const geyserProto = grpc.loadPackageDefinition(packageDefinition).geyser; // Create client with TLS const client = new geyserProto.Geyser("api.1inch.com:443", grpc.credentials.createSsl()); // Set authorization metadata const metadata = new grpc.Metadata(); metadata.add("authorization", "Bearer YOUR_API_KEY"); // Call GetSlot client.getSlot({}, metadata, (err, response) => { if (err) { console.error("Error:", err); return; } console.log("Current slot:", response.slot); }); ``` ### Example 2: Subscribe to account updates ```typescript import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; const packageDefinition = protoLoader.loadSync("path/to/geyser.proto", { longs: String }); const geyserProto = grpc.loadPackageDefinition(packageDefinition).geyser; const client = new geyserProto.Geyser("api.1inch.com:443", grpc.credentials.createSsl()); const metadata = new grpc.Metadata(); metadata.add("authorization", "Bearer YOUR_API_KEY"); // Create bidirectional stream const stream = client.subscribe(metadata); // Send subscription request stream.write({ accounts: { client: { account: ["ACCOUNT_PUBKEY_HERE"], owner: [], filters: [] } } }); // Handle updates stream.on("data", (data) => { if (data.account) { console.log("Account update:", { slot: data.account.slot, lamports: data.account.account.lamports }); } }); stream.on("error", (err) => console.error("Error:", err)); stream.on("end", () => console.log("Stream ended")); ``` ## Go Usage Examples ### Install dependencies ```bash go get google.golang.org/grpc google.golang.org/grpc/credentials # generate bindings protoc --go_out=. --go-grpc_out=. geyser.proto ``` ### Example 1: Simple GetSlot call ```go package main import ( "context" "crypto/tls" "log" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" pb "your/proto/package" // Generated from geyser.proto ) func main() { // Connect with TLS creds := credentials.NewTLS(&tls.Config{ ServerName: "api.1inch.com", }) conn, err := grpc.Dial("api.1inch.com:443", grpc.WithTransportCredentials(creds)) if err != nil { log.Fatal(err) } defer conn.Close() client := pb.NewGeyserClient(conn) // Add authorization ctx := metadata.AppendToOutgoingContext( context.Background(), "authorization", "Bearer YOUR_API_KEY", ) // Call GetSlot resp, err := client.GetSlot(ctx, &pb.GetSlotRequest{}) if err != nil { log.Fatal(err) } log.Printf("Current slot: %d", resp.Slot) } ``` ### Example 2: Subscribe to account updates ```go package main import ( "context" "crypto/tls" "fmt" "io" "log" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" pb "your/proto/package" ) func main() { creds := credentials.NewTLS(&tls.Config{ ServerName: "api.1inch.com", }) conn, err := grpc.Dial("api.1inch.com:443", grpc.WithTransportCredentials(creds)) if err != nil { log.Fatal(err) } defer conn.Close() client := pb.NewGeyserClient(conn) ctx := metadata.AppendToOutgoingContext( context.Background(), "authorization", "Bearer YOUR_API_KEY", ) stream, err := client.Subscribe(ctx) if err != nil { log.Fatal(err) } // Send subscription request err = stream.Send(&pb.SubscribeRequest{ Accounts: map[string]*pb.SubscribeRequestFilterAccounts{ "client": { Account: []string{"ACCOUNT_PUBKEY_HERE"}, Owner: []string{}, Filters: []*pb.SubscribeRequestFilterAccountsFilter{}, }, }, }) if err != nil { log.Fatal(err) } // Receive updates for { update, err := stream.Recv() if err == io.EOF { break } if err != nil { log.Fatal(err) } if account := update.GetAccount(); account != nil { fmt.Printf("Account update: slot=%d, lamports=%d\n", account.Slot, account.Account.Lamports) } } } ``` ## Additional Resources - [Yellowstone gRPC GitHub Repository](https://github.com/rpcpool/yellowstone-grpc) - [Protocol Buffer Definition (geyser.proto)](https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/geyser.proto) - [Solana Geyser Plugins Documentation](https://docs.solana.com/developing/plugins/geyser-plugins) - [gRPC Documentation](https://grpc.io/docs/) ## Aqua https://business.1inch.com/portal/assets/docs-v2/aqua/overview/overview.md 1inch Aqua is a programmable shared liquidity layer. It targets a structural inefficiency in decentralized finance: most capital locked in traditional AMM pools sits idle on any given day, earning no fees while still bearing impermanent loss risk. Aqua's approach is to keep capital in maker-controlled wallets and let the same assets back multiple swap strategies at once, with no pooled custody and no capital migration required. **Positions and strategies are the same thing.** In the 1inch dApp you open a **position**. On-chain, and throughout the SDK, the API and these developer docs, that same object is called a **strategy**. The code identifiers are fixed and cannot be renamed: `strategyHash`, the `Strategy` struct/DTO, the `/strategies` endpoint and `ship()`/`dock()`. These docs use **strategy** so the prose matches the code you call. Read it as the dApp's **position**. Aqua is self-custodial. Tokens stay in the maker's own wallet under a revocable, per-chain, per-token allowance and move only when a taker fills a swap atomically. The protocol holds 0 tokens; virtual balances are an internal accounting counter in `Aqua.sol`. Smart-contract risk and approval risk still apply. --- ## The problem Aqua solves Traditional AMMs achieve O(1) swap complexity by forcing all liquidity providers into an identical strategy: same fee tier, same pricing curve, same parameters. This standardization was the price of computational simplicity. It also locks capital in a single pool, making it unavailable for governance, money-market collateral, or any other DeFi use while deployed. The consequences compound: - **Idle capital.** Most capital locked in AMM pools sits idle on any given day, earning no fees while still bearing impermanent loss risk. - **Capital fragmentation.** LPs must split finite capital across protocols, pairs, and price ranges, diluting depth everywhere. - **Locked utility.** Tokens committed to pools lose governance rights, staking rewards, and money-market composability. Aqua addresses all three by separating the accounting layer (who has how much) from the strategy layer (how to price and execute), while leaving custody entirely with the maker. --- ## How Aqua works ### Virtual balances Instead of taking custody, Aqua maintains a four-level nested mapping that tracks **virtual token balances**. These are authorizations, not deposits: ```solidity Maker Address -> Application Address -> Strategy Hash (bytes32) -> Token Address -> Balance ``` A maker calls `ship()` to allocate a virtual balance to a strategy. No tokens move; only the accounting record is created. The strategy hash is derived from the strategy's immutable parameters (`strategyHash = keccak256(abi.encode(strategy))`), making each strategy a fixed, auditable artifact. If parameters need to change, the maker docks the old strategy and ships a new one. ### The four lifecycle verbs | Operation | Who calls it | What it does | | --------- | ------------ | ----------------------------------------------------------------------------------------------- | | `ship()` | Maker | Allocates virtual balance to a strategy; no token transfer | | `dock()` | Maker | Revokes virtual balance instantly; no token transfer | | `pull()` | AquaApp | Decreases virtual balance; transfers tokens from the maker wallet to fill a swap | | `push()` | AquaApp | Increases virtual balance; returns tokens to the maker; auto-compounds into available liquidity | - `pull()` checks the maker's actual wallet balance at execution time and reverts if it is insufficient, so a fill is atomic with no partial fills, no bad debt, and no protocol insolvency. - `push()` immediately expands the strategy's usable balance, so earned tokens compound into productive liquidity without manual rebalancing. A taker fill is atomic: `swap() = pull() + push()` in one transaction, all-or-nothing. Takers call `swap()` directly, and `quote()` is a static call for simulation. Aqua has no order-level partial fills. ## Strategies are data, not contracts A strategy is an ABI-encoded structure, opaque bytes from Aqua's perspective, that encodes all pricing parameters (price curves, fee tiers, concentration ranges, and so on). The swap logic lives in an **AquaApp** contract, not in the strategy itself. This separation means a single AquaApp can serve many strategies, and makers never need to deploy per-strategy contracts. The full model is three layers: a **Program** (the instruction sequence) is encoded by SwapVM, the Aqua swap engine, into an **Order**, which is then registered on Aqua as a **Strategy** via `ship()`. Aqua treats the Order body as opaque bytes and only accounts for virtual balances; it does not interpret pricing logic. The chain is Program -> Order -> Strategy. --- ## The two core contracts Aqua is deployed on 13 EVM chains. Both core contracts are deployed at the same address on every chain by a nonce-synchronized deployer account (plain CREATE). | Contract | Address (all 13 chains) | Purpose | | ---------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------- | | `Aqua` (a.k.a. Aqua registry; deployed on-chain as `AquaRouter`) | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` | Virtual-balance registry; lifecycle operations (`ship`, `dock`, `pull`, `push`) | | `AquaSwapVMRouter` v1.0.2 | `0x111111338c5091e8440b67b168bae16a668ac0de` | The deployed SwapVM router; runs strategy programs at swap time | The Aqua registry is the settlement layer: it owns no tokens and enforces accounting invariants. It is deployed on-chain under the name `AquaRouter`, but it is the registry, not a swap router. `AquaSwapVMRouter` is the execution engine: it interprets the Program instructions inside a SwapVM Order at swap time. A single production router serves all strategy types; the strategy type is set by the instruction program inside the strategy bytes, not by the app address. See [Contract Addresses](/portal/documentation/aqua/reference/contract-addresses) for the full chain matrix, the KycNFT taker credential, and Foundry setup. --- ### The Shared Liquidity Ratio Aqua's capital-efficiency model is expressed as the **Shared Liquidity Ratio (SLR)**: the total notional liquidity a maker provisions across all of its strategies divided by the actual wallet equity backing it. ``` SLR = (total notional liquidity provisioned across all strategies) ------------------------------------------------------------- 1 (actual wallet equity backing it) ``` The SLR measures availability and capital efficiency, not leverage. Consider a maker with $1,000 of wallet equity. Because different strategies rarely need to fill at the same instant, the maker can make that same balance available to several strategies at once. If the balance backs three strategies, the total notional liquidity advertised is higher than the equity behind it, giving an SLR above 1. No tokens are borrowed and no collateral is created. Every fill is capped by the real wallet balance at execution time, because `pull()` reverts if the wallet cannot cover it. Any multiplier is therefore a ceiling on advertised depth, never a claim on funds the maker does not hold, and never a forecast of returns. Because DEX utilization is asynchronous and low at any single moment, different strategies activate at different times, letting the same capital service several strategies' occasional swaps without collision. Higher aggregate utilization means a given balance has more opportunities to earn swap fees across strategies. Swap fees are not guaranteed and do not offset impermanent loss in every market. --- ## Complexity trade-off Aqua deliberately accepts **O(n)** swap complexity, where n is the number of makers or strategies accessed to fill an order, in exchange for true strategy specialization. Traditional AMMs achieve O(1) by homogenizing all LP behavior; Aqua lets each maker run a distinct formula. This fragmentation from the taker's perspective is handled by the existing aggregation layer: DEX aggregators and solvers already maintain off-chain indexing and route across many sources. Aqua liquidity is **off-chain discoverable and on-chain executable**. Aggregators index available virtual balances off-chain, while settlement is trustless and atomic on-chain. --- ### Security model Aqua's security rests on three invariants: 1. **Self-custodial.** Aqua never holds tokens. Maximum exposure equals the ERC-20 allowance a maker explicitly grants, and that allowance is revocable per chain and per token. Smart-contract risk and approval risk still apply. 2. **Allowance-bounded access.** Each strategy's fills cannot exceed the maker's ERC-20 approval to Aqua, giving granular per-token and per-strategy risk control. One approval per chain can back many strategies on that chain. 3. **Atomic fills.** `pull()` reverts if the maker's real wallet balance is insufficient, so a swap completes fully or fails cleanly. Economic risks (impermanent loss, path-dependent losses in Dutch-auction strategies, and reduced fill availability when a wallet is underfunded) remain, but they are the same market-making risks present in any AMM. Aqua makes them transparent and bounded rather than introducing new protocol-level risk. There is no on-chain pause and no liquidation: an underfunded strategy simply stops filling until it is refunded or docked. Makers are advised to dock strategies that become chronically underfunded to avoid accumulating adverse price exposure during illiquid periods. ## Choose your path ### Ship your first strategy - Getting Started: install the SDK (`@1inch/swap-vm-sdk` and `@1inch/aqua-sdk`), compose an XYC program, ship liquidity, execute a swap, and dock. ### Understand the protocol before building - Core Concepts: maker, taker, strategy, virtual balances, and the lifecycle verbs. - Strategy: the three-layer model in full detail. ### Strategy author 1. Core Concepts: the mental model. 2. Strategy Lifecycle: ship / dock / pull / push and their events (`Shipped`, `Docked`, `Pulled`, `Pushed`, `Swapped`), documented on the Smart Contract reference. 3. Strategy Patterns: pick a recipe (XYC AMM, Limit Order, Dutch Auction, and more). 4. Build an AquaApp: implement swap logic in Solidity. ### Router or aggregator integration - SwapVM Instructions: the deployed opcode set (`Controls`, `XYCSwap`, `XYCConcentrate`, `Decay`, `Fee`, `PeggedSwap`, `Extruction`) and how the swap engine runs a program. - Smart Contract reference: the `AquaSwapVMRouter` and Aqua registry ABIs, addresses, and event schemas. --- ## Related - [Contract Addresses](/portal/documentation/aqua/reference/contract-addresses): contract addresses, supported chains, and Foundry setup. --- ## Shared Liquidity Ratio: a worked example The Shared Liquidity Ratio (SLR) measures how many times the same wallet balance is made available across strategies. It is an availability and efficiency figure, **not leverage**: nothing is borrowed, and any multiplier is a ceiling capped by the real wallet balance, never a forecast. ``` SLR = (total liquidity made available across all strategies) / (actual wallet balance backing it) // >= 1 Example: 1,000 USDC referenced by 3 strategies -> SLR = 3 The 3 strategies share the same 1,000 USDC; a fill on one draws from that balance, which then limits the others. No position can pull tokens the wallet does not hold. ``` ## 1inch Aqua vs a traditional pooled AMM | | Traditional pooled AMM | 1inch Aqua | | ------------ | --------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | Custody | Tokens deposited into the pool contract | Self-custodial: tokens stay in the maker's wallet under a revocable allowance; the protocol holds 0 tokens | | Capital | One deposit backs one pool | One wallet balance can back many strategies on a chain (Shared Liquidity Ratio) | | Pricing | Fixed curve per pool | Programmable: SwapVM opcodes (xyc / concentrated / pegged / custom via Extruction) | | Build effort | Deploy a new pool/pair contract | Compose existing opcodes (no new contract, Path B) or embed proprietary pricing (Path C) | | Exit | Withdraw liquidity | `dock()` clears the allocation; no tokens ever moved to the protocol | **Taker access gate (checked at swap time).** At launch every dApp strategy carries the Controls opcode `_onlyTxOriginTokenBalanceNonZero`, which reverts `TxOriginTokenBalanceIsZero` unless `balanceOf(tx.origin) > 0`. It is evaluated at _swap time, not ship time_, so a strategy can be live yet untradeable until a permitted taker holds the credential. Because it reads `tx.origin`, smart-contract wallets, multisigs and ERC-4337 bundlers cannot pass it as takers today. Permitted takers at launch are KYB-verified 1inch Resolvers. See [Access & resolvers](/portal/documentation/aqua/liquidity-layer/access-resolvers-and-pathfinder). https://business.1inch.com/portal/assets/docs-v2/aqua/overview/glossary.md Plain-language definitions of the terms used across these docs, with the 1inch Aqua angle. ## What is an AMM? An **automated market maker (AMM)** is a smart contract that prices swaps from a formula over on-chain reserves instead of an order book. In 1inch Aqua the AMM logic is a _program_ of SwapVM opcodes, and the reserves are virtual balances backed by tokens that never leave the maker's wallet. ## What is a DEX? A **decentralized exchange (DEX)** lets users swap tokens peer-to-contract without a custodian. Aqua is not a DEX brand; it is a self-custodial shared liquidity layer that any DEX or maker can build on. ## What is a programmable AMM? A **programmable AMM** lets builders define custom pricing/behaviour instead of a single fixed curve. SwapVM is Aqua's programmable AMM engine (an alternative to Uniswap v4 hooks and Algebra plugins) -- see [the comparison](../swapvm/swapvm-vs-uniswap-v4-hooks-vs-algebra). ## What is concentrated liquidity? **Concentrated liquidity** puts a maker's liquidity into a bounded price range for deeper depth and higher fee capture inside that range. In Aqua it is the `XYCConcentrate` opcode / the concentrated strategy archetype. ## What is shared liquidity? In Aqua, one wallet balance can back many strategies on a chain at once (the [Shared Liquidity Ratio](./introduction)) -- availability and efficiency, not leverage; tokens are never borrowed. ## Key terms | Term | Meaning | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **SwapVM** | The Aqua swap engine that runs a strategy's opcode program inside `AquaSwapVMRouter`. | | **Opcode / instruction** | A unit of SwapVM logic (pricing, fee, access, control flow). The deployed Aqua set: Controls, XYCSwap, XYCConcentrate, Decay, Fee, PeggedSwap, Extruction. | | **Position** | The 1inch dApp's name for a **strategy** -- the liquidity you open in the Aqua app. On-chain and throughout the SDK, the API and these developer docs it is a **strategy**; the code identifiers keep that name (`strategyHash`, the `/strategies` endpoint, the `Strategy` DTO). | | **Strategy** | The on-chain object registered by `ship()` (identified by `strategyHash`). Shown as a "position" in the dApp. | | **Maker / taker** | Maker = liquidity provider (permissionless). Taker = swapper (gated at launch via the KycNFT). | | **Virtual balance** | An internal counter in `Aqua.sol`; not a deposit. Tokens stay in the wallet. | | **Extruction** | The opcode that delegates pricing to an external non-upgradeable contract (Path C). | | **KycNFT (RES)** | The taker access credential held by KYB-verified 1inch Resolvers. | https://business.1inch.com/portal/assets/docs-v2/aqua/overview/capability-status.md What you can do on 1inch Aqua today. This is current on-chain state, not a forward-looking plan. | Capability | Status | Notes | | ------------------------------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Maker: `ship()` / `dock()` a strategy | **Available** | Permissionless on all 13 chains; self-custodial. | | Maker: build strategies with the SDK | **Available** | `@1inch/aqua-sdk` + `@1inch/swap-vm-sdk`. | | Deployed opcode set (Controls, XYCSwap, XYCConcentrate, Decay, Fee, PeggedSwap, Extruction) | **Available** | Opcodes outside this set are not executable: a reserved gap index is a no-op, an out-of-range index reverts `Panic(0x32)` (no named runtime error). | | Taker: `swap()` against a strategy | **Gated** | Default-deny at launch: the tx.origin must hold the `KycNFT`. Permitted takers are KYB-verified 1inch Resolvers. | | Smart-account / 4337 takers | **Not supported** | The launch gate reads `tx.origin`. | | Hosted APIs, MCP, x402, private-mempool broadcast | **1inch Business** | KYB/KYC-gated, payable. See [API Introduction](../api/introduction). | :::note Aqua is live: strategies are routed and filled on-chain. A strategy is fillable only by takers that hold the `KycNFT` credential, which must currently be requested from 1inch via the 1inch Business Portal. ::: https://business.1inch.com/portal/assets/docs-v2/aqua/overview/changelog.md Contract and docs versions. These docs were **last reviewed against source on 2026-07-08** (`1inch/aqua`, `1inch/swap-vm`, `1inch/sdks`); **contract addresses re-verified on-chain on 2026-07-29** after the 2026-07 vanity-address redeployment (registry 2026-07-19, router 2026-07-26), and **source provenance re-verified against GitHub and Blockscout on 2026-07-29** (`v1.0.2` tag, SDK 0.3.0/0.4.0 constants). > **Status note (2026-07-29).** The vanity-address redeployment announced on 2026-07-17 **has landed**: canonical registry `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a`, canonical router `0x111111338c5091e8440b67b168bae16a668ac0de`. The manifest below describes the live vanity deployment; the 2026-07-16 pair moved to the superseded arrays. ## Contracts | Component | Version | Address / note | | ---------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | AquaSwapVMRouter | **v1.0.2** (current) | `0x111111338c5091e8440b67b168bae16a668ac0de`, deployed 2026-07-26 (Ethereum block 25618917); on-chain: EIP-712 name `1inch SwapVM v1.0`, version `1.0.2` (string), `AQUA()` = `0x1111113c...a90a`. Built from the **`v1.0.2` git tag** (cut 2026-07-26, commit `32c687c2`) -- adds best-effort protocol-fee collection and `ProtocolFeeSkipped` | | AquaSwapVMRouter | superseded | `0x016b41...b070` (2026-07-16, v1.0.1-source recompile); `0x1111113db0...c0de` (first vanity router, 2026-07-19-26); `0x3c4758...790e` (v1.0.1, 2026-06-16); `0xa5368d...331a` (Robinhood CREATE3); `0xdfd05f...06fa` (no KYC opcode); `0x8fdd04...958f` (older 28-opcode build) -- do not target | | Aqua registry | no signing domain | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` (vanity, 2026-07-19; same bytecode as the 2026-07-16 registry). **No EIP-712/ERC-5267 domain** -- `eip712Domain()` reverts. Superseded: `0xe8026b...139b` (universal, 2026-07-16), `0x4a055a...a580` (original 12 chains), `0x7c2d4a...ec86` (Robinhood CREATE3) | | SDK | `@1inch/aqua-sdk` 0.3.0 `@1inch/swap-vm-sdk` 0.4.0 | **Current releases carry the canonical vanity addresses** (verified from the published packages, 2026-07-29). Releases 0.1.1 / 0.2.2 still return superseded addresses -- upgrade or pin from [Verified contract addresses](/portal/documentation/aqua/reference/verified-contract-addresses). Strategy bytecode is opcode-table-versioned. | ## Docs - **2026-07-29 -- Vanity redeployment recorded; SDKs and API caught up.** The redeployment announced in entry (c) landed in two steps (Ethereum data, per Blockscout creation transactions): registry `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` deployed **2026-07-19** (block 25567141, same bytecode as `0xe8026b...139b`), and the current router `0x111111338c5091e8440b67b168bae16a668ac0de` deployed **2026-07-26** (block 25618917), superseding a first vanity router `0x1111113db0e0ef9d0e3a50d5f094a3a57a26c0de` (2026-07-19, block 25567144). The vanity addresses are created through a deployer factory (`0x71481C...abe7`, method `deploy`, sender EOA `0x0BD61d...3A2`) -- unlike the plain-`CREATE` 2026-07-16 generation. **Source provenance correction:** `1inch/swap-vm` now has a **`v1.0.2` git tag** (cut 2026-07-26, commit `32c687c2`), retiring entry (b)'s "no v1.0.2 tag" finding; relative to v1.0.1 it changes only `src/instructions/Fee.sol` (best-effort protocol-fee collection + new `ProtocolFeeSkipped(bytes32,address,address,uint256)` event, topic0 `0x0b2957...6e97`, confirmed in the Blockscout-verified deployed source), leaving `ISwapVM` and `AquaOpcodes.sol` unchanged -- the 5-arg `quote`/`swap` surface and the opcode table carry over. On-chain checks (Ethereum): router `AQUA()` = the vanity registry, `eip712Domain()` = `1inch SwapVM v1.0` / `1.0.2`, `owner()` = `0x4134e6...C352` for both contracts; both source-verified on Blockscout. **SDK releases `@1inch/aqua-sdk` 0.3.0 and `@1inch/swap-vm-sdk` 0.4.0 carry the canonical vanity constants** (verified from the published packages) -- the "SDKs stale" caveat from entries (a)/(c) is retired. The **Aqua API is live** (`https://api.1inch.com/aqua`, e.g. `GET /v1.0/strategies/opened`; 401 without a key). Still stale: both repo READMEs. Sepolia: the vanity registry is live, the vanity router **not yet** (2026-07-29). Per-chain verification beyond Ethereum + Base spot checks has not been re-run for this generation. Updated pages: Contract Addresses, Verified Contract Addresses, Events & Interfaces, SDK overview, API introduction/quick start, this Changelog. - **2026-07-17 (d) -- Deployment report ingested; ownership and verification finalized.** The PT1-690 deployment report (pipeline `aqua-redeploy` / `prod-v1.0.2`, preset `aqua v1-r2`, executed 2026-07-16 16:28-16:58 UTC) is published at 16/07/2026 update - Aqua + SwapVM v1.0.2 universal redeploy. It confirms: **owner**`0x4134e66d52EfC4C77DD8Ccc952D87b9E92E0C352` on every chain for both registry and router -- an **interim EOA**, with the final ownership transfer to occur once everything is set up and working; constructor `name`/`version` = `1inch SwapVM v1.0` / `1.0.2` on every chain; the aggregation executors landed at the same addresses on all chains with **per-chain constructor args** (plain-`CREATE` address equality does not imply identical init code), and the protected executor executes only commands signed by `AGGREGATION_EXECUTOR_SIGNER 0x4447696e7878c0fa2c581768f04c3871a9a57b76`; executors are deliberately never source-verified (policy). **Aqua and the router are source-verified on all chains** (in-pipeline everywhere except zkSync Era and the Avalanche registry, completed manually afterwards). The same four contracts are also live on **Sepolia (11155111)** -- testnet, not part of the production chain set. Repo READMEs will be updated after the final (vanity-address) deployment. - **2026-07-17 (c) -- Consumer sync confirmed; vanity-address redeployment announced.** Backend confirms the 2026-07-16 addresses are **in sync across all 1inch platforms** -- dApp, aqua-api, swap-builder-api, swap-api and FUSE -- so the "consumers are still being repointed" caveat from entries (a)/(b) is retired (published npm SDKs remain on the pre-2026-07-16 set). Backend also announced a **further redeployment of Aqua and SwapVM to vanity addresses** (final addresses not yet available as of 2026-07-17): the registry/router addresses in this manifest will be superseded when it lands, and the address pages carry a dated status note until then. The EIP-712 domain-name findings from entry (b) are with the backend team for the SDK-side fix. - **2026-07-17 (b) -- Source-verification corrections** (GitHub review of `1inch/swap-vm`, `1inch/aqua`, `1inch/sdks`, committed broadcasts, and on-chain state). Four corrections to the entry below and to the address pages: **(1) Deployment mechanism** -- the universal addresses are produced by plain `CREATE` from the nonce-synchronized deployer EOA `0x72B4736F6e482DB07C4F3d7d3b90A24b2FedBe7a` (broadcast `transactionType: CREATE`, `to: null`; creator confirmed on chains 1, 324, 4663), **not** `CREATE2` as first recorded; CREATE3 was used only for the superseded June Robinhood-only pair. Under plain `CREATE` the shared address does not commit to init code -- address equality across chains does not prove identical bytecode/constructor args; verify per chain. **(2) Source provenance** -- there is **no v1.0.2 git tag**; the deployed v1.0.2 router is the v1.0.1 tag source (`b6e4f971`, = `release/1.0.1` tip) recompiled with new constructor args. The unreleased `main` (`19b4d6b`, post-PR-#154) carries a renumbered opcode enum (no reserved gaps, `UnknownOpcode(uint256)` revert) that is **not deployed** -- never derive the live opcode table from `main`. **(3) Registry signing domain withdrawn** -- the registry has **no EIP-712/ERC-5267 domain** (no `EIP712` base in `1inch/aqua` src; `eip712Domain()` reverts on-chain); the earlier `AquaRouter`/v`1` (integer) claim was wrong. Only the router has a signing domain. **(4) Opcode-table claims scoped** -- reserved gaps `[0..9]`/`[22..26]` and `Panic(0x32)` beyond-table behaviour are **confirmed for the deployed v1.0.1-source builds** (v1.0.1 and the v1.0.2 recompile) and are false on `main`. Adjacent stale surfaces flagged for repo owners: swap-vm-sdk `constants.ts` comment names the domain `AquaSwapVMRouter` (actual `1inch SwapVM v1.0`); swap-vm-sdk 0.2.x listed Robinhood's router as `0x3c4758...` when it was `0xa5368d...`; both SDK READMEs still show Unichain as `1301` (actual `130`); both contract-repo README address tables remain stale. Per-chain deploy configs (pipelines `swap-vm-4-aqua`, `full-deploy-factory3`) live in internal CI, not GitHub, so per-chain owner values remain unverified beyond Ethereum. - **2026-07-17 (a)** -- **Universal redeployment recorded.** On 2026-07-16 Aqua was redeployed on **all chains** to one universal address set, ending Robinhood's chain-specific deployment: registry `0xe8026bf31e58b738647319362581ab11be92139b`, AquaSwapVMRouter **v1.0.2** `0x016b417bc933370f5eacc40b1d58b015ac72b070` (both Sourcify exact-match verified 2026-07-16). _[Correction 2026-07-17 (b): this entry originally said "universal_ `CREATE2` address set"; the mechanism is plain `CREATE` from a nonce-synchronized deployer EOA.] The dependent 1inch aggregation executors were redeployed alongside (simple `0xd7d7FE53A5FC886e1B69c7213Fb0B24ccE1511a3`, protected `0xE3B94BF84310fDcec27A219cA0923F46D7137DC7` -- 1inch routing infrastructure, not part of the Aqua contract surface). `KycNFT` unchanged (`0x26FF...a468`). **Robinhood Chain (4663) joined the Aqua chain set (now 13 chains).** Old registry/routers moved to the superseded list; strategies shipped there remain active until makers re-ship. Consumer services (routing, APIs, SDKs) are being repointed -- published SDK releases still return superseded addresses. _[Correction 2026-07-17 (c): platform repointing completed -- dApp, aqua-api, swap-builder-api, swap-api and FUSE confirmed in sync; only the npm SDKs remain on the old set.]_ **EIP-712 correction:** the router domain name is `1inch SwapVM v1.0` (verified constructor arg for both v1.0.1 and v1.0.2), not `SwapVMRouter` as stated in the 2026-07-08 review. Updated pages: Contract Addresses, Verified Contract Addresses, Supported chains, this Changelog. - **2026-07-08** -- Source-verified Events, Encoding, opcode table, Pegged curve, per-chain deploy blocks; SDK browser + raw-ABI; Access & resolvers; Risks; Data & analytics; comparison, glossary, capability pages. Terminology and trade->swap pass. Rulebook D1/D2 (external docs describe only current state; APIs/MCP/x402 -> 1inch Business). ## Version manifest (machine-readable) The block below pins every moving part of the deployed **v1.0.2** release -- router, registry and `KycNFT` addresses, the chain set, the opcode-table binding, and the exact SDK package versions -- in one JSON object that indexers and CI can diff against. It restates values from the [Verified contract addresses](/portal/documentation/aqua/reference/verified-contract-addresses), [Supported chains](/portal/documentation/aqua/liquidity-layer/supported-chains) and [opcode](/portal/documentation/aqua/swapvm/opcode-gallery) pages; those pages remain the prose source of truth. All three contracts share one address on every one of the 13 chains, but that is an operational convention, not a guarantee: the addresses come from plain `CREATE` by a nonce-synchronized deployer EOA and do not commit to init code. **Verify each address on-chain per chain before pinning.** The manifest carries no cross-chain semantics: each chain is an independent deployment with its own registry state and its own KYC set. ```solidity { "manifestVersion": "2026-07-29", "release": "v1.0.2", "addressModel": "shared-address-across-chains", "vanityRedeployment": { "announced": "2026-07-17", "landedRegistry": "2026-07-19 (Ethereum block 25567141)", "landedRouter": "2026-07-26 (Ethereum block 25618917; supersedes the 2026-07-19 first vanity router 0x1111113db0e0ef9d0e3a50d5f094a3a57a26c0de)", "status": "live" }, "deployment": { "mechanism": "deployer factory (vanity generation); plain CREATE from 0x72B4736F6e482DB07C4F3d7d3b90A24b2FedBe7a (2026-07-16 generation)", "factory": "0x71481C3B9C6FBa3066AE84961EA22378A80cabe7 (Ethereum; method deploy, sender 0x0BD61d605C64A857C3D94779aEf7cA295702b3A2)", "note": "address equality does not commit to init code - verify bytecode and constructor args per chain" }, "router": { "name": "AquaSwapVMRouter", "version": "1.0.2", "eip712": { "name": "1inch SwapVM v1.0", "version": "1.0.2", "type": "string" }, "address": "0x111111338c5091e8440b67b168bae16a668ac0de", "deployedAt": "2026-07-26 (Ethereum; per-chain creation blocks differ)", "owner": "0x4134e66d52EfC4C77DD8Ccc952D87b9E92E0C352 (on-chain owner() check, Ethereum, 2026-07-29; interim EOA - final ownership transfer pending)", "source": "1inch/swap-vm v1.0.2 tag (commit 32c687c2, cut 2026-07-26; adds Fee.sol best-effort protocol-fee collection + ProtocolFeeSkipped event)" }, "registry": { "name": "Aqua", "onChainName": "AquaRouter", "eip712": null, "eip712Note": "no EIP-712/ERC-5267 domain; eip712Domain() reverts - never typed-data sign against the registry", "address": "0x1111113ccf1426a8e30e2bff5e005d929bf6a90a", "deployedAt": "2026-07-19 (Ethereum; same bytecode as the 2026-07-16 registry)" }, "kycNFT": { "name": "KycNFT", "symbol": "RES", "address": "0x26FFc7D378E8e49Be2c483295A3e3E511F96a468", "redeployed": false }, "aggregationExecutors": { "note": "2026-07-16 generation; whether they were redeployed again for the vanity registry is unconfirmed (2026-07-29). 1inch routing infrastructure, not part of the Aqua contract surface; never source-verified (policy); constructor args differ per chain", "simple": "0xd7d7FE53A5FC886e1B69c7213Fb0B24ccE1511a3", "protected": "0xE3B94BF84310fDcec27A219cA0923F46D7137DC7", "protectedSigner": "0x4447696e7878c0fa2c581768f04c3871a9a57b76" }, "testnet": { "sepolia": 11155111, "note": "vanity registry live on Sepolia; vanity router NOT yet deployed there (2026-07-29); 2026-07-16-generation contracts remain available" }, "verification": { "aquaAndRouter": "vanity generation: source-verified on Ethereum (Blockscout); bytecode spot-checked on Ethereum + Base; full 13-chain verification not re-run - verify per chain", "executors": "never verified (policy)" }, "chains": [1, 10, 56, 100, 130, 137, 146, 324, 4663, 8453, 42161, 43114, 59144], "unsupportedForAqua": [], "platformSync": { "confirmedAt": "2026-07-29", "inSync": ["dapp", "aqua-api", "swap-builder-api", "swap-api", "fuse", "@1inch/aqua-sdk 0.3.0 (npm)", "@1inch/swap-vm-sdk 0.4.0 (npm)"], "stale": ["1inch/aqua README", "1inch/swap-vm README"] }, "supersededRegistries": [ "0xe8026bf31e58b738647319362581ab11be92139b", "0x4a055aa172c98ec32de118b9b5b6ac8b4099a580", "0x7c2d4aa5c900c08004fadb1c0d953c5b099fec86" ], "supersededRouters": [ "0x016b417bc933370f5eacc40b1d58b015ac72b070", "0x1111113db0e0ef9d0e3a50d5f094a3a57a26c0de", "0x3c4758979ec30ca45857cabc2462a70699ed790e", "0xa5368dd669c5706256cf417b66e71a1efe1d331a", "0xdfd05fe230bfe7b212878414270c72c8345506fa", "0x8fdd04dbf6111437b44bbca99c28882434e0958f", "0x499943e74fb0ce105688beee8ef2abec5d936d31" ], "opcodeTable": { "boundToRouterVersion": "1.0.2", "source": "1inch/swap-vm@v1.0.2:src/opcodes/AquaOpcodes.sol (identical to v1.0.1 - the tag changed only Fee.sol)", "scope": "all deployed v1.0.x builds (v1.0.1 router, the 2026-07-16 v1.0.2 recompile, and the v1.0.2-tag vanity routers)", "reservedGapIndices": [[0, 9], [22, 26]], "missBehavior": { "reservedGap": "no-op", "beyondTable": "Panic(0x32)" }, "mainBranchWarning": "undeployed main (post-PR-154) renumbers the enum: no reserved gaps, UnknownOpcode(uint256) revert - NOT deployed; never derive the live table from main" }, "sdk": { "@1inch/aqua-sdk": "0.3.0", "@1inch/swap-vm-sdk": "0.4.0", "note": "Current releases carry the canonical vanity addresses (verified from the published packages, 2026-07-29). Releases <= 0.1.1 / 0.2.2 return superseded addresses. Pin exact versions." } } ``` ### Field notes - `vanityRedeployment` records the redeployment lifecycle: announced 2026-07-17, registry landed 2026-07-19, current router 2026-07-26 (a first vanity router from 2026-07-19 was superseded in between). The 2026-07-16 registry/router moved into the superseded arrays per the protocol this page prescribed. - `platformSync` (2026-07-29): dApp, aqua-api, swap-builder-api, swap-api, FUSE and the current npm SDK releases all carry the vanity addresses; only the two contract-repo READMEs remain stale. - `router.eip712` uses a **string** version (`"1.0.2"`) and the domain name `1inch SwapVM v1.0` (verified on-chain via `eip712Domain()` -- corrects the `SwapVMRouter` name stated in earlier revisions). `registry.eip712` is **null**: the registry has no EIP-712/ERC-5267 domain (`eip712Domain()` reverts); the earlier `AquaRouter`/v`1` (integer) claim is withdrawn. Only the router can be typed-data signed against -- and `verifyingContract` changes with each router generation. - `deployment` records the per-generation mechanism: the vanity addresses come from a deployer factory (Ethereum creation txs), the 2026-07-16 generation from plain `CREATE` by a nonce-synchronized EOA. Address equality across chains is an operational convention -- it does not prove identical bytecode or constructor args. Vanity-generation source verification has been confirmed on Ethereum (Blockscout); check other chains' explorers per chain. - `supersededRegistries` / `supersededRouters` list addresses your indexer should reject as current. They remain live on-chain: strategies shipped there stay active (and decodable) until makers dock and re-ship on the current deployment. Decode historical fills against them only, never route to them. - Robinhood Chain (`4663`) joined the chain set with the 2026-07-16 universal redeploy; `unsupportedForAqua` is now empty. Earlier manifests excluded 4663 because its registry was a different CREATE3 address. - `opcodeTable` is source-verified and **scoped to the deployed v1.0.x builds** (the `v1.0.2` tag left `AquaOpcodes.sol` identical to v1.0.1): a reserved-gap index is a no-op (execution continues, no price, no revert) and an index beyond the registered table reverts with a Solidity array-out-of-bounds `Panic(0x32)`; there is no named runtime error for an unknown opcode **in the deployed builds**. The undeployed `main` branch renumbers the enum and reverts with `UnknownOpcode(uint256)` instead -- those semantics apply to no deployed router; re-pin this block when the next release actually ships. - Pin the two SDK packages with exact versions (no `^`) so an off-cycle patch cannot shift the opcode table under your build. Current releases (0.3.0 / 0.4.0) carry the canonical vanity addresses; anything older does not. ## This changelog is an append-only log This page is an **append-only revision log**. Entries are added, never rewritten or removed: each carries an ISO date, and history stays intact so a reader can reconstruct what the docs asserted on any past date. When a fact changes, add a new dated entry rather than editing an old one. When the version manifest above changes, bump `manifestVersion` to the new date and add a matching entry below -- the manifest block is the current head; the log is its running history. Do not rewrite history. Superseded addresses, versions and behaviours are kept on the record (see `supersededRouters` and the [Stale / historical](/portal/documentation/aqua/reference/verified-contract-addresses) section) precisely so integrators can decode old activity and audit what changed. Correcting an error means appending a dated correction, not silently editing the earlier entry. https://business.1inch.com/portal/assets/docs-v2/aqua/getting-started/getting-started.md **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](/portal/documentation/aqua/getting-started/build-an-aquaapp) (Path A). ```typescript 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](/portal/documentation/aqua/reference/sdk-overview) and [Write your own opcode](/portal/documentation/aqua/swapvm/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](/portal/documentation/aqua/getting-started/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-sdk` builders (for example `AquaXYCAmmStrategy` or the lower-level `AquaProgramBuilder`), encode the Order, and register it with `ship()` on the shared `AquaSwapVMRouter`. 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 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 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](/portal/documentation/aqua/liquidity-layer/strategy-lifecycle). --- ## 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 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 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](/portal/documentation/aqua/getting-started/strategy-template): a fork-runnable Foundry + TypeScript version of this exact flow - [Patterns & decision tree](/portal/documentation/aqua/swapvm/patterns/patterns-and-decision-tree): concentrated liquidity, pegged, and other strategy types beyond XYC - [Strategy Lifecycle](/portal/documentation/aqua/liquidity-layer/strategy-lifecycle): the full lifecycle, events, and re-shipping patterns - [Contract Addresses](/portal/documentation/aqua/reference/contract-addresses): contract addresses on all 13 supported chains https://business.1inch.com/portal/assets/docs-v2/aqua/getting-started/build-an-aquaapp.md This guide shows how to add custom pricing to **1inch Aqua**, the shared liquidity layer, using its swap engine **SwapVM**. An `AquaApp` is an audited Solidity contract that owns pricing logic: it inherits `AquaApp`, registers strategies with `ship()` on the Aqua registry, and fills swaps by calling `pull()` and `push()`. Aqua is self-custodial. A maker's tokens stay in the maker's own wallet under a revocable, per-chain, per-token allowance, and move only when a taker fills a swap. `pull()` draws those tokens directly from the wallet and `push()` credits the maker; the registry holds 0 tokens, and the balances your app reads are internal virtual balances tracked in `Aqua.sol`. Standard smart-contract and approval risk still applies. --- ## Three ways to author pricing Aqua offers three authoring paths, from most to least custom. Pick the least custom one that expresses your pricing. - **Path A: write a custom AquaApp.** A per-app, audited Solidity contract that owns pricing and calls `pull()` / `push()` on the Aqua registry. Use this when your logic cannot be expressed with the built-in opcodes. This is the path covered in detail below. - **Path B: compose SwapVM opcodes into a program.** Assemble a strategy program from the opcodes already deployed on `AquaSwapVMRouter`, with no new contract to write, audit, or deploy. Best when a constant-product, concentrated, or pegged curve already covers your needs. - **Path C: embed proprietary pricing with Extruction.** Keep an off-list or proprietary pricing formula in your own contract and reference it through `IExtruction`, so you run inside the shared router without publishing a full AquaApp. Deployed contracts, identical across all 13 supported chains: Aqua registry (balance accounting) `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a`; the SwapVM router `AquaSwapVMRouter` v1.0.2 `0x111111338c5091e8440b67b168bae16a668ac0de`; taker credential `KycNFT` `0x26FFc7D378E8e49Be2c483295A3e3E511F96a468`. Aqua is audited but new; audits reduce, they do not eliminate, smart-contract risk. --- ## Path A: write a custom AquaApp ### 1. Inherit from the AquaApp contract ```solidity contract MyAMM is AquaApp { constructor(IAqua aqua) AquaApp(aqua) {} struct Strategy { address maker; // required -- makes strategyHash unique per maker address token0; address token1; // ... strategy parameters (IMMUTABLE once shipped) uint256 feeBps; // optional - strategy fees bytes32 salt; // optional -- allows multiple strategies per (maker, pair, fee) } } ``` The `maker` field in the strategy struct is required. Without it, two different makers using identical parameters would produce the same `strategyHash`, resulting in a collision in the registry. --- ### 2. Choose a swap pattern Two patterns exist for handling the input token. The choice is a trade-off between gas efficiency and implementation complexity. #### Pattern A: callback (recommended) Uses the `IXYCSwapCallback` interface (from `1inch/aqua` `examples/apps/interfaces/`). The taker implements the callback, which fires after `pull()`. Requires `nonReentrantStrategy` on the swap function. ```solidity function swapExactIn( Strategy calldata strategy, bool isZeroForOne, uint256 amountIn, uint256 amountOutMin, address to, // recipient of the output tokens bytes calldata takerData // forwarded to the taker callback ) external nonReentrantStrategy(strategy.maker, keccak256(abi.encode(strategy))) returns (uint256 amountOut) { bytes32 strategyHash = keccak256(abi.encode(strategy)); address tokenIn = isZeroForOne ? strategy.token0 : strategy.token1; address tokenOut = isZeroForOne ? strategy.token1 : strategy.token0; (uint256 balanceIn, uint256 balanceOut) = AQUA.safeBalances(strategy.maker, address(this), strategyHash, tokenIn, tokenOut); amountOut = // ... compute output amount based on AMM logic require(amountOut >= amountOutMin, "insufficient output"); uint256 expectedBalanceIn = balanceIn + amountIn; // Pull output tokens to the recipient (SWAP EXECUTION) AQUA.pull(strategy.maker, strategyHash, tokenOut, amountOut, to); // Callback: the taker pushes tokenIn to the maker's Aqua balance IXYCSwapCallback(msg.sender).xycSwapCallback( tokenIn, tokenOut, amountIn, amountOut, strategy.maker, address(this), strategyHash, takerData ); // Verify input received (SWAP EXECUTION) _safeCheckAquaPush(strategy.maker, strategyHash, tokenIn, expectedBalanceIn); } ``` `_safeCheckAquaPush` reads the virtual balance after the callback and reverts if `tokenIn` balance did not reach `expectedBalanceIn`. The `nonReentrantStrategy` lock is what makes this check safe: it prevents a nested `pull()` from inflating the balance before the check. --- #### Pattern B: direct transfer (simpler) The taker transfers `tokenIn` to the app contract directly, which then calls `push()`. No reentrancy protection required, but costs one extra `transferFrom`. ```solidity function swapExactIn( Strategy calldata strategy, bool isZeroForOne, uint256 amountIn, address to // recipient of the output tokens ) external returns (uint256 amountOut) { bytes32 strategyHash = keccak256(abi.encode(strategy)); address tokenIn = isZeroForOne ? strategy.token0 : strategy.token1; address tokenOut = isZeroForOne ? strategy.token1 : strategy.token0; (uint256 balanceIn, uint256 balanceOut) = AQUA.safeBalances(strategy.maker, address(this), strategyHash, tokenIn, tokenOut); amountOut = // ... compute output amount based on AMM logic // Pull output tokens to the recipient (SWAP EXECUTION) AQUA.pull(strategy.maker, strategyHash, tokenOut, amountOut, to); // Transfer input tokens from taker and push to Aqua (SWAP EXECUTION) IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn); IERC20(tokenIn).approve(address(AQUA), amountIn); AQUA.push(strategy.maker, address(this), strategyHash, tokenIn, amountIn); } ``` --- ### 3. Implement the taker callback (Pattern A only) Takers interacting with a callback-based app must implement the callback interface (`IXYCSwapCallback`) and call `push()` inside it: ```solidity contract SimpleTrader is IXYCSwapCallback { IAqua public immutable AQUA; constructor(IAqua _aqua, IERC20[] memory tokens) { AQUA = _aqua; for (uint256 i = 0; i < tokens.length; i++) { tokens[i].approve(address(AQUA), type(uint256).max); } } function swap( MyAMM app, MyAMM.Strategy calldata strategy, bool zeroForOne, uint256 amountIn ) external { app.swapExactIn( strategy, zeroForOne, amountIn, 0, // amountOutMin (calculate properly in production) msg.sender, // recipient "" // takerData ); } function xycSwapCallback( address tokenIn, address, // tokenOut uint256 amountIn, uint256, // amountOut address maker, address app, bytes32 strategyHash, bytes calldata ) external override { // Transfer input tokens to complete swap (SWAP EXECUTION ONLY) AQUA.push(maker, app, strategyHash, tokenIn, amountIn); } } ``` --- ### 4. Query balances Always use `safeBalances()` before computing swap output. It reverts if either token was not registered in the strategy at `ship()` time, preventing pricing errors on misconfigured or inactive strategies. ```solidity (uint256 balanceIn, uint256 balanceOut) = AQUA.safeBalances( strategy.maker, address(this), strategyHash, tokenIn, tokenOut ); // Reverts if tokenIn or tokenOut is not part of the active strategy ``` Use `rawBalances()` only for debugging or when querying arbitrary storage slots. --- ### Pattern comparison | | Callback (A) | Direct Transfer (B) | | ----------------------------- | ---------------------------- | --------------------------- | | Reentrancy protection needed | Yes (`nonReentrantStrategy`) | No | | Taker must implement callback | Yes | No | | Gas | Lower (one `transferFrom`) | Higher (two `transferFrom`) | | Taker approval target | `AQUA` | App contract | --- ## Path B: compose SwapVM opcodes into a program If your pricing fits an existing curve, you do not need to deploy a contract at all. Compose a strategy program from the opcodes already available on `AquaSwapVMRouter` (the deployed SwapVM router) and register it with `ship()`. The router owns pricing, and the type is set by the program bytes rather than by a separate contract address, so one router serves every strategy type. The deployed Aqua opcode set is `Controls`, `XYCSwap`, `XYCConcentrate`, `Decay`, `Fee`, `PeggedSwap`, and `Extruction`. (`Invalidators` and `SeriesEpochManager` belong to the limit-order router, not to Aqua.) The strategy types are `xyc` (constant product), `concentrated` (concentrated liquidity), and `pegged` (stable or reward-bearing). v1.0 ships two-token strategies only. Build programs with `@1inch/swap-vm-sdk` (SwapVM program builders and opcode encoding) and write strategies with `@1inch/aqua-sdk` (`ship` / `dock` and event decoders). Because a program encodes opcode numbers, each SDK version targets a specific router opcode table, so a strategy's bytecode is not portable across router versions. --- ## Path C: embed proprietary pricing with Extruction Use `Extruction` when your pricing formula is proprietary or otherwise not expressible with the built-in opcodes, but you still want to run inside the shared router instead of shipping a full AquaApp. The `Extruction` opcode calls out to an external contract that you supply through the `IExtruction` interface, which returns the quote for the swap. Two requirements are mandatory: - **The external target must be non-upgradeable.** A mutable pricing target would let the quote change out from under takers and would break the guarantees established by the audit. - `quote()` **and** `swap()` **must return identical amounts.** The static `quote()` used for simulation has to match what `swap()` actually executes. Any divergence between the two is a critical bug. Note the spelling: the opcode and interface are `Extruction` / `IExtruction` (not "extraction"). --- ## Related - [Smart Contract](/portal/documentation/aqua/reference/smart-contract) -- `pull()`, `push()`, `safeBalances()` full reference - [Liquidity provider & taker guide](/portal/documentation/aqua/getting-started/liquidity-provider-and-taker-guide) -- how makers ship / dock and takers fill swaps in Solidity - [Strategy Lifecycle](/portal/documentation/aqua/liquidity-layer/strategy-lifecycle) -- ship / dock / pull / push state machine - [Patterns & decision tree](/portal/documentation/aqua/swapvm/patterns/patterns-and-decision-tree) -- recipes for AMMs, limit orders, Dutch auctions ## Recipe: a contract as the maker (a composed multi-strategy vault) A maker does not have to be an EOA. A contract -- a vault that holds pooled inventory -- can be the maker and ship several strategies from one balance. Every registry entry point keys off either `msg.sender` (`ship`, `dock`) or an explicit `maker` argument (`pull`, `push`), so a contract becomes the maker simply by calling `ship()` itself. Its own on-chain token balance then backs everything it ships. This is a recipe, not a full contract. It shows the wiring a custody vault needs; pricing still lives in the `app` (a custom `AquaApp`, or the deployed `AquaSwapVMRouter` when you compose opcodes). ### How it fits together - The vault holds inventory (ERC20) in its own address. - The vault approves the **Aqua registry** (not the `app`) for each token it will ship. - The vault calls `ship()` once per strategy. Because `ship` records `msg.sender` as the maker, the vault is the maker of each one -- for example an `xyc` pair and a `pegged` pair, both against the same `app`, both drawing on the same balance. - On a fill, the `app` calls `pull(vault, strategyHash, token, amount, to)`, which runs `safeTransferFrom(vault -> taker)`; the taker side settles with `push(vault, app, strategyHash, token, amount)`, which runs `safeTransferFrom(app -> vault)`. `ship()` moves no tokens. It only writes a virtual balance (an allowance) into the registry and emits `Shipped` / `Pushed` events. Real inventory never leaves the vault until a taker fills and `pull()` transfers it out. Shipping a strategy is therefore pure bookkeeping plus the one-time approval above. ### One approval, one balance: strategies share the vault's inventory Each strategy gets its own virtual-balance slot, keyed by `(maker=vault, app, strategyHash, token)`. But those slots are accounting only. When `pull()` fires it runs `IERC20(token).safeTransferFrom(vault, to, amount)` against the vault's _single real ERC20 balance_, under the _single approval_ the vault granted the registry. The per-strategy numbers are independent; the tokens behind them are not. The consequence is the shared-liquidity behaviour of Aqua, applied inside one maker: a fill against one strategy reduces the real inventory available to all the others. If the vault ships an `xyc` pair holding 100k USDC of virtual balance and a `pegged` pair holding another 100k USDC of virtual balance, but only holds 120k USDC for real, the two strategies are competing for the same 120k. A large fill on the `xyc` strategy leaves less than the `pegged` strategy's virtual balance claims. | | Per strategy (virtual) | Across all strategies (real) | | -------------------- | ----------------------------------- | ------------------------------------------------ | | Where it lives | Registry slot per `strategyHash` | Vault's ERC20 balance + approval to the registry | | Changed by | `ship` / `pull` / `push` arithmetic | Actual token transfers at `pull` / `push` | | Binding at fill time | `safeBalances` gates pricing | `safeTransferFrom` reverts if inventory is short | Do not let the sum of virtual balances silently exceed real holdings unless you intend the overlap. When it does, some fills will revert on `safeTransferFrom` (insufficient balance) even though `safeBalances()` still reports room on that strategy. Either keep total virtual balance at or below real inventory, or actively monitor the vault's token balance and re-price / `dock` as it drains. ### strategyHash is per (maker, app, encoded strategy) The registry computes `strategyHash = keccak256(strategy)` over the encoded strategy bytes, and stores balances at `(maker, app, strategyHash, token)`. Within one vault the maker is fixed, so two strategies must encode to _distinct_ bytes -- otherwise they hash to the same slot and the second `ship()` reverts with `StrategiesMustBeImmutable` (the slot is already occupied and strategies are immutable once shipped). Give each strategy distinct parameters, or add a salt, so the `xyc` pair and the `pegged` pair land on separate slots. ### What the vault must implement - **Approval, targeting the registry.** The vault must `approve` the Aqua registry address for every token it ships, sized to cover its pulls. `pull()` transfers _from the maker_, so without this approval every fill reverts. Approve the registry, never the `app`. - **Receiving.**`push()` credits the vault via `safeTransferFrom(app -> vault)`. The vault only needs to hold the ERC20 and must not block incoming transfers (no reverting transfer hook). Aqua settles in ERC20 via `safeTransferFrom` only, so no payable `receive()` is required for the swap path. - **Maker actions run as the caller.**`ship()` and `dock()` record `msg.sender` as the maker, so the vault itself must call them -- typically through owner-gated functions that forward to `AQUA.ship` / `AQUA.dock`. `dock()` must list every token of the strategy or it reverts with `DockingShouldCloseAllTokens`. ```solidity // Sketch, not a full contract. Custody + maker plumbing only; pricing lives in `app`. contract InventoryVault { IAqua public immutable AQUA; // the Aqua registry address public immutable app; // AquaApp or AquaSwapVMRouter that prices the strategies // One approval backs every strategy: lets pull() do transferFrom(vault -> taker). function approveRegistry(IERC20 token, uint256 amount) external onlyOwner { token.approve(address(AQUA), amount); } // Ship a strategy from this vault's balance; msg.sender == this vault == maker. // Call once for the xyc pair and once for the pegged pair, with distinct strategy bytes. function shipStrategy(bytes calldata strategy, address[] calldata tokens, uint256[] calldata amounts) external onlyOwner returns (bytes32 strategyHash) { strategyHash = AQUA.ship(app, strategy, tokens, amounts); } // Revoke a strategy; must list all of its tokens. function dockStrategy(bytes32 strategyHash, address[] calldata tokens) external onlyOwner { AQUA.dock(app, strategyHash, tokens); } // push() credits this contract by ERC20 transfer; holding ERC20 needs no receiver hook. } ``` Custody stays self-directed. The vault's tokens sit in the vault under a revocable approval; `dock()` zeroes a strategy's virtual balances and the vault can revoke the token approval at any time. Standard smart-contract and approval risk applies to the vault itself -- it is now the maker of record for each strategy it ships. ## Runnable Path A: custom AquaApp end-to-end This section turns the Path A description above into a single, self-contained Foundry test. It deploys a custom AquaApp (`MyAMM`), has a maker `ship()` a strategy against it, and has a taker (`SimpleTrader`) fill through the `IXYCSwapCallback` path -- asserting the registry events that prove the swap. `MyAMM` reuses the audited `XYCSwap` constant-product curve, so it already exposes `swapExactIn`, the `nonReentrantStrategy` lock, `AQUA.safeBalances` / `AQUA.pull`, the callback, and `_safeCheckAquaPush` -- the exact surface documented in Path A. In Path A the strategy is registered with **APP = your AquaApp's own address**. The maker calls `AQUA.ship(address(myAMM), ...)`, and every balance row is keyed by `(maker, myAMM, strategyHash, token)`. This is the one structural difference from Path B, where APP is the shared `AquaSwapVMRouter`. Because `pull()` authorises against `msg.sender`, and `MyAMM` is the caller, the registry resolves the pull against that same `MyAMM` key. A custom app therefore never routes through the SwapVM router. ### The custom AquaApp (MyAMM) and the taker Both contracts are tiny. `MyAMM` inherits the audited example curve; `SimpleTrader` is the callback taker from section 3 -- it approves the registry once, then pushes `tokenIn` from inside `xycSwapCallback`. ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.30; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IAqua } from "aqua/src/interfaces/IAqua.sol"; import { XYCSwap } from "aqua/examples/apps/XYCSwap.sol"; import { IXYCSwapCallback } from "aqua/examples/apps/interfaces/IXYCSwapCallback.sol"; // A custom AquaApp. It reuses the audited XYCSwap constant-product curve, so it // exposes swapExactIn(...), nonReentrantStrategy, AQUA.safeBalances / AQUA.pull, // the IXYCSwapCallback path and _safeCheckAquaPush out of the box. contract MyAMM is XYCSwap { constructor(IAqua aqua) XYCSwap(aqua) {} } // A taker that fills through the callback. It approves the Aqua registry (not the // app) so push() can transferFrom it during the callback. contract SimpleTrader is IXYCSwapCallback { IAqua public immutable AQUA; constructor(IAqua aqua, IERC20[] memory tokens) { AQUA = aqua; for (uint256 i = 0; i < tokens.length; i++) { tokens[i].approve(address(aqua), type(uint256).max); } } function swap(MyAMM app, XYCSwap.Strategy calldata s, bool zeroForOne, uint256 amountIn) external returns (uint256) { // recipient = msg.sender (the EOA calling this trader) return app.swapExactIn(s, zeroForOne, amountIn, 0, msg.sender, ""); } function xycSwapCallback( address tokenIn, address /* tokenOut */, uint256 amountIn, uint256 /* amountOut */, address maker, address app, bytes32 strategyHash, bytes calldata /* takerData */ ) external override { // Deliver the input leg into the maker's Aqua balance. AQUA.push(maker, app, strategyHash, tokenIn, amountIn); } } ``` ### The end-to-end Foundry test The test deploys a fresh `Aqua` registry locally (its constructor takes no arguments), funds the maker, and drives ship -> fill. On mainnet the same flow targets the deployed registry at `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a`, identical across all 13 chains. ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.30; import { Test } from "forge-std/Test.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Aqua } from "aqua/src/Aqua.sol"; import { IAqua } from "aqua/src/interfaces/IAqua.sol"; import { XYCSwap } from "aqua/examples/apps/XYCSwap.sol"; // MyAMM and SimpleTrader from the block above. contract TestToken is ERC20 { constructor(string memory n, string memory s) ERC20(n, s) {} function mint(address to, uint256 a) external { _mint(to, a); } } contract RunnablePathATest is Test { Aqua aqua; MyAMM myAMM; TestToken token0; TestToken token1; SimpleTrader trader; address maker = makeAddr("maker"); address taker = makeAddr("taker"); XYCSwap.Strategy strategy; bytes32 strategyHash; function setUp() public { aqua = new Aqua(); myAMM = new MyAMM(IAqua(address(aqua))); token0 = new TestToken("T0", "T0"); token1 = new TestToken("T1", "T1"); // Maker owns the liquidity and approves the Aqua REGISTRY (never the app). // Tokens stay in the maker wallet; ship() only records a virtual balance. token0.mint(maker, 1_000 ether); token1.mint(maker, 1_000 ether); vm.startPrank(maker); token0.approve(address(aqua), type(uint256).max); token1.approve(address(aqua), type(uint256).max); vm.stopPrank(); // The taker contract approves the registry so push() can pull tokenIn from it. IERC20[] memory toApprove = new IERC20[](2); toApprove[0] = IERC20(address(token0)); toApprove[1] = IERC20(address(token1)); vm.prank(taker); trader = new SimpleTrader(IAqua(address(aqua)), toApprove); token0.mint(address(trader), 10 ether); // input inventory for the fill // maker is part of the struct, so the strategyHash is unique per maker. strategy = XYCSwap.Strategy({ maker: maker, token0: address(token0), token1: address(token1), feeBps: 30, // 0.30% salt: bytes32(0) }); // MyAMM computes keccak256(abi.encode(strategy)); the registry computes // keccak256(strategy-bytes). They match only if abi.encode(strategy) is shipped. strategyHash = keccak256(abi.encode(strategy)); } function test_pathA_end_to_end() public { // 1) SHIP: maker registers the strategy against MyAMM. APP == address(myAMM). address[] memory tokens = new address[](2); tokens[0] = address(token0); tokens[1] = address(token1); uint256[] memory amounts = new uint256[](2); amounts[0] = 1_000 ether; // initial virtual balance of token0 amounts[1] = 1_000 ether; // initial virtual balance of token1 // ship() emits Shipped + one Pushed per funded token. vm.expectEmit(address(aqua)); emit IAqua.Shipped(maker, address(myAMM), strategyHash, abi.encode(strategy)); vm.expectEmit(address(aqua)); emit IAqua.Pushed(maker, address(myAMM), strategyHash, address(token0), 1_000 ether); vm.expectEmit(address(aqua)); emit IAqua.Pushed(maker, address(myAMM), strategyHash, address(token1), 1_000 ether); vm.prank(maker); bytes32 shipped = aqua.ship(address(myAMM), abi.encode(strategy), tokens, amounts); assertEq(shipped, strategyHash); // 2) FILL: token0 -> token1 through the callback. Quote first for the assert. uint256 amountIn = 10 ether; uint256 expectedOut = myAMM.quoteExactIn(strategy, true, amountIn); // There is no single "Swapped" event. The fill is the Pulled(tokenOut) + // Pushed(tokenIn) pair: token1 leaves the maker, token0 returns. vm.expectEmit(address(aqua)); emit IAqua.Pulled(maker, address(myAMM), strategyHash, address(token1), expectedOut); vm.expectEmit(address(aqua)); emit IAqua.Pushed(maker, address(myAMM), strategyHash, address(token0), amountIn); vm.prank(taker); uint256 amountOut = trader.swap(myAMM, strategy, true, amountIn); // 3) ASSERT: recipient received tokenOut; virtual balances moved by the legs. assertEq(amountOut, expectedOut); assertEq(token1.balanceOf(taker), expectedOut); (uint248 bal0,) = aqua.rawBalances(maker, address(myAMM), strategyHash, address(token0)); (uint248 bal1,) = aqua.rawBalances(maker, address(myAMM), strategyHash, address(token1)); assertEq(bal0, uint248(1_000 ether + amountIn)); // pushed in assertEq(bal1, uint248(1_000 ether - expectedOut)); // pulled out } } ``` **Ship the exact bytes.** The registry stores the strategy under `keccak256(strategy)` where `strategy` is the raw calldata you pass to `ship()`. `MyAMM` recomputes `keccak256(abi.encode(strategy))` inside every call. The two hashes agree only when the maker ships precisely `abi.encode(strategy)`. Ship any other encoding and `safeBalances` reads an unregistered row and reverts with `SafeBalancesForTokenNotInActiveStrategy`, so the fill fails before any transfer. ### What the events prove Aqua has no aggregate _Swapped_ event; the registry records the maker's fund movements and the app returns the amounts. The table maps each lifecycle step to the events the test asserts. Every event is emitted by the Aqua registry. | Step | Call | Registry event(s) | | ---------------- | ------------------------------------------------------- | --------------------------------------------------------------------- | | Register | maker -> `AQUA.ship(myAMM, ...)` | `Shipped` + one `Pushed` per funded token (initial balance) | | Fill, output leg | `MyAMM` -> `AQUA.pull(..., tokenOut, ..., to)` | `Pulled` (tokenOut leaves the maker wallet to the recipient) | | Fill, input leg | `SimpleTrader` -> `AQUA.push(..., tokenIn, ...)` in callback | `Pushed` (tokenIn returns to the maker balance) | | Settlement check | `MyAMM._safeCheckAquaPush(...)` | none -- reverts with `MissingTakerAquaPush` if the input never arrived | The `nonReentrantStrategy(maker, strategyHash)` lock is what makes the post-callback `_safeCheckAquaPush` trustworthy: it blocks a nested `pull()` from inflating the input balance before the check reads it. That guarantee is why the callback pattern is safe with a single `transferFrom`. https://business.1inch.com/portal/assets/docs-v2/aqua/getting-started/worked-examples.md This page walks through two fully-worked numeric examples on Aqua. The first is a **fee round-trip** that shows exactly how the LP swap fee is charged, in wei-level arithmetic, and where it is set in the SDK and the on-chain opcode. The second is an **illustrative net-P&L** for a concentrated-range position, to show how gross swap-fee income and impermanent loss combine. :::info The fee example is **exact**: every number is reproducible from the on-chain formula. The P&L example uses **illustrative assumptions** -- they are clearly labelled and are not a forecast or a promise of return. ::: ## Example 1 -- Fee round-trip on the LP swap fee Aqua expresses fees in basis points on a **1e9 base**, where `1e9 = 100%`. A LP swap fee of **0.30%** is therefore `feeBps = 3,000,000` on that base (0.0030 x 1e9 = 3,000,000). In v1 the LP "Swap fee" is the only non-zero fee. ### Where the fee is set -- SDK The maker sets the input fee on the strategy with `withFeeTokenIn`. The SDK takes the fee in conventional basis points (10,000 bps = 100%) and scales it onto the 1e9 base internally via `FlatFeeArgs.fromBps`, which multiplies by 100,000: ```typescript import { AquaXYCAmmStrategy } from "@1inch/swap-vm-sdk"; const program = AquaXYCAmmStrategy.new() .withFeeTokenIn(30) // 30 bps = 0.30% swap fee on the input token .build(); // Internally: FlatFeeArgs.fromBps(30) => fee = 30 * 100_000 = 3_000_000 // i.e. feeBps = 3,000,000 on the 1e9 base (3,000,000 / 1e9 = 0.30%) ``` ### Where the fee is applied -- opcode At execution time the `flatFeeAmountInXD` instruction (the `Fee` opcode) runs before the swap-amounts computation. For an **exact-in** swap it reduces the input amount by the fee, rounding the fee _up_ in the pool's favour, and feeds the remainder into the XYC swap curve: ``` // swap-vm/src/instructions/Fee.sol (BPS = 1e9) if (ctx.query.isExactIn) { ctx.swap.amountIn -= Math.ceilDiv(ctx.swap.amountIn * feeBps, BPS); ctx.runLoop(); } ``` So the fee amount is `ceilDiv(amountIn x feeBps, 1e9)`, and the amount that actually reaches the swap curve (and hence the maker) is `amountIn fee`. ### Concrete swap -- taker sends 1,000 USDC USDC has 6 decimals, so 1,000 USDC = `1,000,000,000` base units. With `feeBps = 3,000,000` and `BPS = 1e9`: | Step | Formula | Value (base units) | Human | | ---------------------------- | --------------------------------------------------- | ------------------ | ----------------- | | 1\. Taker input (`amountIn`) | given | 1,000,000,000 | 1,000.000000 USDC | | 2\. Fee | `ceilDiv(1,000,000,000 x 3,000,000, 1,000,000,000)` | 3,000,000 | 3.000000 USDC | | 3\. Net into swap curve | `1,000,000,000 3,000,000` | 997,000,000 | 997.000000 USDC | | 4\. Fee retained by maker | = step 2 | 3,000,000 | 3.000000 USDC | The fee is **exactly 3 USDC**. The remaining **997 USDC** is what the constant-product XYC curve prices to produce the taker's `amountOut`; the 3 USDC never leaves the pool -- it stays in the maker's virtual `tokenIn` balance. ### Resulting amountOut (illustrative reserves) The exact `amountOut` depends on the live virtual reserves. For a constant-product XYC pool the output is `amountOut = R_out x dx / (R_in + dx)`, where `dx` is the **net** input (997 USDC). Taking illustrative reserves of 3,000,000 USDC and 1,000 WETH (a 3,000 USDC/WETH mid-price): | Case | `dx` (USDC) | `amountOut = 1,000 x dx / (3,000,000 + dx)` | | ------------------ | ----------- | ------------------------------------------- | | With 0.30% fee | 997 | 0.332223 WETH | | Fee-free reference | 1,000 | 0.333222 WETH | The fee costs the taker 0.000999 WETH of output -- the value the maker keeps. Reserve numbers here are _illustrative_; the 3 USDC / 997 USDC split above is exact and independent of reserves. ### The fee auto-compounds Because the retained 3 USDC stays inside the maker's virtual reserves rather than being paid out, it is added to the maker's Aqua balance and surfaced by the `Pushed(maker, app, strategyHash, token, amount)` event. On the next quote the curve prices against the larger reserve, so accrued fees **compound** into the position automatically -- makers do not claim them separately. ## Example 2 -- Concentrated-range net P&L (illustrative) :::note Every number in this section is an **ILLUSTRATIVE ASSUMPTION**, not a forecast. Swap-fee income is **not guaranteed**: it depends entirely on how much taker volume is actually routed through your range while the price is inside it. A concentrated range is **not leverage** -- it concentrates the same capital into a tighter band, which raises fee capture when in range and raises impermanent loss when the price moves. Impermanent loss can exceed fees, and a range that the price has left earns nothing until it returns. ::: ### Setup A maker concentrates WETH/USDC liquidity into the 1,500-3,000 USDC-per-WETH band and charges a 0.30% swap fee: ```typescript import { AquaXYCAmmStrategy } from "@1inch/swap-vm-sdk"; const ONE_E18 = 1_000_000_000_000_000_000n; const program = AquaXYCAmmStrategy.newConcentrate({ rawPriceMin: ONE_E18 / 3000n, // 3,000 USDC per WETH (upper USDC price) rawPriceMax: ONE_E18 / 1500n // 1,500 USDC per WETH (lower USDC price) }) .withFeeTokenIn(30) // 30 bps = 0.30% .build(); ``` ### Assumptions | Input | Assumed value | Note | | ------------------------- | ---------------------------- | ---------------------------------------- | | Position size | \$50,000 | Illustrative capital in the range | | Swap fee | 0.30% (`withFeeTokenIn(30)`) | Exact, from Example 1 | | Period | 30 days | Illustrative window | | Number of fills (`N`) | 400 | Each is a `Swapped` event | | Average fill notional | \$2,000 | Illustrative | | Volume routed in-range | \$800,000 | = 400 x \$2,000 | | Realised impermanent loss | \$1,500 | Illustrative price-drift within the band | ### Net P&L Gross swap-fee income is the fee rate applied to the in-range volume; each fill's fee accrues via `Swapped(orderHash, maker, taker, tokenIn, tokenOut, amountIn, amountOut)` and is retained into the maker's balance via `Pushed` (same mechanism as Example 1). Net P&L subtracts the illustrative impermanent loss: | Line | Formula | Amount | | ----------------------------------- | ----------------- | ---------- | | Gross swap-fee income | 0.30% x \$800,000 | +\$2,400 | | Impermanent loss (illustrative) | assumption | \$1,500 | | **Net P&L (30 days, illustrative)** | \$2,400 \$1,500 | **+\$900** | The **+\$900** net figure is only as good as the assumptions above. If less volume is routed in-range, gross fees fall proportionally; if the price drifts further within or out of the band, impermanent loss rises and can outweigh fees, turning net P&L negative. Use this template with your own realised `Swapped` volume and mark-to-market to compute an _actual_ result -- do not treat these inputs as expected values. :::info **How to measure the real numbers:** sum the fee portion of each `Swapped` event over your period for gross income, read your virtual-reserve growth from `Pushed` events, and mark your position to the current price to compute realised impermanent loss. Net P&L is gross fees minus that impermanent loss. ::: ## Illustrative operating numbers This section puts concrete numbers on two setups a quant, market maker, or stablecoin issuer would parameterize: a volatile-pair **XYC WETH/USDC** pool, and a **pegged USDC/DAI** pool on the PeggedSwap curve. The **fee model is exact** (reused verbatim from Example 1: `feeBps` on the `1e9` base, `withFeeTokenIn(30)` = 0.30% = `feeBps 3,000,000`). The **PeggedSwap curve parameters are exact facts** from the deployed contract (`linearWidth` is coefficient A scaled by `1e27`; the valid range is `0` to `MAX_LINEAR_WIDTH = 5000e27` inclusive). Every reserve, volume, price-drift, and P&L figure below is an **ILLUSTRATIVE ASSUMPTION**. :::note **ILLUSTRATIVE -- not a forecast, not a promise of yield.** The reserves, daily volumes, impermanent-loss figures, and net P&L below are worked-example inputs, not expected values. Swap-fee income is earned only from taker volume actually routed through your pool while price is in range; it is **not guaranteed**. Impermanent loss can exceed fees and turn a net figure negative. Aqua's shared liquidity is **capital availability** for makers to quote against -- it is not leverage and it does not pay a yield. Replace every ILLUSTRATIVE number with your own realised `Swapped` volume and mark-to-market before drawing any conclusion. ::: ### Setup A -- XYC WETH/USDC (volatile pair) A constant-product XYC pool quoting WETH against USDC at a 0.30% swap fee. The fee line is exact; reserves and volume are ILLUSTRATIVE. ```typescript import { AquaXYCAmmStrategy } from "@1inch/swap-vm-sdk"; const program = AquaXYCAmmStrategy.new() .withFeeTokenIn(30) // 30 bps = 0.30% -> feeBps 3,000,000 on the 1e9 base (EXACT) .build(); ``` | Parameter | Value | Basis | | --------------------- | -------------------------- | --------------------------------------------- | | USDC reserve (6 dec) | 3,000,000 USDC | ILLUSTRATIVE | | WETH reserve (18 dec) | 1,000 WETH | ILLUSTRATIVE | | Implied mid-price | 3,000 USDC / WETH | = 3,000,000 / 1,000 (ILLUSTRATIVE) | | Pool TVL | ~\$6,000,000 | 3M USDC + 1,000 WETH x \$3,000 (ILLUSTRATIVE) | | Swap fee | 0.30% (`feeBps 3,000,000`) | EXACT | | Sample daily volume | \$2,000,000 | ILLUSTRATIVE | Per fill the fee is exactly `ceilDiv(amountIn x 3,000,000, 1e9)` retained into the maker's virtual reserves (it auto-compounds, per Example 1). Aggregated across a day of ILLUSTRATIVE volume: | Line | Formula | Amount (ILLUSTRATIVE) | | ------------------------------- | ------------------- | --------------------- | | Gross daily swap-fee income | 0.30% x \$2,000,000 | +\$6,000 | | Impermanent loss (day, assumed) | assumption | \$2,000 | | **Net (day, ILLUSTRATIVE)** | \$6,000 \$2,000 | **+\$4,000** | The 0.30% rate and the per-fill `ceilDiv` arithmetic are exact; the \$6,000 gross scales linearly with whatever volume actually routes through the pool, and the \$2,000 IL is a placeholder -- a larger price move raises it and can outweigh fees. ### Setup B -- Pegged USDC/DAI (PeggedSwap curve) PeggedSwap uses the square-root-linear invariant `(x/X) + (y/Y) + A(x/X + y/Y) = 1 + A`, with curvature p=0.5 hardcoded. The five program args are exact fields of `PeggedSwapArgsBuilder.Args` (`swap-vm/src/instructions/PeggedSwap.sol`): `x0`, `y0`, `linearWidth`, `rateLt`, `rateGt`. - `rateLt` / `rateGt` -- decimal-normalization multipliers assigned **by token address**: `rateLt` applies to the token with the _lower_ address, `rateGt` to the _greater_. For an 18-vs-6-decimal pair the 18-dec token uses rate `1` and the 6-dec token uses rate `1e12`, normalizing both to a common 1e18 scale. These are **exact** (structural), not tunable. - `x0` / `y0` -- initial reserve normalization factors = initial balance x that token's rate. `x0` corresponds to the lower-address token, `y0` to the greater. - `linearWidth` -- coefficient A scaled by `1e27`. Higher A means tighter price near 1:1. For tight stablecoin pairs (USDC/USDT, USDC/DAI) the contract's parameter guide recommends **A 100e27-300e27**. Hard cap: `MAX_LINEAR_WIDTH = 5000e27`. ``` // PeggedSwapArgsBuilder.Args (swap-vm/src/instructions/PeggedSwap.sol, v1.0.1) // USDC (6 dec) / DAI (18 dec). ILLUSTRATIVE assumption below: the 18-decimal // token holds the LOWER address, so rateLt = 1 (DAI) and rateGt = 1e12 (USDC). // If your actual addresses order the other way, swap rateLt<->rateGt and x0<->y0. Args({ x0: 1_000_000e18, // lower-address (DAI) reserve x rate = 1,000,000e18 x 1 (ILLUSTRATIVE reserve) y0: 1_000_000e18, // greater-address (USDC) reserve x rate = 1e12 x 1e12 (ILLUSTRATIVE reserve) linearWidth: 200e27, // A = 200, mid of the recommended 100e27-300e27 band (ILLUSTRATIVE choice) rateLt: 1, // 18-dec token (DAI) -> scale 1 (EXACT: structural) rateGt: 1_000_000_000_000 // 6-dec token (USDC) 1e12 -> normalize to 1e18 (EXACT: structural) }) ``` | Arg | Value | Basis | | ------------------- | -------------------------- | ------------------------------------------------------------------------- | | `x0` | 1,000,000e18 | ILLUSTRATIVE initial reserve x rate | | `y0` | 1,000,000e18 | ILLUSTRATIVE initial reserve x rate | | `linearWidth` (A) | 200e27 | ILLUSTRATIVE choice, inside EXACT 100e27-300e27 guide band; 5000e27 cap | | `rateLt` | 1 | EXACT -- 18-dec (lower-address) token | | `rateGt` | 1e12 | EXACT -- 6-dec (greater-address) token | | Swap fee (optional) | 0.30% (`feeBps 3,000,000`) | EXACT if `withFeeTokenIn(30)` is set | :::info **Peg behaviour and bounds.** Raising A tightens the curve so a swap near the 1:1 anchor moves price very little (deep effective liquidity); the swap output still rounds in the maker's favour, exactly as in Example 1. This curve has **finite reserves and a hard price boundary** -- it is designed for assets pegged to a fixed ratio (stablecoins, wrapped tokens), and is **not** suitable for a drifting peg whose ratio changes over time without a moving anchor. `linearWidth` must satisfy `0 A 5000e27`; a value above the cap reverts at parse time with `PeggedSwapInvalidLinearWidth`. None of this constitutes a promise of return: with a well-chosen A a pegged pool captures fee volume at low slippage, but income still depends entirely on realised taker flow. ::: To turn either template into an _actual_ result, sum the fee portion of each `Swapped` event over your period for gross income, read virtual-reserve growth from `Pushed` events, and mark the position to current price for realised impermanent loss. Net = gross fees impermanent loss. https://business.1inch.com/portal/assets/docs-v2/aqua/getting-started/strategy-template.md A complete, copy-pasteable starting point for building an Aqua strategy. It ships an **XYC** (constant-product) strategy, runs a quote and a swap as a **credentialed taker**, and docks - in two flavours you can lift verbatim: - a **Foundry** test that runs against a mainnet fork and asserts every protocol event by its `topic0` hash; - a **TypeScript** script using `@1inch/aqua-sdk` + `@1inch/swap-vm-sdk`, broadcast through your own RPC. :::info There is no separate starter repository - this page _is_ the starter. Copy the two files below into a fresh Foundry / Node project and fill in your addresses and keys. ::: ## What the flow does Aqua is a shared liquidity layer: a maker keeps inventory in its own wallet and grants an app the right to move it. A strategy is an Aqua _program_ (a byte stream of VM instructions) embedded in an order. The lifecycle is: 1. **ship** - the maker registers the strategy and its initial virtual balances with the Aqua registry (`0x1111113ccf1426a8e30e2bff5e005d929bf6a90a`). Emits `Shipped` plus one `Pushed` per token. 2. **quote** - a taker simulates the swap through the `AquaSwapVMRouter` (`0x111111338c5091e8440b67b168bae16a668ac0de`) to read the output amount. 3. **swap** - the taker executes. The registry pulls the maker's output token (`Pulled`) and pushes the taker's input token to the maker (`Pushed`); the router emits `Swapped`. 4. **dock** - the maker closes the strategy for all tokens. Emits `Docked`. The strategy here is credential-gated: it begins with the `onlyTxOriginTokenBalanceNonZero` instruction pointing at the KycNFT (`0x26FFc7D378E8e49Be2c483295A3e3E511F96a468`), so only a taker whose `tx.origin` holds a KycNFT may swap. Drop that instruction for a permissionless pool. ## How the program bytes are laid out An Aqua program is a sequence of `[opcode][argsLength][args]` instructions. The strategy built by `AquaXYCAmmStrategy.new().withTxOriginAccessToken(KYC_NFT).build()` compiles to exactly: | Bytes | Instruction | Aqua opcode index | | ------------------------ | ---------------------------------------- | ----------------- | | `21 14 <20-byte KycNFT>` | `onlyTxOriginTokenBalanceNonZero(token)` | 33 (`0x21`) | | `11 00` | `xycSwapXD` (x\*y=k, no args) | 17 (`0x11`) | The order that wraps the program uses `MakerTraits.default()`, which sets only bit 254 (`useAquaInsteadOfSignature`) - so `traits = 1 << 254`, with no hooks and a zero receiver. The shipped `strategy` bytes are simply `abi.encode(Order{maker, traits, data})`, and the strategy hash is `keccak256` of those bytes. ## 1. Foundry test Save as `test/StrategyTemplate.t.sol`. It builds the program inline (no SDK needed on the Solidity side), so the exact opcode bytes above are visible and auditable. ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {Test, Vm} from "forge-std/Test.sol"; interface IERC20 { function approve(address spender, uint256 value) external returns (bool); function balanceOf(address account) external view returns (uint256); } interface IAqua { function ship(address app, bytes calldata strategy, address[] calldata tokens, uint256[] calldata amounts) external returns (bytes32 strategyHash); function dock(address app, bytes32 strategyHash, address[] calldata tokens) external; function rawBalances(address maker, address app, bytes32 strategyHash, address token) external view returns (uint248 balance, uint8 tokensCount); } interface ISwapVM { struct Order { address maker; uint256 traits; bytes data; } function quote(ISwapVM.Order calldata order, address tokenIn, address tokenOut, uint256 amount, bytes calldata takerTraitsAndData) external returns (uint256 amountIn, uint256 amountOut, bytes32 orderHash); function swap(ISwapVM.Order calldata order, address tokenIn, address tokenOut, uint256 amount, bytes calldata takerTraitsAndData) external returns (uint256 amountIn, uint256 amountOut, bytes32 orderHash); } /// @notice End-to-end Aqua strategy flow against a mainnet fork: /// ship an XYC strategy -> quote + swap as a credentialed taker -> dock, /// asserting every protocol event by its topic0 hash. contract StrategyTemplateTest is Test { // --- Verified mainnet deployment (Aqua v1) --- IAqua internal constant AQUA = IAqua(0xE8026bF31E58b738647319362581AB11Be92139B); // registry ISwapVM internal constant ROUTER = ISwapVM(0x016B417Bc933370F5EAcC40B1d58B015ac72B070); // AquaSwapVMRouter v1.0.2 address internal constant KYC_NFT = 0x26FFc7D378E8e49Be2c483295A3e3E511F96a468; IERC20 internal constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); IERC20 internal constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); // --- Event topic0 hashes (all Aqua/SwapVM events declare NO indexed params) --- bytes32 internal constant SHIPPED = 0xdc3622e06fb145651f567d421c9ef261d71d43e3778b761907bc0d70d42e52b0; bytes32 internal constant DOCKED = 0xd173a1d140c154eb1ce9298d251d5eb8c4089cc2d16e70f1067bdc810c6fe004; bytes32 internal constant PULLED = 0x3ad61047071575417c75e3311e5d46ff042e292b5dd8769ff18b4b254098ca7a; bytes32 internal constant PUSHED = 0x3f18354abbd5306dd1665c2c90f614a4559e39dd620d04fbe5458e613b6588f3; bytes32 internal constant SWAPPED = 0x54bc5c027d15d7aa8ae083f994ab4411d2f223291672ecd3a344f3d92dcaf8b2; address internal maker = makeAddr("maker"); address internal taker = makeAddr("taker"); /// @dev Aqua program bytecode is a stream of [opcode][argsLen][args] instructions. /// This is exactly what `AquaXYCAmmStrategy.new().withTxOriginAccessToken(KYC_NFT).build()` /// produces in @1inch/swap-vm-sdk: /// 0x21 0x14 <20-byte KYC_NFT> -> onlyTxOriginTokenBalanceNonZero (aquaInstructions index 33) /// 0x11 0x00 -> xycSwapXD (constant product x*y=k) (aquaInstructions index 17, no args) function _program() internal pure returns (bytes memory) { return abi.encodePacked(uint8(0x21), uint8(0x14), KYC_NFT, uint8(0x11), uint8(0x00)); } /// @dev order.encode() == abi.encode(ISwapVM.Order{maker, traits, data}). /// MakerTraits.default() sets only bit 254 (useAquaInsteadOfSignature); no hooks, receiver = 0. function _order() internal view returns (ISwapVM.Order memory) { return ISwapVM.Order({maker: maker, traits: uint256(1) << 254, data: _program()}); } function testShipQuoteSwapDock() public { ISwapVM.Order memory order = _order(); bytes memory strategy = abi.encode(order); bytes32 strategyHash = keccak256(strategy); // TakerTraits.default().encode(): 10 uint16 offsets (all 0) + uint16 flags. // flags = exactIn (bit0) | useTransferFromAndAquaPush (bit6) = 0x0041. bytes memory takerTraits = abi.encodePacked(bytes20(0), uint16(0x0041)); // --- Maker seeds real inventory; funds stay in the maker wallet --- uint256 usdcSeed = 15_000e6; uint256 wethSeed = 5 ether; deal(address(USDC), maker, usdcSeed); deal(address(WETH), maker, wethSeed); // Approve the Aqua registry: Aqua.pull() transfers straight from the maker wallet. vm.startPrank(maker); USDC.approve(address(AQUA), type(uint256).max); WETH.approve(address(AQUA), type(uint256).max); vm.stopPrank(); address[] memory tokens = new address[](2); tokens[0] = address(USDC); tokens[1] = address(WETH); uint256[] memory amounts = new uint256[](2); amounts[0] = usdcSeed; amounts[1] = wethSeed; // --- Ship (emits Shipped + one Pushed per token) --- vm.recordLogs(); vm.prank(maker); bytes32 returnedHash = AQUA.ship(address(ROUTER), strategy, tokens, amounts); assertEq(returnedHash, strategyHash, "ship: strategyHash mismatch"); Vm.Log[] memory shipLogs = vm.getRecordedLogs(); assertTrue(_seen(shipLogs, SHIPPED), "no Shipped"); assertTrue(_seen(shipLogs, PUSHED), "no Pushed on ship"); // --- Credential the taker: a real taker holds a KycNFT minted after KYC --- uint256 amountIn = 1_000e6; // 1,000 USDC deal(address(USDC), taker, amountIn); vm.mockCall(KYC_NFT, abi.encodeWithSelector(IERC20.balanceOf.selector, taker), abi.encode(uint256(1))); vm.prank(taker); USDC.approve(address(ROUTER), type(uint256).max); // --- Quote: simulate on a snapshot, then roll the state back --- uint256 snap = vm.snapshotState(); vm.prank(taker, taker); // (msg.sender, tx.origin): the credential guard reads tx.origin (, uint256 quotedOut,) = ROUTER.quote(order, address(USDC), address(WETH), amountIn, takerTraits); vm.revertToState(snap); assertGt(quotedOut, 0, "quote returned zero"); // --- Swap (emits Pulled + Pushed + Swapped) --- uint256 takerWethBefore = WETH.balanceOf(taker); vm.recordLogs(); vm.prank(taker, taker); (, uint256 amountOut,) = ROUTER.swap(order, address(USDC), address(WETH), amountIn, takerTraits); assertEq(amountOut, quotedOut, "swap != quote"); assertEq(WETH.balanceOf(taker), takerWethBefore + amountOut, "taker did not receive WETH"); Vm.Log[] memory swapLogs = vm.getRecordedLogs(); assertTrue(_seen(swapLogs, PULLED), "no Pulled"); assertTrue(_seen(swapLogs, PUSHED), "no Pushed on swap"); assertTrue(_seen(swapLogs, SWAPPED), "no Swapped"); // --- Dock: close the strategy for all tokens (emits Docked) --- vm.recordLogs(); vm.prank(maker); AQUA.dock(address(ROUTER), strategyHash, tokens); Vm.Log[] memory dockLogs = vm.getRecordedLogs(); assertTrue(_seen(dockLogs, DOCKED), "no Docked"); (uint248 wethBal,) = AQUA.rawBalances(maker, address(ROUTER), strategyHash, address(WETH)); assertEq(wethBal, 0, "balance not cleared after dock"); } function _seen(Vm.Log[] memory logs, bytes32 topic0) internal pure returns (bool) { for (uint256 i = 0; i < logs.length; i++) { if (logs[i].topics.length > 0 && logs[i].topics[0] == topic0) return true; } return false; } } ``` ## 2. TypeScript script Save as `strategy.ts`. This builds the identical strategy with the SDK (`AquaXYCAmmStrategy` -> `Order.new` -> `aqua.ship` -> `swapVm.quote`/`swap` -> `aqua.dock`) and broadcasts through your own node. Note: use the current SDK releases (`@1inch/aqua-sdk` 0.3.0+, `@1inch/swap-vm-sdk` 0.4.0+) -- their `AQUA_CONTRACT_ADDRESSES`/`AQUA_SWAP_VM_CONTRACT_ADDRESSES` constants carry the canonical vanity addresses. Older releases (0.1.1 / 0.2.2) return superseded addresses; if you cannot upgrade, take the registry and router addresses from the [Verified Contract Addresses](/portal/documentation/aqua/reference/verified-contract-addresses) page. ```solidity // strategy.ts - Aqua "Strategy template" reference flow. // Ship an XYC strategy, quote + swap as a credentialed taker, then dock. // Everything is broadcast through YOUR OWN RPC endpoint - never a hosted gateway. // // pnpm add @1inch/aqua-sdk @1inch/swap-vm-sdk viem dotenv // pnpm add -D ts-node typescript // npx ts-node strategy.ts import 'dotenv/config' import { createWalletClient, createPublicClient, http, decodeFunctionResult, isHex, type Hex, } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { mainnet } from 'viem/chains' import { AquaProtocolContract, AQUA_CONTRACT_ADDRESSES, ShippedEvent, PushedEvent, PulledEvent, DockedEvent, } from '@1inch/aqua-sdk' import { AQUA_SWAP_VM_CONTRACT_ADDRESSES, SwapVMContract, SwappedEvent, AquaXYCAmmStrategy, Order, MakerTraits, TakerTraits, Address, NetworkEnum, ABI, } from '@1inch/swap-vm-sdk' // --- Config: your RPC + two funded keys (maker provides liquidity, taker swaps) --- const RPC_URL = process.env.RPC_URL as string // e.g. https://your-node.example/eth const MAKER_PK = process.env.MAKER_PRIVATE_KEY as Hex const TAKER_PK = process.env.TAKER_PRIVATE_KEY as Hex if (!RPC_URL) throw new Error('Set RPC_URL to your own node') if (!isHex(MAKER_PK) || !isHex(TAKER_PK)) throw new Error('Set MAKER_PRIVATE_KEY / TAKER_PRIVATE_KEY') const chainId = NetworkEnum.ETHEREUM // 1 const WETH = new Address('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') const USDC = new Address('0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48') const KYC_NFT = new Address('0x26FFc7D378E8e49Be2c483295A3e3E511F96a468') const AQUA_ADDR = AQUA_CONTRACT_ADDRESSES[chainId] // registry (Address) const aqua = new AquaProtocolContract(AQUA_ADDR) const swapVm = new SwapVMContract(AQUA_SWAP_VM_CONTRACT_ADDRESSES[chainId]) // AquaSwapVMRouter // --- Transport: your RPC only --- const transport = http(RPC_URL) const maker = privateKeyToAccount(MAKER_PK) const taker = privateKeyToAccount(TAKER_PK) const makerClient = createWalletClient({ account: maker, chain: mainnet, transport }) const takerClient = createWalletClient({ account: taker, chain: mainnet, transport }) const pub = createPublicClient({ chain: mainnet, transport }) const ERC20_APPROVE = [{ type: 'function', name: 'approve', stateMutability: 'nonpayable', inputs: [{ name: 'spender', type: 'address' }, { name: 'value', type: 'uint256' }], outputs: [{ name: '', type: 'bool' }], }] as const async function main() { // 1) Build a credential-gated XYC strategy. // withTxOriginAccessToken() prepends onlyTxOriginTokenBalanceNonZero(KYC_NFT): // only takers whose tx.origin holds a KycNFT may swap against this liquidity. const program = AquaXYCAmmStrategy.new() .withTxOriginAccessToken(KYC_NFT) .build() const order = Order.new({ maker: new Address(maker.address), program, traits: MakerTraits.default(), }) const strategy = order.encode() // HexString == abi.encode(Order) const strategyHash = AquaProtocolContract.calculateStrategyHash(strategy) const usdcSeed = 15_000n * 10n ** 6n const wethSeed = 5n * 10n ** 18n // 2) Maker approves the Aqua registry (Aqua pulls straight from the maker wallet). await makerClient.writeContract({ address: USDC.toString() as Hex, abi: ERC20_APPROVE, functionName: 'approve', args: [AQUA_ADDR.toString() as Hex, usdcSeed], }) await makerClient.writeContract({ address: WETH.toString() as Hex, abi: ERC20_APPROVE, functionName: 'approve', args: [AQUA_ADDR.toString() as Hex, wethSeed], }) // 3) Ship: registers the strategy and its virtual balances. app == the router. const shipTx = aqua.ship({ app: new Address(swapVm.address.toString()), strategy, amountsAndTokens: [ { token: USDC, amount: usdcSeed }, { token: WETH, amount: wethSeed }, ], }) const shipHash = await makerClient.sendTransaction({ to: shipTx.to as Hex, data: shipTx.data as Hex, value: shipTx.value, }) const shipRcpt = await pub.waitForTransactionReceipt({ hash: shipHash }) for (const log of shipRcpt.logs) { tryParse(() => console.log('Shipped', ShippedEvent.fromLog(log))) tryParse(() => console.log('Pushed ', PushedEvent.fromLog(log))) } // 4) Taker approves the router for tokenIn, then quotes. // The taker's tx.origin must hold a KycNFT or the guard reverts. await takerClient.writeContract({ address: USDC.toString() as Hex, abi: ERC20_APPROVE, functionName: 'approve', args: [swapVm.address.toString() as Hex, usdcSeed], }) const amountIn = 1_000n * 10n ** 6n // 1,000 USDC const swapParams = { order, tokenIn: USDC, tokenOut: WETH, amount: amountIn, takerTraits: TakerTraits.default(), } // quote is a simulation: eth_call, decode (amountIn, amountOut, orderHash). const q = swapVm.quote(swapParams) const sim = await pub.call({ account: taker.address, to: q.to as Hex, data: q.data as Hex }) const [, quotedOut] = decodeFunctionResult({ abi: ABI.SWAP_VM_ABI, functionName: 'quote', data: sim.data as Hex, }) as unknown as [bigint, bigint, Hex] console.log('quoted amountOut (WETH):', quotedOut) // 5) Swap: broadcast through your RPC. const s = swapVm.swap(swapParams) const swapHash = await takerClient.sendTransaction({ to: s.to as Hex, data: s.data as Hex, value: s.value, }) const swapRcpt = await pub.waitForTransactionReceipt({ hash: swapHash }) for (const log of swapRcpt.logs) { tryParse(() => console.log('Pulled ', PulledEvent.fromLog(log))) tryParse(() => console.log('Pushed ', PushedEvent.fromLog(log))) tryParse(() => console.log('Swapped', SwappedEvent.fromLog(log))) } // 6) Dock: close the strategy for every shipped token. const dockTx = aqua.dock({ app: new Address(swapVm.address.toString()), strategyHash, tokens: [USDC, WETH], }) const dockHash = await makerClient.sendTransaction({ to: dockTx.to as Hex, data: dockTx.data as Hex, value: dockTx.value, }) const dockRcpt = await pub.waitForTransactionReceipt({ hash: dockHash }) for (const log of dockRcpt.logs) { tryParse(() => console.log('Docked ', DockedEvent.fromLog(log))) } } function tryParse(fn: () => void) { try { fn() } catch { /* log is from a different event - skip */ } } main().catch((e) => { console.error(e); process.exit(1) }) ``` :::note The maker approves the **Aqua registry** (not the router) for its tokens, because `Aqua.pull()` calls `transferFrom` straight from the maker wallet. The taker approves the **router** for its input token. Getting these two approvals backwards is the most common first-run failure. ::: ## 3. How to run ### Foundry ```bash # Install Foundry via the official installer at https://getfoundry.sh, then: foundryup forge test --fork-url $RPC_URL --match-contract StrategyTemplateTest -vvv ``` Required `foundry.toml` - the Aqua/SwapVM contracts compile with **solc 0.8.30**, the **cancun** EVM target, the **optimizer**, and **viaIR** enabled: ``` [profile.default] solc = "0.8.30" evm_version = "cancun" via_ir = true optimizer = true optimizer_runs = 1000000 fs_permissions = [{ access = "read", path = "./" }] ``` :::info Add `forge-std` with `forge install foundry-rs/forge-std`. `--fork-url` must point at a full archive-capable mainnet RPC that _you_ control. ::: ### TypeScript ```bash pnpm add @1inch/aqua-sdk @1inch/swap-vm-sdk viem dotenv pnpm add -D ts-node typescript npx ts-node strategy.ts ``` Provide a `.env` with your own endpoint and two funded keys: ``` RPC_URL=https://your-node.example/eth MAKER_PRIVATE_KEY=0x... # provides liquidity TAKER_PRIVATE_KEY=0x... # holds a KycNFT, swaps ``` ## Adapting the strategy The `AquaXYCAmmStrategy` builder composes further instructions before the final `xycSwapXD`. Common adjustments: - **Concentrated liquidity** - `AquaXYCAmmStrategy.newConcentrate({ rawPriceMin, rawPriceMax })` (price = tokenGt/tokenLt, scaled by 1e18). - **LP swap fee** - `.withFeeTokenIn(bps)` (the only non-zero fee in v1). Fees use the 1e9 base, so the builder's `FlatFeeArgs.fromBps` maps `fee = bps * 100000` (10000 bps = 100%): `.withFeeTokenIn(30)` = 30 bps = 0.30% = `3000000`. - **MEV decay** - `.withDecayPeriod(seconds)`. - **Permissionless pool** - drop `.withTxOriginAccessToken(...)` (and the `0x21 0x14 ...` prefix in Solidity). :::info Only opcodes in the deployed _Aqua subset_ are executable on-chain today: Controls, XYCSwap, XYCConcentrate, Decay, Fee, PeggedSwap and Extruction. The SDK can encode the full Swap VM instruction set, but programs using instructions outside that subset will not run on current Aqua deployments. ::: https://business.1inch.com/portal/assets/docs-v2/aqua/getting-started/automate-and-agentic-liquidity.md Aqua is a good substrate for headless and agent-driven liquidity because a maker keeps custody the whole time: the SDK builds calldata, the maker's own wallet signs, and you broadcast it through your own RPC. No 1inch component ever holds keys or tokens. ## The headless maker loop (live today, zero build) 1. **Monitor** strategy health and coverage via your own RPC and the on-chain event log. 2. **Build**`ship()`/`dock()` calldata with `@1inch/aqua-sdk` (program + predicted `strategyHash`). 3. **Sign** in the maker wallet. 4. **Broadcast** the signed transaction through your own RPC provider. 5. **Confirm** via `Shipped`/`Docked`/`Pulled`/`Pushed`/`Swapped` events. ## Keeper / re-ship on illiquidity Underfunded strategies are never paused or liquidated on-chain -- they simply stop filling (a Coverage effect). Because strategies are immutable, the rebalance primitive is `dock()` then `ship()`, producing a new `strategyHash`. LP fees auto-compound via `Pushed`, so an automated keeper mostly re-ships on illiquidity and recenters ranges. ## Hosted APIs, MCP and paid access :::info Aqua's hosted/gated REST APIs, the MCP servers, x402 paid access, and private-mempool broadcast (Transaction Gateway) are part of [1inch Business](https://business.1inch.com/portal/documentation) (KYB/KYC-gated, payable). These docs cover the permissionless on-chain + SDK path only; broadcast the calldata you build here through your own RPC. For hosted infrastructure or agent tooling, see 1inch Business. Read-only Aqua analytics and maker position writes (`create_position` with a preview-then-confirm flow, and `close_position`) are exposed to AI agents through the [`aqua` tool](/portal/documentation/ai-integration/tools-reference#aqua) on the 1inch MCP server. ::: :::note **Honest caveat.** The launch taker gate reads `tx.origin`, so agentic _takers_ using smart accounts or 4337 bundlers cannot fill today. Agentic _makers_ are unaffected -- headless algo-LP works now. Computed metrics are estimates from public data, not official 1inch figures. ::: https://business.1inch.com/portal/assets/docs-v2/aqua/getting-started/liquidity-provider-and-taker-guide.md Solidity-native reference for liquidity providers and takers who interact directly with the Aqua registry and AquaApp contracts, with no SDK required. **Position = strategy.** This Solidity-native guide uses **strategy**, the on-chain name for what the 1inch dApp shows as a **position** -- the same object. The code identifiers keep that name (`strategyHash`, the `Strategy` struct, the `/strategies` endpoint). **Audience.** The maker side of this guide is permissionless: anyone can approve, `ship()`, and `dock()` with no credential. The taker side is gated at launch: filling strategies -- including every strategy created through the 1inch dApp -- requires the per-chain Aqua `KycNFT` (the **Aqua resolver NFT**), issued to KYB-verified 1inch Resolvers. If you are a **resolver or B2B integrator** who wants to fill Aqua liquidity, request contract whitelisting first via **csm@1inch.com** -- see [Becoming a resolver](../liquidity-layer/access-resolvers-and-pathfinder#can-i-take-becoming-a-resolver) for the request checklist. This applies to existing 1inch resolvers too: Aqua is enabled only on request, after manual review -- unlike Limit Order, Fusion, and Fusion+ contracts, it cannot be added self-service in the [Resolver profile](/portal/profile/resolver). :::info **Strategy model** **Immutable**: once shipped, a strategy's parameters and initial allocation are fixed and cannot be edited in place. **Self-custodial**: your tokens stay in your wallet under a revocable allowance to Aqua; the protocol holds zero tokens and takes no custody. **Rebalancing**: call `dock()` then `ship()` to change parameters (no token transfers needed). **Simpler integration**: immutability means fewer moving parts for takers and integrators to reason about. ::: --- ## For liquidity providers ### 1. Approve tokens to Aqua (one-time per token) ```solidity // Grant max allowance -- Aqua only pulls when a swap executes token.approve(address(aqua), type(uint256).max); ``` Aqua never takes custody. The allowance is drawn only when `pull()` fires during a swap, and only for the exact fill amount. Your tokens stay in your wallet. The allowance is granted per token and per chain, and you can revoke it at any time by setting it back to `0`. Granting `type(uint256).max` is an optional convenience; you can instead approve a bounded amount. --- ### 2. Ship a strategy Encode the strategy struct for your target AquaApp (each AquaApp defines its own struct shape), then call `ship()`: ```solidity // Example: XYCSwap two-token strategy XYCSwap.Strategy memory strategy = XYCSwap.Strategy({ maker: msg.sender, token0: DAI, token1: USDC, feeBps: 30, // 0.3% salt: bytes32(0) }); address[] memory tokens = new address[](2); tokens[0] = DAI; tokens[1] = USDC; uint256[] memory amounts = new uint256[](2); amounts[0] = 1000e18; // 1000 DAI amounts[1] = 1000e6; // 1000 USDC bytes32 strategyHash = aqua.ship( address(xycSwapApp), abi.encode(strategy), tokens, amounts ); ``` On success, the strategy is registered and available to fill immediately, and the `Shipped` event is emitted. No tokens move. --- ### 3. Check virtual balances Virtual balances are an internal counter in `Aqua.sol`; they track a strategy's committed liquidity without moving any tokens out of your wallet. ```solidity // Validate tokens are in the strategy and read balances (uint256 balanceDAI, uint256 balanceUSDC) = aqua.safeBalances( maker, address(xycSwapApp), strategyHash, DAI, USDC ); // Raw read without validation (use for debugging) (uint248 raw, uint8 count) = aqua.rawBalances( maker, address(xycSwapApp), strategyHash, DAI ); ``` --- ### 4. Dock a strategy Docking revokes the strategy's virtual balances and stops it from filling swaps. Your tokens are already in your wallet, so `dock()` moves nothing. ```solidity address[] memory tokens = new address[](2); tokens[0] = DAI; tokens[1] = USDC; aqua.dock(address(xycSwapApp), strategyHash, tokens); ``` Emits `Docked(maker, app, strategyHash)`. --- ### 5. Update parameters (dock, then re-ship) Strategies are immutable once shipped. To change any parameter (fee, amounts, pair), dock the old strategy and ship a new one. This is how a rebalance works: `dock()` followed by `ship()`. ```solidity // 1. Dock existing strategy aqua.dock(address(xycSwapApp), strategyHash, tokens); // 2. Ship with updated parameters -- no token transfer needed XYCSwap.Strategy memory newStrategy = XYCSwap.Strategy({ maker: msg.sender, token0: DAI, token1: USDC, feeBps: 10, // updated to 0.1% salt: bytes32(0) }); bytes32 newHash = aqua.ship( address(xycSwapApp), abi.encode(newStrategy), tokens, amounts ); ``` The new `strategyHash` differs from the old one, so integrators referencing the old hash must update. --- ## For takers :::info **Most strategies require the Aqua resolver NFT.** A strategy's program can include a Conditional Access rule that requires the taker to hold a specific token or NFT, checked on-chain before the fill. Every strategy created through the 1inch dApp carries a `tx.origin` token-balance gate bound to the Aqua `KycNFT` (the **Aqua resolver NFT**) -- the taker credential minted to KYB-verified 1inch Resolvers -- so the swap reverts unless `tx.origin` holds that token. Because the check reads `tx.origin`, contract wallets, multisigs, and ERC-4337 bundlers cannot pass it. When you call a strategy directly on-chain, you must satisfy whatever access rule that strategy encodes. Resolvers and B2B integrators request contract whitelisting via **csm@1inch.com** -- see [Becoming a resolver](../liquidity-layer/access-resolvers-and-pathfinder#can-i-take-becoming-a-resolver). This applies to existing 1inch resolvers too -- Aqua is enabled only on request, after manual review, and cannot be added self-service in the [Resolver profile](/portal/profile/resolver). ::: ### 1. Implement the callback interface When calling a callback-based AquaApp, your contract must implement the app's callback interface and call `push()` inside it to deliver the input token: ```solidity contract Trader is IAquaAppSwapCallback { function aquaAppSwapCallback( address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut, address maker, address app, bytes32 strategyHash, bytes calldata takerData ) external override { // Transfer tokenIn to complete the swap (requires token approval) // This is the ONLY appropriate use of push() - during swap execution IERC20(tokenIn).forceApprove(aqua, amountIn); aqua.push(maker, app, strategyHash, tokenIn, amountIn); } } ``` --- ### 2. Execute a swap Call the AquaApp's swap function directly. There is no signature round-trip: the taker callback fires automatically during execution. ```solidity // swapExactIn -- swap a fixed input amount uint256 amountOut = xycSwapApp.swapExactIn( strategy, true, // zeroForOne: token0 -> token1 1e18, // amountIn (1 DAI) 0.99e18, // amountOutMin (slippage tolerance) address(this), // recipient "" // takerData (forwarded to callback) ); ``` Swap reverts if: - `amountOut < amountOutMin` (slippage exceeded) - The maker's real wallet balance is insufficient (`pull()` reverts) - The callback does not deliver the correct `amountIn` - A Conditional Access rule on the strategy is not satisfied --- ## Key invariants | Property | Behavior | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Self-custodial | Tokens never leave the maker's wallet until `pull()` fires, and then only for the exact fill amount. The protocol holds zero tokens. | | Atomic fill | A swap completes fully or reverts, with no partial fills and no bad debt | | Coverage | If the maker's real balance is below the strategy's virtual commitment, `pull()` reverts and the fill fails. The strategy keeps quoting; it is never paused on-chain or liquidated. | | Fee auto-compounding | Input tokens delivered via `push()` immediately expand the strategy's virtual balance | :::info Swap fees accrue to the maker and are not guaranteed; there is no guaranteed yield, APY, or return. Aqua is self-custodial and non-custodial, but smart-contract risk and the risk of granting a token allowance still apply. ::: --- ## Related - [Smart contract](../reference/smart-contract): complete function reference for `Aqua.sol` and `AquaApp` - [Build an AquaApp](./build-an-aquaapp): implement a custom AquaApp with swap logic - [Strategy lifecycle](../liquidity-layer/strategy-lifecycle): ship / dock / pull / push state machine - [Virtual balances](../liquidity-layer/virtual-balances): how shared liquidity and virtual accounting work - [Contract addresses](../reference/contract-addresses): Aqua registry address across all 13 supported chains https://business.1inch.com/portal/assets/docs-v2/aqua/liquidity-layer/core-concepts.md 1inch Aqua is a shared liquidity layer. Its swap engine is **SwapVM**, and its on-chain registry is the **Aqua** contract (`Aqua.sol`, deployed as the _Aqua registry_). This page covers the concepts you need before writing a strategy: the maker and taker roles, what a strategy is, how virtual balances keep liquidity self-custodial, the strategy lifecycle, and the Shared Liquidity Ratio. :::info **Positions and strategies are the same thing.** In the 1inch dApp you create a **position**. On-chain, and throughout the SDK, the API and these developer docs, that same object is called a **strategy**. The code identifiers are fixed and cannot be renamed: `strategyHash`, the `Strategy` struct/DTO, the `/strategies` endpoint and `ship()`/`dock()`. These developer docs use **strategy** so the prose matches the code you call. Read it as the dApp's **position**. ::: :::info Aqua is self-custodial. A maker's tokens stay in the maker's own wallet under a revocable ERC-20 allowance that is granted per chain and per token. The protocol holds zero tokens and never borrows them. Standard smart-contract and token-approval risk still applies. ::: ## 1. Maker and taker **Maker** is the liquidity provider. A maker allocates tokens to one or more strategies by calling `ship()` on the Aqua registry. The maker's tokens are never transferred to Aqua. Instead, Aqua records a **virtual allocation**: an internal counter that represents a claim against the maker's existing ERC-20 allowance. The maker keeps full custody and can continue to use the same tokens for governance, money-market collateral, or any other DeFi purpose. Becoming a maker is permissionless. **Taker** is the swap initiator. A taker references an active strategy and fills against it by calling `swap()` on the AquaApp (the deployed swap router is `AquaSwapVMRouter`). The taker supplies the input token, and the maker supplies the output token directly from their wallet. The fill is atomic: if any condition fails (insufficient maker balance, slippage exceeded, expired order) the entire transaction reverts with no partial state change. At launch, takers are gated: the app checks a `KycNFT` access credential on-chain at swap time, so only approved takers can fill. Makers are not gated. > A maker and a taker are not necessarily different entities. A smart contract can act as both, providing liquidity as a maker while routing swaps as a taker. --- ## 2. Strategy A **strategy** (shown as a **position** in the 1inch dApp) is the central unit of Aqua. It is the on-chain artifact a maker ships to express: _"I will provide this liquidity, under these pricing rules, with these permissions."_ A strategy is modeled as four layers: | Layer | Answers | Where it lives | | ------------------ | --------------------------- | -------------------------------------------------------------------------------------------------- | | **Identification** | Which strategy is this? | `(maker, app, strategyHash)` triple | | **Registry state** | What liquidity backs it? | `balances[maker][app][strategyHash][token]` in `Aqua.sol` | | **Program** | How does it price and fill? | SwapVM bytecode: an ordered sequence of opcodes inside a SwapVM Order, ABI-encoded as the Strategy | | **Configuration** | Who authorized it and when? | `MakerTraits` + `TakerTraits` headers | With SwapVM, the Strategy is the ABI-encoded form of a SwapVM **Order**. The Order wraps a **Program**. SwapVM executes the Program instructions, not the Strategy. Aqua only manages balance accounting against the strategy hash, which it treats as opaque bytes. A strategy is **immutable once shipped**. To change parameters, dock the old strategy and ship a new one, which produces a new `strategyHash`. --- ## 3. Virtual balances Aqua never takes custody of tokens. Instead, `Aqua.sol` maintains a **virtual balance**, an internal counter that represents a claim against the maker's ERC-20 allowance to the registry contract. Tokens remain in the maker's wallet at all times, and the protocol's own token balance is always zero. The on-chain storage is a four-level nested mapping: ```solidity Maker Address -> Application Address -> Strategy Hash (bytes32) -> Token Address -> Virtual Balance ``` A concrete example: a maker holds 10 WETH in their wallet with a 10 WETH allowance granted to `Aqua.sol`. ``` Maker wallet: 10 WETH (allowance to Aqua: 10 WETH) Strategy A: 4 WETH virtual Strategy B: 3 WETH virtual Unallocated: 3 WETH ``` Both strategies are live at the same time. The same 10 WETH backs both of them, and this is what **"shared liquidity"** means. A virtual balance is a ceiling on what a strategy can make available, not a promise of returns. Every fill still draws from the maker's real wallet balance. **What happens during a swap** When a taker fills against Strategy A: 1. `pull()`: the AquaApp calls Aqua to transfer the output token (for example 1 WETH) directly from the maker's wallet to the taker. Aqua decrements Strategy A's virtual balance by 1 WETH. 2. `push()`: the AquaApp calls Aqua to transfer the input token (for example 3,000 USDC) from the taker to the maker's wallet. Aqua increments Strategy A's virtual balance for USDC by 3,000. This increment is **immediate**, so received tokens (including swap fees) auto-compound into available liquidity with no manual rebalancing step. Neither operation moves tokens through Aqua itself. Aqua only updates the internal counter and triggers the ERC-20 transfer directly between maker and taker. ### Coverage and underfunded strategies If the maker's actual wallet balance falls below their virtual commitment, `pull()` reverts. Aqua keeps quoting prices from virtual balances, because it does not check real balances at quote time, so price continuity is preserved. Swaps simply stop filling until the maker's wallet is topped up. This is an economic effect (temporary illiquidity), not bad debt or protocol insolvency, and there is no on-chain pause and no liquidation. Makers are advised to `dock()` strategies that become chronically underfunded to avoid accumulating adverse price exposure while a strategy cannot fill. --- ## 4. The lifecycle Every strategy moves through states controlled by four entry points on `Aqua.sol`: ``` [Not shipped] | | ship(app, strategy, tokens, amounts) v [Active] | | pull() / push() execute on every swap | | dock(app, strategyHash, tokens) v [Docked] | | re-ship with updated params v back to [Active] ``` | Verb | Who calls it | What it does | | ------ | ------------ | -------------------------------------------------------------------------------------- | | `ship` | Maker | Allocates virtual balances; the strategy becomes fillable | | `dock` | Maker | Revokes virtual balances instantly; strategy stops accepting swaps; **no tokens move** | | `pull` | AquaApp | Transfers output token from maker's wallet to taker; decrements virtual balance | | `push` | AquaApp | Transfers input token from taker to maker's wallet; increments virtual balance | `ship()` and `dock()` are pure configuration operations. There is no token transfer, no withdrawal delay, and no liquidity migration. A maker can move from zero liquidity to a live strategy, or exit a strategy entirely, in a single transaction. `pull()` and `push()` are never called directly by makers or takers. They are internal calls made by the AquaApp contract during swap execution, where a fill runs `pull()` and `push()` atomically in one transaction. See [Strategy lifecycle](./strategy-lifecycle) for the full state machine, event log (`Shipped` / `Docked` / `Pulled` / `Pushed` / `Swapped`), and SDK helpers. --- ## 5. Shared Liquidity Ratio The **Shared Liquidity Ratio (SLR)** is Aqua's core efficiency metric. It measures how much liquidity a maker makes available across strategies relative to the wallet equity backing it. Because the same tokens can back several strategies at once, SLR is always at least 1: ``` sum of liquidity made available across all strategies SLR = --------------------------------------------------------- 1 sum of wallet equity backing those strategies ``` SLR describes availability and capital efficiency. It is **not** leverage, margin, or borrowing, and no tokens are ever borrowed. Any multiplier it implies is a theoretical ceiling that is always capped by the maker's real wallet balance, and it is never a forecast of returns. #### An example A maker holds \$1,000 in their wallet and grants the corresponding allowance. They ship three strategies, each able to reference up to that same \$1,000 of liquidity: 1. The wallet equity is \$1,000. 2. The three strategies together reference \$3,000 of available liquidity, drawn from the same \$1,000. That is an SLR of **3x**: \$3,000 of concurrently available liquidity from \$1,000 of wallet equity, achieved purely by sharing one balance across strategies. No borrowing is involved, and total fills can never exceed the real \$1,000 in the wallet at any instant. This works because swap activity is asynchronous. Different strategies fill at different times, so one wallet balance can service many strategies' occasional fills without collision. While any single strategy may sit idle most of the time (consistent with on-chain data across major AMMs), the same idle capital stays available to every strategy it backs, turning it into concurrent fee-earning strategies. Swap fees are not guaranteed. > _The Shared Liquidity Ratio is an efficiency metric, not a safety invariant._ SLR measures how efficiently wallet equity is shared across strategies. Safety properties are enforced separately: - **ERC-20 allowance cap**: a strategy's virtual balance cannot exceed the maker's total ERC-20 allowance to `Aqua.sol`, which gives explicit per-token risk control. The allowance is per chain and per token, and it is revocable at any time. - **Atomic** `pull()` **fill**: if the maker's real wallet balance is insufficient at swap execution time, `pull()` reverts. There are no partial fills and no bad debt. - **Underfunding is recoverable**: underfunded strategies stop filling but do not create protocol-level losses. The maker can dock and re-evaluate. --- ## What to read next - [Strategy](./strategy): the full four-layer model - [Strategy lifecycle](./strategy-lifecycle): lifecycle state machine with events and SDK parsers - [Quickstart](../getting-started/quickstart): ship your first strategy --- ## Taker access :::note **Taker access gate (checked at swap time).** At launch every dApp strategy carries the Controls opcode `_onlyTxOriginTokenBalanceNonZero`, which reverts `TxOriginTokenBalanceIsZero` unless `balanceOf(tx.origin) > 0`. It is evaluated at _swap time, not ship time_, so a strategy can be live yet unfillable until a permitted taker holds the credential. Because it reads `tx.origin`, smart-contract wallets, multisigs and ERC-4337 bundlers cannot pass it as takers today. Permitted takers at launch are KYB-verified 1inch Resolvers. See [Access, resolvers & Pathfinder](./access-resolvers-and-pathfinder). ::: https://business.1inch.com/portal/assets/docs-v2/aqua/liquidity-layer/strategy.md A **strategy** is the unit of liquidity allocation in Aqua. It is the on-chain artifact a maker ships to declare, _"I will provide this liquidity, under these pricing rules."_ A taker references a strategy when asking, _"Swap against this liquidity on these terms."_ Every protocol operation (`ship`, `dock`, `pull`, `push`, `quote`, and `swap`) acts on a strategy. :::info **Position = strategy.** This page uses **strategy**, the on-chain and API name for what the 1inch dApp shows as a **position**. They are the same object; the code identifiers cannot be renamed (`strategyHash`, the `Strategy` struct/DTO, the `/strategies` endpoint). ::: --- ## What a strategy is A strategy is an ABI-encoded structure, opaque bytes from Aqua's perspective, that carries all parameters defining its swap behavior. The registry treats the strategy body as opaque bytes and never parses them. Aqua manages only balance accounting; all pricing and execution logic lives inside the AquaApp. When built with SwapVM, the Aqua swap engine, the strategy is the ABI-encoded form of a SwapVM **Order**. The Order wraps a **Program**, an ordered sequence of opcodes and instructions that AquaSwapVMRouter (the deployed SwapVM router) executes at swap time. Aqua does not interpret the Program; it only accounts for virtual balances keyed by the strategy hash. ``` Program (opcodes + args) -> SwapVM Order -> Aqua Strategy (ABI-encoded) ``` This separation between **accounting** (managed by `Aqua.sol`) and **swap logic** (implemented by AquaApps or SwapVM Programs) is the core architectural principle. The same virtual balance infrastructure can back any strategy type, such as constant product AMMs, concentrated liquidity, or pegged and reward-bearing curves, without Aqua itself needing to understand any of them. --- ## Identity: how a strategy is addressed Every strategy is uniquely identified by the triple **(maker, app, strategyHash)**: - **maker** -- the EOA or contract that shipped the liquidity. - **app** -- the deployed AquaApp contract that governs swap logic. For SwapVM strategies this is `AquaSwapVMRouter`, the single router that serves all three strategy types (`xyc`, `concentrated`, and `pegged`); the type is selected by the program inside the strategy bytes, not by a separate app contract. Developers can also deploy a custom AquaApp, which defines its own strategy struct shape. - **strategyHash** -- a `bytes32` derived from the hash of the strategy's immutable parameters. Because the hash encodes all parameters, the same inputs always produce the same hash, and any parameter change produces a different hash entirely. ```solidity bytes32 strategyHash = keccak256(abi.encode(strategy)); ``` The `(maker, app, strategyHash)` triple is the key Aqua uses throughout the registry. It is how `ship()`, `dock()`, `pull()`, and `push()` locate the correct virtual balance slot. --- ## Registry: what liquidity backs a strategy Inside `Aqua.sol`, virtual balances are tracked in a four-level nested mapping: ```solidity Maker Address -> Application Address -> Strategy Hash (bytes32) -> Token Address -> Balance ``` The storage slot `balances[maker][app][strategyHash][token]` holds the virtual balance for each token in the strategy. These are claims against the maker's revocable ERC-20 allowance to `Aqua.sol`, not custody balances. The allowance is granted per chain and per token, and the tokens stay in the maker's wallet at all times; the protocol itself holds zero tokens. Shipping a strategy populates these slots. Docking empties them. Because each strategy holds only a claim on a slice of the maker's total allowance, **one per-token approval on a chain can back many strategies on that chain in parallel**. This is the "shared" in Aqua's shared liquidity layer. Read balances via: - `rawBalances` -- raw read, does not validate that the token belongs to the strategy. - `safeBalances` -- reverts if the queried token is not registered in the strategy. See Virtual Balances for the full on-chain layout and packed struct details. --- ## Swap logic: how a strategy prices and fills swaps Swap logic is entirely the responsibility of the AquaApp. Aqua itself has no knowledge of pricing curves, fee calculations, or AMM invariants. When a taker calls `swap()`, the AquaApp runs its own logic (a constant product formula, a pegged curve, or a custom quote) and then calls `pull()` and `push()` on `Aqua.sol` to complete the transfer. For apps that use SwapVM, the execution logic is encoded as a bytecode **Program**: an ordered sequence of instructions the engine interprets against swap-state registers. The Program is wrapped in a SwapVM **Order**, which is ABI-encoded to produce the strategy. It is the Program instructions, not the strategy container, that SwapVM executes. The strategy is only the registered, immutable wrapper. See Order, SwapVM Instructions, and Program Model for the composition rules and bytecode format. All strategies, regardless of which AquaApp implements them, must preserve the core invariants: symmetry, additivity, quote/swap consistency, monotonicity, rounding in the maker's favor, balance sufficiency, and strategy liveness. --- ## Configuration: authorization and permissions Each strategy carries two configuration headers that govern who can swap against it and under what conditions: - **MakerTraits** -- a 256-bit packed header the engine reads before executing. It carries authorization flags, hook indexes, receiver address, epoch and nonce fields, and time bounds. The `useAquaInsteadOfSignature` flag is particularly important: when set to `true`, the engine skips signature verification and treats the strategy as authorized by virtue of being shipped to Aqua's registry. When `false`, the strategy functions as an isolated SwapVM order authorized by an EIP-712 signature. - **TakerTraits** -- a variable-length payload the taker supplies at execution time to parameterize the swap (recipient override and extension data slices). Aqua fills are all-or-nothing; there are no order-level partial fills. --- ## Lifecycle Strategies pass through three states: ``` [Not shipped] | | ship(app, strategy, tokens, amounts) v [Active] | | pull() / push() execute on every swap | | dock(app, strategyHash, tokens) v [Docked] | | re-ship with updated params v back to [Active] ``` | State | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Not shipped** | The strategy struct exists off-chain but no registry slot is populated. It cannot be swapped against. | | **Active** | The maker called `ship()`; `balances[maker][app][strategyHash][token]` is populated; takers can swap via `pull()` and `push()`. | | **Docked** | The maker called `dock()`; virtual balances are revoked and registry slots are zeroed. No tokens move; the maker's wallet balance is unaffected. The hash remains known and can be re-shipped with fresh balances. | **A shipped strategy is completely immutable.** Parameters, execution logic, and configuration headers cannot change after shipping. To modify anything, dock the current strategy and ship a new one with the updated struct. Because a different struct always produces a different `strategyHash`, this design removes entire classes of parameter-manipulation vulnerabilities: immutable strategies have smaller attack surfaces, predictable behavior, and are fully auditable by hash alone. ### Activation is pure configuration `ship()` and `dock()` involve no token transfers, only virtual accounting updates. A maker can go from zero liquidity to a live, swappable strategy in a single transaction, with no withdrawal delay and no opportunity cost from exiting existing strategies. This lets a maker allocate or revoke liquidity in minutes rather than through a lengthy deposit-and-withdraw cycle. --- ## Completing a swap: pull() and push() When a taker executes a swap, the AquaApp completes the fill through two operations on `Aqua.sol`: | Operation | Caller | Effect | | --------- | ------- | --------------------------------------------------------------------------------------- | | `pull()` | AquaApp | Decreases virtual balance; transfers output token directly from maker's wallet to taker | | `push()` | AquaApp | Increases virtual balance; transfers input token from taker to maker's wallet | `pull()` checks the maker's actual wallet balance at execution time and **reverts if insufficient**, so every fill is atomic. A swap either completes in full or fails cleanly: no partial fills, no bad debt, no protocol insolvency. `push()` is auto-compounding by design. When input tokens arrive at the maker's wallet, the virtual balance for that token is immediately incremented, expanding available liquidity without any manual rebalancing step. Earned swap fees and received tokens instantly become available liquidity. ### Illiquidity behavior If the maker's wallet balance falls below their virtual commitment, `pull()` reverts. Aqua keeps quoting prices from virtual balances (it does not check real balances at quote time), which preserves price continuity, while swap execution fails until the maker's wallet is replenished. Underfunded strategies simply stop filling; this is temporary illiquidity, not bad debt, an on-chain pause, or protocol insolvency. Makers are strongly advised to `dock()` strategies that become chronically underfunded. During an illiquid period, if prices move unfavorably, the first executable swap when liquidity returns may lock in those adverse price movements, analogous to impermanent loss in a constant product AMM. For strategies with balance invariants such as the constant-product rule `x * y = k`, these movements remain bounded by the same mathematical limits as traditional pools. --- ## Reentrancy The `nonReentrantStrategy` modifier on AquaApp locks per `(maker, strategyHash)` using transient storage. Two strategies owned by the same maker can execute concurrently; the same strategy cannot re-enter itself during execution. See IAquaAppSwapCallback for the callback interface that completes the `push()` side of a swap. --- ## Observability The registry emits the following events over the strategy lifecycle: | Event | Emitted when | | --------- | ------------------------------------------------------ | | `Shipped` | Strategy is activated via `ship()` | | `Docked` | Strategy is closed via `dock()` | | `Pulled` | Output tokens are transferred from maker during a swap | | `Pushed` | Input tokens are delivered to maker during a swap | On each fill, `AquaSwapVMRouter` additionally emits `Swapped(orderHash, maker, taker, tokenIn, tokenOut, amountIn, amountOut)`, where `orderHash` equals the strategy's `strategyHash`. The `@1inch/aqua-sdk` package parses these via `ShippedEvent.fromLog(log)`, `DockedEvent.fromLog(log)`, and equivalent helpers. https://business.1inch.com/portal/assets/docs-v2/aqua/liquidity-layer/virtual-balances.md 1inch Aqua never takes custody of maker tokens. Instead, the Aqua registry keeps a **virtual balance**, a counter that records how much of a maker's ERC-20 allowance is currently allocated to each strategy. This counter is the mechanical heart of shared liquidity. Tokens stay in the maker's wallet, the protocol holds zero tokens, and the allowance that backs them is revocable per chain and per token. **Self-custodial by design.** A virtual balance is an internal counter inside `Aqua.sol`. It is not a deposit, a lock, or a transfer of ownership. Real tokens move only during an atomic fill, straight from the maker's wallet to the taker. Smart-contract and approval risk still apply. --- ## The core idea When a maker calls `ship()`, no tokens are transferred to the registry. Instead, `Aqua.sol` records: ```solidity balances[maker][app][strategyHash][token] = Balance { amount: uint248, // the virtual allocation in token base units tokensCount: uint8 // how many tokens belong to this strategy } ``` The real tokens remain in the maker's wallet. `ship()` only consumes part of the maker's existing ERC-20 allowance to the Aqua registry (`0x1111113ccf1426a8e30e2bff5e005d929bf6a90a`), never ownership of the tokens. During a swap: - `pull()` moves tokens _directly_ from the maker's wallet to the taker, and the registry decrements `amount`. - `push()` credits tokens from the taker to the maker's wallet, and the registry increments `amount`. The incremented balance is immediately available to the strategy, so tokens received through swaps expand the strategy's usable virtual balance without manual rebalancing. No intermediary custody, no wrapping, and zero tokens held by the protocol at rest. --- ## Why it enables shared liquidity Because tokens stay in the maker's wallet, a single ERC-20 allowance on a given chain can back **many strategies at once**: ``` Maker wallet: 10 WETH (allowance to Aqua.sol) Strategy A -- 3 WETH virtual Strategy B -- 4 WETH virtual Strategy C -- 2 WETH virtual Total allocated: 9 WETH (< 10, safe) ``` A maker shipping Strategy D must ensure the sum of all virtual allocations does not exceed the actual allowance at **execution time**. The registry does not enforce a hard cap at `ship()`. It enforces at fill time: if the maker's real balance is insufficient when `pull()` fires, the transaction reverts. Strategies keep quoting from virtual balances even when the maker is temporarily underfunded, so integrators should monitor real versus virtual balance ratios. Allowances are scoped **per chain and per token**. One approval on a chain backs many strategies on that chain; it does not span chains, and each token a strategy uses needs its own approval. The allowance stays revocable at any time. This differs from pooled-liquidity AMMs such as Uniswap v2/v3, where tokens are deposited into a pool contract and segregated per pool. In Aqua there are no deposits. One wallet balance can participate in many strategies at the same time. --- ## Shared Liquidity Ratio The **Shared Liquidity Ratio (SLR)** measures how much quotable liquidity a maker's wallet balance can make available across all of that wallet's strategies: ``` total liquidity made available across all strategies SLR = ---------------------------------------------------- >= 1 actual wallet balance ``` Example: a wallet holds 1,000 USDC and three strategies each reference up to 1,000 USDC of that same balance. That makes 3,000 USDC of availability backed by 1,000 USDC of real balance, an SLR of 3. The same balance can earn swap fees from every strategy it backs, because real utilization in any single strategy is usually low. The ratio is a **theoretical ceiling, not a forecast**. Only the real wallet balance can ever actually fill, no matter how many strategies reference it. Nothing is borrowed, leveraged, or held as collateral. The multiplier simply reflects that an idle balance can be made available in several places until it is used. The Shared Liquidity Ratio is an availability and efficiency metric, not a safety invariant. Multiple strategies may quote against the same virtual balance at once. Safety comes from the `pull()` execution model: if the maker's actual wallet balance is insufficient when a swap runs, `pull()` reverts (via `IERC20.transferFrom` failure) and the swap cannot complete, so Aqua never carries bad debt. While underfunded, the strategy simply stops filling until the wallet is topped up. That is an availability effect rather than a loss, and strategies are never liquidated. --- ## Reading balances Two view functions read the on-chain state: | Function | Behaviour | | ------------ | --------------------------------------------------------------------------------------------- | | rawBalances | Returns the raw storage value; does not validate that the token belongs to the strategy | | safeBalances | Reverts if the token was not registered in the strategy at `ship()` time (uses `tokensCount`) | Use `safeBalances` in production integrations, and `rawBalances` for debugging or querying arbitrary slots. --- ## Immutability consequence Because virtual balances are keyed by `(maker, app, strategyHash)` and `strategyHash = keccak256(abi.encode(strategy_struct))`, changing any field of the strategy struct would produce a different hash and therefore a different storage slot. That is why **strategies are immutable once shipped**: to change parameters, dock the old strategy (which zeros its storage slot) and ship a new one with the updated struct. --- ## Related - [Strategy](/portal/documentation/aqua/liquidity-layer/strategy) shows how virtual balances fit into the strategy model. - [Strategy Lifecycle](/portal/documentation/aqua/liquidity-layer/strategy-lifecycle) shows how `ship()` and `dock()` change the balance slot. https://business.1inch.com/portal/assets/docs-v2/aqua/liquidity-layer/strategy-lifecycle.md A strategy moves through three states, driven by four entry points on `Aqua.sol`: `ship()`, `pull()`, `push()`, and `dock()`. Swap execution itself is handled by SwapVM, the Aqua swap engine. Understanding this lifecycle is essential before building on Aqua. :::info Shipping a strategy never moves the maker's tokens. They stay in the maker's wallet under a revocable, per-chain, per-token ERC-20 allowance to `Aqua.sol`, and move only when a taker fills a swap atomically. The protocol holds zero tokens. If the maker's wallet no longer covers the strategy, the strategy simply stops filling; this is an economic effect, not an on-chain pause or liquidation. ::: --- ## State machine ``` [Not Shipped] | | maker calls ship(app, strategy, tokens, amounts) | -> emits Shipped event v [Active / Shipped] | | takers call quote() / swap() on the app | -> pull() fires: output tokens go maker -> taker | -> push() fires: input tokens go taker -> maker | | maker calls dock(app, strategyHash, tokens) | -> emits Docked event v [Docked] | | maker ships again with new params | (a new strategy; a docked strategy is never reactivated in place) v back to [Active / Shipped] ``` --- ## The four entry points ### `ship(app, strategy, tokens, amounts)` Registers a strategy and allocates virtual balances so it can be filled. The maker's tokens are not transferred. - `app`: the deployed app contract that executes swaps against this strategy. Aqua ships a single deployed app, the `AquaSwapVMRouter`, which serves every strategy type. The type (`xyc`, `concentrated`, or `pegged`) is selected by the program bytes inside the strategy, not by the app address. - `strategy`: ABI-encoded strategy struct (its shape is defined by the app's program). - `tokens`: ordered list of token addresses to allocate. - `amounts`: virtual amounts to allocate per token (parallel to `tokens`). On success: - `balances[maker][app][strategyHash][token]` is populated for each token. - The `Shipped(maker, app, strategyHash, strategy)` event is emitted, followed by a `Pushed` event per token for the initial allocation. - `strategyHash` is derived as `keccak256(abi.encode(strategy))` and is immutable. > The maker's tokens are **not** transferred. Shipping only relies on the ERC-20 allowance granted to `Aqua.sol`. For how virtual balances work, see [Virtual Balances](./virtual-balances). --- ### `pull(maker, strategyHash, token, amount, to)` Called by the app during a swap to move the maker's output token to the taker. - Transfers `amount` of `token` from the `maker`'s wallet to `to`. Reverts if the maker's wallet balance is insufficient. - Decrements `balances[maker][app][strategyHash][token]`. - Emits `Pulled(maker, app, strategyHash, token, amount)`. Makers never call `pull()` directly. The app calls it as part of `swap()`. --- ### `push(maker, app, strategyHash, token, amount)` Called by the app (via the `IAquaAppSwapCallback` callback) to receive the taker's input token. - Transfers `amount` of `token` from the taker (the caller of `swap()`) to `maker`. - Increments `balances[maker][app][strategyHash][token]`. LP swap fees auto-compound here. - Emits `Pushed(maker, app, strategyHash, token, amount)`. --- ### `dock(app, strategyHash, tokens)` Revokes virtual balances and stops the strategy from filling, without moving any tokens. - For each token in `tokens`, zeroes `balances[maker][app][strategyHash][token]`. - The tokens are already in the maker's wallet (they were never transferred to the registry), so `dock()` only clears the virtual accounting entries. - Emits `Docked(maker, app, strategyHash)`. A strategy is immutable, so `dock()` does not edit it. To change any parameter, dock the current strategy and ship a new one. Re-encoding the same struct reproduces the same `strategyHash`, while changing any field produces a new hash (see Re-shipping patterns below). --- ## Events The four `Aqua.sol` entry points emit `Shipped`, `Pulled`, `Pushed`, and `Docked`. On each fill, the `AquaSwapVMRouter` additionally emits `Swapped(orderHash, maker, taker, tokenIn, tokenOut, amountIn, amountOut)`, where `orderHash` equals the strategy's `strategyHash`. These events are documented alongside the registry (Aqua) and router references. The `@1inch/aqua-sdk` package exports decoders for the four registry events: ```typescript import { ShippedEvent, DockedEvent, PulledEvent, PushedEvent } from "@1inch/aqua-sdk"; const shipped = ShippedEvent.fromLog(log); // { maker, app, strategyHash, strategy } const pulled = PulledEvent.fromLog(log); // { maker, app, strategyHash, token, amount } ``` --- ## Re-shipping patterns Because strategies are immutable, updating a parameter requires a dock-then-ship cycle: ```typescript // 1. Dock the old strategy await wallet.sendTransaction(aqua.dock({ app, strategyHash: oldHash, tokens: [USDC, WETH] })); // 2. Ship with updated params (e.g., a new fee) // feePercent is a decimal in [0,1]; 0.0030 = 0.30% const newStrategyData = { ...strategyData, feePercent: 0.0030 }; const newStrategy = encodeAbiParameters([...], [newStrategyData]); await wallet.sendTransaction(aqua.ship({ app, strategy: new HexString(newStrategy), amountsAndTokens: [...] })); ``` The new `strategyHash` will differ from the old one because `feePercent` changed. Any integration or strategy reference that points at the old hash is now stale, so integrators must update to the new hash. --- ## Reentrancy The `nonReentrantStrategy()` modifier on `AquaApp` locks per `(maker, strategyHash)` using transient storage. Two different strategies owned by the same maker can execute concurrently; the same `(maker, strategyHash)` pair cannot re-enter itself mid-swap. --- ## Execution lifecycle flow ``` Execution order (actor -- message --> target): 1. Maker -- approve() token balance --> Aqua 2. Maker -- ship() allocation to Strategy --> Aqua [ Aqua records the maker allocation scoped to the Strategy ] 3. Taker -- trigger swap against Strategy --> Aqua 4. Aqua -- validate Strategy authorization --> Strategy 5. Strategy -- hand off Order for execution --> SwapVM 6. SwapVM -- begin Program execution --> Program loop [ for each instruction ] 7. Program -- dispatch opcode, read/write ctx --> Program opt [ Path C: Extruction opcode ] 8. Program -- call external contract --> External Contract 9. External Contract -- return result --> Program 10. Program -- write result into ctx --> Program 11. Program -- return computed amounts --> SwapVM 12. SwapVM -- return swap output --> Aqua 13. Aqua -- credit & debit balances atomically (internal) 14. Aqua -- deliver tokens --> Taker ``` --- ## Related - [Strategy](./strategy): how a strategy is structured across its layers. - [Virtual Balances](./virtual-balances): how `ship()` and `dock()` affect the storage slot. - [Smart Contract](../reference/smart-contract): the `ship()` / `dock()` / `pull()` / `push()` reference. :::note **Taker access gate (checked at swap time).** At launch every dApp strategy carries the Controls opcode `_onlyTxOriginTokenBalanceNonZero`, which reverts `TxOriginTokenBalanceIsZero` unless `balanceOf(tx.origin) > 0`. It is evaluated at _swap time, not ship time_, so a strategy can be live yet untradeable until a permitted taker holds the credential. Because it reads `tx.origin`, smart-contract wallets, multisigs and ERC-4337 bundlers cannot pass it as takers today. Permitted takers at launch are KYB-verified 1inch Resolvers. See [Access, resolvers & Pathfinder](./access-resolvers-and-pathfinder). ::: https://business.1inch.com/portal/assets/docs-v2/aqua/liquidity-layer/access-resolvers-and-pathfinder.md Access in 1inch Aqua is enforced at **four surfaces** with two independent levers: **makers are open by design** everywhere, and **takers are allow-listed by design at launch**. This page consolidates the model; see Controls and [Conditional Access](../swapvm/patterns/conditional-access) for the opcode-level detail. ## The four-surface access model | Surface | Makers (liquidity providers) | Takers (swappers) | | --------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Smart contracts** | Permissionless: `ship()`/`dock()`, no pause | Default-deny via `_onlyTxOriginTokenBalanceNonZero` on router v1.0.1; per-order `_whitelistSingleTaker`/`_whitelistMultipleTakers` exist in source but are _not registered_ on the deployed Aqua router | | **Pathfinder (routing)** | Off-chain maker blacklist (routing exclusion, never custodial) | KYC-routing gate: routes only KYC-opcode Aqua strategies | | **dApp** | Open behind the per-chain `enable-aqua` flag | Assembler attaches `withTxOriginAccessToken(aquaKycToken)` to every strategy; sanctioned-address screening | | **Compliance / onboarding** | Never KYC'd | KYB-gated `KycNFT` minting; sanctioned-wallet deny-list | ## The contract gate The deployed launch gate is the Controls opcode `_onlyTxOriginTokenBalanceNonZero(token)`, which reverts `TxOriginTokenBalanceIsZero` unless `balanceOf(tx.origin) > 0`. Because it reads `tx.origin`, only a swap sent directly from a credential-holding EOA passes -- **smart-contract wallets, multisigs and ERC-4337 bundlers cannot satisfy it**. It is evaluated at _swap time_, not ship time: a strategy can be live yet untradeable until a permitted taker holds the credential. ## Resolvers are the permitted takers At launch the permitted takers are **1inch Resolvers**: KYB-verified firms who accept the taker role. A `KycNFT` (symbol `RES`, also called the **Aqua resolver NFT**, reusing the Fusion `KycNFT.sol` design) is minted to each resolver's operator EOA. The credential is **one-per-address, soulbound, per chain**, and must sit on the `tx.origin` EOA that originates the swap. A chain's `enable-aqua` flag flips only once at least two verified resolvers hold credentials there. Revocation is by burn. :::note **Launch status.** Aqua is live and routable: permitted takers hold `KycNFT` credentials and fills execute on-chain. A `KycNFT` must currently be requested from 1inch by emailing **csm@1inch.com** -- see [How to request the Aqua resolver NFT](#how-to-request-the-aqua-resolver-nft) for what to include. ::: ## Pathfinder routing Pathfinder is the 1inch routing engine that discovers Aqua liquidity off-chain and executes it on-chain. Two access controls live here: an off-chain **maker blacklist** (routing exclusion, non-custodial, fail-closed) and a **KYC-routing gate** that routes only KYC-opcode Aqua strategies and blocks Classic-Swap routing through Aqua. This routing is part of the 1inch routing engine, separate from the Aqua contracts. --- ## Can I take? Becoming a resolver At launch, external solvers and takers **cannot fill Aqua strategies directly**: every dApp strategy is gated to holders of the `KycNFT` credential, checked on `tx.origin`. Permitted takers are **KYB-verified 1inch Resolvers**, and the credential must sit on the operator EOA that originates the swap (a smart-contract/settlement wallet cannot pass a `tx.origin` check). - **On-chain reality:** the swap reverts `TxOriginTokenBalanceIsZero` unless `balanceOf(tx.origin) > 0` of the per-chain `KycNFT`. - **Who qualifies:** the taker role is offered to KYB-verified 1inch Resolvers; the credential is one-per-EOA, soulbound, per chain, and revocable by burn. - **Onboarding:** Aqua taker access is not yet part of the self-service [Resolver onboarding](/portal/documentation/resolvers/introduction) flow (which covers Limit Order, Fusion, and Fusion+ credentials). Request it by email as described below. ### How to request the Aqua resolver NFT Aqua taker credentials are granted case by case. **Step 1 -- become a 1inch resolver.** If your firm is not yet onboarded, [register a 1inch Business Portal account](https://business.1inch.com/portal/registration), select **Resolver** among your business segments, and complete the self-service [Resolver onboarding](/portal/documentation/resolvers/introduction) flow -- it covers your company details, KYB verification, and the resolver compliance survey. **Step 2 -- request Aqua access.** Email **csm@1inch.com** with the subject **"Aqua resolver NFT request"** and include: - **Years in operation** and company website - **Contact person** -- name, role, and business email - **Track record** -- the trading or solver activity you run today: venues, roles (e.g. resolver/solver/filler on 1inch Fusion, CoW Protocol, UniswapX, Across), and indicative monthly volumes - **Chains and inventory** -- which of the 13 Aqua chains you plan to fill on and the indicative inventory you will quote with - **Operator EOA address(es)** the credential should be minted to -- one NFT per address, per chain. The address must be an EOA that is the transaction origin (`tx.origin`) of your fills; a smart-contract wallet, multisig, or 4337 bundler cannot pass the gate. - **Key management** -- how the operator EOA keys are secured (e.g. HSM, MPC, custody provider) After manual review, wallet addresses are screened and -- once approved -- the Aqua resolver NFT is minted to your operator EOA on the agreed chains. It is soulbound, one per address, per chain, and revocable by burn. **Already a 1inch resolver?** Skip step 1 and go straight to the email -- Aqua is enabled per organization only after manual review. The self-service [Resolver profile](/portal/profile/resolver) covers additional Limit Order, Fusion, and Fusion+ contracts, not Aqua; once Aqua is enabled for your organization, the Aqua section appears there as well. ## Concurrency & inventory-safety Aqua does not custody funds. A shipped strategy is **virtual accounting**, not an escrow, so both makers and takers must reason about the state of the world _between_ the moment they read a strategy and the moment a swap actually settles. This section covers the two races that follow from that: makers over-committing shared inventory, and takers acting on a stale quote. ### Shared inventory & double-commitment (maker side) When a maker calls `ship()`, Aqua records per-token amounts in the mapping `_balances[maker][app][strategyHash][token]` and moves **no tokens** -- the source comment calls these entries _"makers' allowances"_. The inventory stays in the maker's own wallet. It only leaves the wallet at swap time, when the router calls `pull()`, which executes `IERC20(token).safeTransferFrom(maker, to, amount)` and decrements the virtual balance. :::info **Consequence.** The virtual balance is a bookkeeping ceiling, not a set-aside reserve. The same tokens sitting in the maker wallet simultaneously back _every_ `strategyHash` they were shipped under, as well as any Fusion or classic limit orders the maker has open against that wallet. Nothing on-chain prevents the sum of these commitments from exceeding the wallet's actual balance or allowance. ::: The result is a **first-fill-wins race**. Each virtual balance decrements independently, but they all draw from one pool of real tokens. Whichever swap reaches `safeTransferFrom` first succeeds; a competing fill that arrives after the wallet has been drained reverts inside the ERC-20 transfer (insufficient balance or allowance), even though its Aqua virtual balance still looked sufficient a block earlier. | State | What it reflects | Where it lives | | --------------- | ------------------------------------ | ----------------------------------------------------------- | | Virtual balance | What a strategy is _allowed_ to swap | `_balances[...][strategyHash][token]` in Aqua | | Real inventory | What can _actually_ settle right now | `balanceOf(maker)` + allowance to Aqua, in the maker wallet | :::note **Operators must reserve or segregate inventory themselves.** If you run multiple Aqua strategies, or mix Aqua with Fusion/limit orders, out of one wallet, treat the wallet balance as the true budget and keep the sum of live commitments below it. Options: hold distinct inventory per strategy in separate wallets, cap the shipped `amounts` so overlapping strategies cannot collectively overshoot, or actively re-ship / `dock()` as real inventory moves. Aqua will not do this for you. ::: ### Quote-to-fill safety (taker side) A taker's `quote()` and the eventual `swap()` are separate transactions, and the strategy's output can change between them. Both entry points seed the VM from the same call -- `AQUA.safeBalances(order.maker, address(this), orderHash, tokenIn, tokenOut)` -- so anything that alters that state between your quote and your fill alters the price you get: - **The maker can walk away.** A maker may call `dock()` on the strategy at any time; it stamps the tokens `_DOCKED`, after which `safeBalances` reverts `SafeBalancesForTokenNotInActiveStrategy`. A strategy you quoted can be gone by the time you submit. - **Other takers move the balances.** Concurrent fills push and pull against the same `strategyHash`, so `balanceIn` / `balanceOut` are different when you settle than when you quoted. - **The curve is state- and time-dependent.** Price-shaping opcodes make the divergence intrinsic, not incidental: `Decay` moves the rate as a function of time, and `Extruction` shapes it from the current balances. Even with no competing fills, quoting early and settling late gives a different `amountOut`. The protocol gives takers two enforcement knobs in `TakerTraits`, both checked in `TakerTraitsLib.validate()` after the VM runs: | Knob | Field | On-chain check | | -------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Slippage bound | `threshold` (32 bytes) | exact-in: reverts `TakerTraitsInsufficientMinOutputAmount` unless `amountOut >= threshold`; exact-out: reverts `TakerTraitsExceedingMaxInputAmount` unless `amountIn <= threshold` | | Time bound | `deadline` (uint40) | reverts `TakerTraitsDeadlineExpired` when `deadline != 0 && block.timestamp > deadline` | :::note **Do not commit on a stale quote.** Set a conservative `threshold` (a genuine min-out / max-in, not the raw quoted number) so a moved curve reverts instead of filling at a worse rate; set a **tight** `deadline` so a delayed transaction expires rather than settling against a decayed price; and **re-run** `quote()` **(static) immediately before submitting** to confirm the strategy is still active and the numbers still hold. An empty `threshold` or a zero `deadline` disables that check entirely. ::: ## Resolver settlement worked example This section walks a single fill end to end from the **resolver (taker) perspective** against the deployed `AquaSwapVMRouter` v1.0.1. It is written for a solver calling the contracts directly; the same on-chain path is what the 1inch resolver network executes under the hood. Everything below is the deployed v1.0.1 behaviour of `SwapVM.swap(...)` and `TakerTraitsLib` -- no off-chain service is involved in settlement. :::info **Scope: Aqua does no discovery or matching.** The protocol prices and settles one maker strategy against one taker per `swap()` call. It performs **no off-chain price discovery, no CoW/order matching, and no multi-order aggregation**. Finding which strategy to fill, at what size, and against what counter-flow is the job of the **resolver network** (when swapping through the 1inch dApp) or **your own solver** (when calling the contracts directly). Hosted resolver/discovery APIs are a **1inch Business** product and are out of scope here -- see [1inch Business](https://business.1inch.com/portal/documentation). ::: ### Preconditions the resolver must satisfy Take a shipped Aqua strategy that quotes **WETH out for USDC in** (the maker holds WETH inventory in Aqua; `tokenIn = USDC` flows to the maker, `tokenOut = WETH` flows to the taker). Before the fill can succeed: - The resolver's operator **EOA holds the per-chain** `KycNFT` (symbol `RES`). - The `swap()` transaction is sent **directly from that EOA**. The strategy program embeds the Controls check `_onlyTxOriginTokenBalanceNonZero(KycNFT)`, which reverts `TxOriginTokenBalanceIsZero` unless `balanceOf(tx.origin) > 0`. Because it reads `tx.origin`, a smart-contract settlement wallet, multisig or 4337 bundler in the call path does not satisfy the gate -- the credentialed EOA must be the transaction origin. - The resolver has (or sources) the `tokenIn` it will push, and approves the router to move it. ### The call: the 5-arg swap On v1.0.1 both `quote` and `swap` take five arguments. Pricing is done first with a static call to `quote` (same signature, no state change), then the fill is sent to `swap`: ```solidity function swap( ISwapVM.Order calldata order, // maker strategy: { maker, traits, data }; data = hooks || program address tokenIn, // token the taker pays (USDC) address tokenOut, // token the taker receives (WETH) uint256 amount, // exact-in: amount of tokenIn; exact-out: amount of tokenOut bytes calldata takerTraitsAndData ) external returns (uint256 amountIn, uint256 amountOut, bytes32 orderHash); ``` The resolver builds `takerTraitsAndData` with `TakerTraitsLib.build(Args)`. For an Aqua fill the resolver sets, at minimum: ```solidity TakerTraitsLib.Args({ taker: resolverEOA, isExactIn: true, // sell an exact amount of USDC shouldUnwrapWeth: false, // MUST be false: unwrap is incompatible with Aqua isStrictThresholdAmount: false, // treat threshold as a minimum, not an exact match isFirstTransferFromTaker: false, // pull maker output first, then push taker input useTransferFromAndAquaPush: true, // router does transferFrom(taker) then AQUA.push(maker) threshold: abi.encode(minWethOut), // 32-byte min amountOut (slippage floor) to: address(0), // 0 => proceeds go to the taker EOA deadline: ..., // 0 = no deadline, else unix ts // hook / callback fields empty for a plain fill instructionsArgs: "", signature: "" // empty: Aqua strategies use useAquaInsteadOfSignature }) ``` The first **22 bytes** of the packed blob are the traits header (`TakerTraitsLib.parse` splits header from tail); the `USE_TRANSFER_FROM_AND_AQUA_PUSH_FLAG` is bit `0x0040`. ### What the router does, in order 1. **Hash & lock.** Computes `orderHash = hash(order)` and takes a per-order reentrancy lock. For Aqua strategies `hash` is `keccak256(abi.encode(order))` (no EIP-712 signature branch). 2. **Parse taker traits.**`TakerTraitsLib.parse` yields `takerTraits` and `takerData`; `isExactIn` is read from the header. 3. **Load maker balances.** Because the strategy sets `useAquaInsteadOfSignature`, the router reads the maker's virtual balances with `AQUA.safeBalances(maker, router, orderHash, tokenIn, tokenOut)` instead of verifying a signature. 4. **Run the program.**`runLoop()` executes the strategy's opcodes (the AMM curve, any Fee opcode, the `tx.origin` access check, etc.) and computes `(amountIn, amountOut)`. 5. **Validate.**`order.traits.validate(...)` then `takerTraits.validate(...)`: `amountOut > 0`; for exact-in the passed `amount` must equal `amountIn` and `amountOut >= minWethOut` (or exact if `isStrictThresholdAmount`); the deadline, if set, must not have passed. 6. **Settle, ordered by** `isFirstTransferFromTaker`**.** With the flag `false` the router runs `_transferOut` then `_transferIn`: - **Maker output (pull):**`_transferOut` calls `AQUA.pull(maker, orderHash, tokenOut, amountOut, to)`, moving `amountOut` WETH out of the maker's Aqua balance to the recipient (`to`, defaulting to the taker EOA). - **Taker input (push):** because `useTransferFromAndAquaPush` is set, `_transferIn` does `IERC20(tokenIn).safeTransferFrom(taker, router, amountIn)`, `forceApprove(AQUA, amountIn)`, then `AQUA.push(maker, router, orderHash, tokenIn, amountIn)`, crediting the maker's USDC balance. (With the flag off, the router instead only checks that the maker's raw Aqua balance already grew by the required amount -- i.e. the taker pushed inventory itself.) 7. **Unlock & emit.** Releases the lock and emits `Swapped(orderHash, maker, taker, tokenIn, tokenOut, amountIn, amountOut)`. :::note **Aqua strategy constraints enforced at settlement.** For `useAquaInsteadOfSignature` orders the router requires `shouldUnwrapWeth == false` (reverts `MakerTraitsUnwrapIsIncompatibleWithAqua`) and the maker to be its own receiver (reverts `MakerTraitsCustomReceiverIsIncompatibleWithAqua`). Set `shouldUnwrapWeth: false` and do not attempt a custom maker receiver. ::: ### Token flow (exact-in, illustrative amounts) | Leg | Mechanism | From -> To | Amount | | ------------ | ---------------------------- | ------------------------------------------ | ------------------------ | | Maker output | `AQUA.pull` | Maker Aqua balance -> resolver EOA | `amountOut` = 0.994 WETH | | Taker input | `transferFrom` + `AQUA.push` | Resolver EOA -> router -> maker Aqua balance | `amountIn` = 3,000 USDC | Amounts are illustrative; the real numbers come from the strategy's on-chain program (AMM curve plus any Fee opcode) as returned by `quote`. ### Economics: who earns what - **The maker earns the LP swap fee.** The strategy's realized price already embeds the AMM curve and, if present, a `Fee` opcode (basis points, denominator `1e9`). The maker swaps inventory (WETH out, USDC in) at that price and keeps the fee/curve spread as compensation for providing liquidity. The maker never pays gas and is never the transaction origin. - **The resolver captures spread minus gas.** The resolver pays `amountIn` of `tokenIn` and receives `amountOut` of `tokenOut` at the strategy price. Its profit is the difference between the value it realizes for the pulled `tokenOut` (an end user, its own book, or another venue) and the cost of the `tokenIn` it sourced and pushed -- **minus the gas it pays** as `msg.sender`/`tx.origin`. The resolver bears execution risk and gas; the maker does not. A fill is only worth sending when that spread clears gas and sourcing cost, which is why the `threshold` (min output) and `deadline` exist as the resolver's on-chain guardrails. Because the protocol settles exactly one maker-taker pair per call, any batching, netting, or cross-venue routing is the solver's own logic wrapped _around_ these calls -- Aqua neither provides nor requires it. ## Inventory-coverage health check (maker/keeper side) The double-commitment race above has no on-chain guard, so a maker or keeper running shared inventory needs its own **pollable health signal**: for every token, the sum of what has been committed across all live strategies must stay at or below what the wallet can actually deliver. This section gives the formula, the exact primitives to read, and a Foundry test that reproduces the revert. ### The coverage formula (per token) For a given `maker` wallet and the `app` its strategies were shipped under, the committed amount of a token is the sum of that token's _virtual_ balance across every active `strategyHash`. The real backing is the wallet's balance capped by its allowance to the Aqua contract, because `pull()` settles with `IERC20(token).safeTransferFrom(maker, to, amount)` where the spender is Aqua itself -- a fill needs both real balance _and_ allowance. ``` # Inputs the keeper already holds off-chain. The _balances mapping is NOT # enumerable, so you must track the strategyHashes you have shipped yourself. # maker - wallet that called ship() # app - AquaApp address the strategies were shipped under # strategies - strategyHashes you have shipped and not docked # tokens - tokens those strategies touch for token in tokens: committed = 0 for h in strategies: (bal, tokensCount) = AQUA.rawBalances(maker, app, h, token) if tokensCount == 0 or tokensCount == 0xff: # never-shipped or DOCKED continue # skip: contributes nothing committed += bal real = min( IERC20(token).balanceOf(maker), IERC20(token).allowance(maker, address(AQUA)) ) coverage = real / committed # >= 1.0 is healthy if committed > real: ALERT(token, committed, real) # double-commitment danger zone ``` :::info **Read** `rawBalances`**, not** `safeBalances`**, for the sweep.** `rawBalances(maker, app, strategyHash, token)` returns `(uint248 balance, uint8 tokensCount)` for any slot and never reverts, so you can loop it. `safeBalances` _reverts_ `SafeBalancesForTokenNotInActiveStrategy` the moment it hits a never-shipped or `_DOCKED` (`0xff`) token, which would abort your loop. Use the `tokensCount` field from `rawBalances` to skip inactive slots (docked entries store `0` anyway). If the same wallet also backs open Fusion or classic limit orders, fold their live commitments into `committed` as well -- they draw from the identical real balance. ::: :::note `safeBalances` **can report room the wallet no longer backs.** `safeBalances` reads only the one strategy's virtual entry; it knows nothing about sibling strategies sharing the wallet. So a strategy can return a full, non-zero balance from `safeBalances` at quote time and still have its `pull()` revert at swap time, because another fill already drained the shared wallet below the amount. That revert surfaces as a bare ERC-20 insufficient-balance (or insufficient-allowance) revert from inside `SafeERC20` -- **not** an Aqua named error and **not** a virtual-balance underflow (each strategy's own balance decrements cleanly to zero). Poll the formula above rather than trusting a per-strategy read. ::: ### Reproducing the double-commitment revert (Foundry) This minimal test ships two strategies over one wallet, each committing 100 tokens while the wallet holds only 120 of real inventory. The first fill drains the wallet; the second reverts inside `safeTransferFrom` even though its virtual balance still reads a full 100. Drop it in the `aqua` repo's `test/` directory (it reuses that repo's remappings and mock `ERC20`) and run `forge test`. ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.30; import { Test } from "forge-std/Test.sol"; import { IERC20 } from "@1inch/solidity-utils/contracts/interfaces/IERC20.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { Aqua } from "src/Aqua.sol"; contract Mock is ERC20 { constructor() ERC20("Mock", "MOCK") {} function mint(address to, uint256 a) external { _mint(to, a); } } // This test contract plays the `app` that calls pull() at swap time, // exactly as AquaRouter would. contract DoubleCommitmentTest is Test { Aqua aqua; Mock token; address maker = makeAddr("maker"); address taker = makeAddr("taker"); function setUp() public { aqua = new Aqua(); token = new Mock(); token.mint(maker, 120e18); // only 120 REAL inventory vm.prank(maker); token.approve(address(aqua), type(uint256).max); // allowance is not the bottleneck } function _ship(bytes memory strategy) internal returns (bytes32 h) { address[] memory tks = new address[](1); tks[0] = address(token); uint256[] memory amt = new uint256[](1); amt[0] = 100e18; // commit 100 to EACH strategy vm.prank(maker); h = aqua.ship(address(this), strategy, tks, amt); } function test_secondFillReverts_onSharedInventory() public { bytes32 hA = _ship("STRATEGY_A"); bytes32 hB = _ship("STRATEGY_B"); // Both virtual balances advertise a full 100 -> 200 committed vs 120 real. (uint256 a,) = aqua.safeBalances(maker, address(this), hA, address(token), address(token)); (uint256 b,) = aqua.safeBalances(maker, address(this), hB, address(token), address(token)); assertEq(a, 100e18); assertEq(b, 100e18); assertGt(a + b, token.balanceOf(maker)); // the danger zone // First fill wins: pull() drains the wallet from 120 to 20. aqua.pull(maker, hA, address(token), 100e18, taker); assertEq(token.balanceOf(maker), 20e18); // Strategy B's virtual balance is UNTOUCHED and still reports room ... (uint256 stillB,) = aqua.safeBalances(maker, address(this), hB, address(token), address(token)); assertEq(stillB, 100e18); assertLt(token.balanceOf(maker), 100e18); // ... that the wallet no longer backs // ... so the second pull reverts inside SafeERC20's safeTransferFrom // (ERC20 insufficient balance) -- not an Aqua error, not an underflow. vm.expectRevert(); aqua.pull(maker, hB, address(token), 100e18, taker); } } ``` The test proves the asymmetry the formula is built to catch: `safeBalances(hB)` returns `100e18` right up until the failing call, while `balanceOf(maker)` has already fallen to `20e18`. Only the wallet-level sum -- not any single strategy read -- exposes the shortfall before a taker hits it. Keep `coverage >= 1.0` by segregating inventory per wallet, capping shipped `amounts` so overlapping strategies cannot collectively overshoot, or re-shipping / `dock()`-ing as real inventory moves. https://business.1inch.com/portal/assets/docs-v2/aqua/liquidity-layer/supported-chains.md 1inch Aqua is deployed at one deterministic address on **13 EVM chains**. The registry (`Aqua`), the router (`AquaSwapVMRouter` v1.0.2) and the `KycNFT` credential share the same address on every one of these chains. ## The 13 Aqua chains | Chain | Chain ID | | --------------- | -------- | | Ethereum | `1` | | Optimism | `10` | | BNB Chain | `56` | | Gnosis | `100` | | Unichain | `130` | | Polygon | `137` | | Sonic | `146` | | zkSync Era | `324` | | Robinhood Chain | `4663` | | Base | `8453` | | Arbitrum | `42161` | | Avalanche | `43114` | | Linea | `59144` | `supportedChains = [1, 10, 56, 100, 130, 137, 146, 324, 4663, 8453, 42161, 43114, 59144]`. Note Unichain is `130` (the SDK README's `1301` is a known error). ## Robinhood Chain (4663) joined the chain set on 2026-07-16 Robinhood Chain was previously **excluded** from the Aqua chain set: its registry was a different, CREATE3-deployed address (`0x7c2D...ec86`), it had no `KycNFT`, and it was flagged `unsupportedForAqua`. That exclusion ended with the 2026-07-16 universal redeployment: - Aqua was redeployed on **all** chains -- Robinhood included -- to one universal registry address (`0xe8026b...139b`), together with the dependent `AquaSwapVMRouter` v1.0.2 (`0x016b41...B070`). Both were superseded in the 2026-07 vanity redeployment -- the current addresses are `0x1111113ccf...a90a` (registry) and `0x111111338c...c0de` (router); see [Contract addresses](/portal/documentation/aqua/reference/contract-addresses). - The `KycNFT` credential (`0x26FF...a468`) is configured for chain 4663; resolver flows on Robinhood have been validated. - The Aqua API accepts and indexes chain 4663 in production. Robinhood's earlier chain-specific contracts (`0x7c2D...ec86` registry, `0xA536...331A` router) are superseded -- use them only to decode historical activity. See [Verified contract addresses](/portal/documentation/aqua/reference/verified-contract-addresses) for the full historical list. ## Cross-chain: no native settlement The unified address is a **deployment convenience, not a shared ledger**. Aqua and the `AquaSwapVMRouter` have **no native cross-chain settlement**. Each of the 13 chains is a fully independent deployment with its own registry state, its own router, and its own `KycNFT` credential set. A strategy shipped on one chain can only be filled on that same chain. The shared address from the Supported chains table means the _same bytecode_ lives at the _same address_ on every chain. It does **not** mean the deployments share liquidity, balances, or a KYC allowlist. There is no bridge, no message-passing, and no cross-chain accounting anywhere in the v1.0.2 router or the Aqua registry. ### Why each chain is isolated Two independent facts in the deployed code keep every chain self-contained: - **Registry balances are per-deployment.** Maker inventory lives in the Aqua registry on a single chain, keyed by `_balances[maker][app][strategyHash][token]`. Shipping a strategy funds that mapping on that chain only; `pull` and `dock` read and write the same local mapping. Nothing reads or credits balances on another chain. - **The order/strategy hash is chain-bound.**`SwapVM` is an `EIP712` contract, so a maker-signed order is hashed through `_hashTypedDataV4`, whose domain separator folds in `chainId` and the router address (`verifyingContract`). The same order re-submitted on a different chain produces a different digest and is not a valid signature there. Aqua-native strategies (`useAquaInsteadOfSignature`) are identified by `strategyHash = keccak256(strategy)` in the local registry, which only exists where the strategy was shipped. A fill always executes against the router deployed on the chain the transaction is sent to. Each `AquaSwapVMRouter` is constructed with one Aqua registry address and one WETH address for that chain: ```solidity // swap-vm/src/routers/AquaSwapVMRouter.sol (v1.0.2) constructor( address aqua, // the Aqua registry on THIS chain address weth, address owner, string memory name, string memory version ) SwapVM(aqua, weth, owner, name, version) AquaOpcodes(aqua) { } // quote / swap are 5-arg and resolve inventory from the local registry only: // swap(Order order, address tokenIn, address tokenOut, uint256 amount, bytes takerTraitsAndData) ``` ### What this means for makers A maker who wants to be fillable on multiple chains operates each chain as a separate venue: - Ship the strategy on every target chain. `ship(app, strategy, tokens, amounts)` is a per-chain call against that chain's Aqua registry; the returned `strategyHash` is only meaningful there. - Fund inventory per chain. The `amounts` pushed on Ethereum are not available to fills on Base or Arbitrum. Inventory does not rebalance itself across chains. - Manage lifecycle per chain. `dock` and `pull` act on one chain's balances at a time; parking or withdrawing on one chain leaves the others untouched. ### What this means for takers The `KycNFT` credential is a per-chain deployment as well. Holding the credential on one chain does not make a taker recognized on another -- the credential must be held on each chain where that taker intends to fill. Because balances and the KYC set are local, a route that succeeds on one chain gives no guarantee about any other chain. **Moving value across chains is out of scope for Aqua.** Neither the router nor the registry bridges tokens or relays state between chains. To move funds from one chain to another, use a separate bridge as an independent step, then ship or fill against the Aqua deployment on the destination chain. Hosted routing and cross-chain aggregation are handled by **1inch Business** products, not by the on-chain Aqua contracts documented here. https://business.1inch.com/portal/assets/docs-v2/aqua/liquidity-layer/risks-and-disclosures.md 1inch Aqua is a self-custodial, permissionless shared liquidity layer. This page consolidates the risk framing that applies across the docs; it is engineering context, not legal or financial advice. ## Self-custody Tokens stay in the maker's wallet under a **revocable, per-chain, per-token** ERC-20 allowance and move only when a taker fills atomically. The protocol holds **zero** tokens; virtual balances are an internal counter in `Aqua.sol`. Revoke by setting the allowance to 0. There are no deposits and nothing is locked. ## Smart-contract & approval risk Audited, but new -- audits reduce, not eliminate, smart-contract risk. Granting an allowance is a risk decision: a bug or a compromised approval target could move approved tokens. Prefer bounded allowances where practical and monitor approvals. ## No guaranteed yield Makers earn **swap fees**, which are not guaranteed and do not offset impermanent loss in every market. Aqua does not promise APY, APR, or returns. Any APY/APR shown by a tool is a computed position metric, not a promise. ## Access is taker-side only Makers are permissionless. Only **takers** are gated at launch (default-deny `KycNFT` credential held by KYB-verified 1inch Resolvers). Makers and end users are never KYC'd by the protocol. dApp surfaces also apply sanctioned-address screening. ## Launch status & estimates Aqua is live and routable. Takers need a `KycNFT` credential to fill, and the credential must currently be requested from 1inch via the 1inch Business Portal. Every derived metric (analytics, subgraph) is an estimate from public data, carrying a method block, and is not an official 1inch figure. ## Immutability A shipped strategy is immutable: any change is `dock()` then a fresh `ship()`, producing a new `strategyHash`. There is no on-chain pause and no liquidation; an underfunded strategy simply stops filling (a Coverage effect). --- ## Audits 1inch Aqua is **audited, but new** -- audits reduce, not eliminate, smart-contract risk. 1inch Aqua and the SwapVM engine have been audited by OpenZeppelin, Bailsec, Hexens, Nethermind, Theori, Decurity, Hashlock and MixBytes. Even so, treat the code as new in production. ## Admin-key concentration :::note The Aqua registry and the router Rescuable are owned by an **externally-owned account** (`0x4134e66d52EfC4C77DD8Ccc952D87b9E92E0C352`, an interim arrangement pending the final ownership transfer), and the `KycNFT` minter by another EOA (`0x0bd61D605c64A857c3d94779aEF7cA295702b3A2`) -- none of them a multisig. A key compromise is a live risk to consider when integrating. A multisig migration is proposed but not executed. ::: ## Legal & disclaimers This is engineering documentation for the deployed 1inch Aqua protocol. It is **not** investment, legal, tax or financial advice. :::note **No offer or recommendation.** Nothing on this page or anywhere in these docs is an offer, solicitation, inducement or recommendation to provide liquidity, acquire or dispose of any asset, or enter into any transaction. Any decision to interact with the protocol is your own. ::: **Self-custodial and permissionless for makers.** Tokens stay in the maker's own wallet under a revocable ERC-20 allowance; the protocol holds zero tokens and no intermediary takes custody. Maker funds move only inside an atomic taker fill, and only up to the allowance the maker granted. Anyone can act as a maker without permission. **Jurisdiction and compliance.** Availability and use of the protocol, the dApp and any related interface may be restricted or unlawful in some jurisdictions. You are solely responsible for determining whether your use is permitted where you are, and for meeting your own tax, regulatory, sanctions and compliance obligations. Nothing here waives any such restriction. :::info **No warranty -- provided as is.** The software is licensed under `LicenseRef-Degensoft-Aqua-Source-1.1` and is provided **"as is"** and **"as available"**, without warranties of any kind, express or implied. It is **audited, but new**: audits reduce, they do not remove, smart-contract risk, and the code should be treated as unproven in production. ::: ### Audit coverage 1inch Aqua and the SwapVM engine have been audited by **OpenZeppelin, Bailsec, Hexens, Nethermind, Theori, Decurity, Hashlock and MixBytes**. Audits reduce risk, they do not remove it: treat the code as audited, but new. (This mirrors the Audits note above -- a single audit-status position, not two.) https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/swapvm-engine.md SwapVM, the Aqua swap engine, is the bytecode execution engine for Aqua strategies. It is a purpose-built virtual machine deployed as the `AquaSwapVMRouter` contract (`0x111111338c5091e8440b67b168bae16a668ac0de`), the AquaApp that runs SwapVM programs and interprets them at swap time. Aqua and SwapVM own separate concerns. The Aqua registry (`Aqua.sol`) owns custody and balance accounting; SwapVM owns strategy pricing and swap execution. --- ## Separation of concerns | Concern | Handled by | | ------------------------------------ | ------------------------------------------------------------ | | Asset ownership and custody | Maker's wallet | | Balance accounting and authorization | `Aqua.sol` (the Aqua registry) | | Strategy pricing and swap execution | SwapVM (deployed as `AquaSwapVMRouter`), or a custom AquaApp | `Aqua.sol` is strategy-agnostic. It does not know or care how a strategy prices a swap; it only enforces that `pull()` and `push()` are balanced and authorized. Any contract that calls `pull()` and `push()` correctly is a valid AquaApp. :::info Self-custodial by design: tokens stay in the maker's wallet under a revocable, per-chain, per-token allowance and move only when a taker fills a swap atomically. The protocol holds 0 tokens. The balances tracked in `Aqua.sol` are an internal accounting counter, not deposits. Smart-contract and approval risk still apply. ::: --- ## From program to order to strategy When building with SwapVM, you do not ship a Strategy directly. You compose a **Program**, wrap it in an **Order**, and Aqua registers the ABI-encoded Order as a Strategy: ``` Program (opcodes + args) -> SwapVM Order -> Aqua Strategy (ABI-encoded) ``` Aqua treats the Strategy body as opaque bytes. It does not parse instructions or execute pricing logic; it only manages balance accounting. The `AquaSwapVMRouter` executes the Program instructions at swap time. --- ## Three paths to building on Aqua ### Path A: custom AquaApp (no SwapVM) Write a Solidity contract that implements swap logic directly. The contract calls `pull()` and `push()` on `Aqua.sol` to move tokens. SwapVM is not involved; this shows that Aqua is execution-engine-agnostic. Full flexibility. It requires auditing and deploying a new contract per strategy type. See Build an AquaApp for the full pattern. ### Path B: SwapVM with existing opcodes Compose a **Program** from SwapVM's built-in instruction set (AMM formula, fee model, range logic, and so on) without writing any new smart contracts. Wrap the Program in an Order, then register the ABI-encoded Order as an Aqua Strategy via `ship()`. The `AquaSwapVMRouter` interprets the Program at swap time against swap-state registers. No new contract is required; a new strategy is a new Program. The deployed Aqua opcode set covers: | Category | Opcode | | --------------------------------- | ---------------- | | Constant-product AMM | `XYCSwap` | | Concentrated liquidity | `XYCConcentrate` | | Pegged and reward-bearing pricing | `PeggedSwap` | | Fee logic | `Fee` | | Price decay over time | `Decay` | | Control flow | `Controls` | | External calls | `Extruction` | :::info The deployed `AquaSwapVMRouter` inherits the `AquaOpcodes` set: `Controls`, `XYCSwap`, `XYCConcentrate`, `Decay`, `Fee`, `PeggedSwap`, `Extruction`. Instructions such as `Invalidators` and `SeriesEpochManager`, and the Dutch-auction price modifier, belong to the separate limit-order and Fusion router, not the Aqua router. v1.0 ships two-token strategies only. ::: See SwapVM Instructions for the full bytecode format and opcode table. ### Path C: SwapVM with an external contract extension Deploy a custom smart contract with proprietary logic and call it from inside a Program via SwapVM's `Extruction` opcode (`IExtruction`). The value returned by the external contract is written back into SwapVM's execution context (`ctx`); this extends VM state with arbitrary external logic while SwapVM remains the execution coordinator. This path enables novel pricing formulas or external oracle integrations without forking the VM. The Program still runs inside an Order registered as an Aqua Strategy, and the external contract call happens mid-execution. The `Extruction` target must be non-upgradeable, and `quote()` and `swap()` must return identical amounts for the same inputs. --- ## Architecture overview ``` DEVELOPER PATHS +----------------+ +--------------------+ +------------------------------+ | Path A: | | Path B: | | Path C: | | Custom AquaApp | | Built-in opcodes | | External Contract Extension | +----------------+ +--------------------+ +------------------------------+ | | | | registers | assembles Program | Extruction opcode | custom payload | into Order | modifies ctx | as Strategy | | | v v | ............ SWAPVM ............ | +---------+ builds into +-----------+ | | Order | --------------> | Program | | +---------+ +-----------+ | | | | Order is ABI-encoded and registered as Strategy v v .............. AQUA .............. +------------+ authorizes +--------------+ | Strategy | <------------- | Balance | | | --accounts--> | Accounting | +------------+ +--------------+ ``` --- ## Choosing a path **Does your pricing logic fit SwapVM's existing instruction set?** - **Yes.** Choose **Path B.** Compose a Program from the deployed opcode set. No new contracts required; a new strategy is a new Program. This is the fastest path to deployment. - **No, but you want SwapVM to coordinate execution.** Choose **Path C.** Deploy a contract with your custom logic and call it mid-Program via the `Extruction` opcode. SwapVM remains the executor; your contract handles the custom computation. - **No, and you need full control over swap logic in Solidity.** Choose **Path A.** Write a custom AquaApp contract from scratch. Maximum flexibility; it requires auditing and deploying a new contract per strategy type. --- ## Related - [Program Order Strategy](/portal/documentation/aqua/swapvm/program-order-and-strategy): the SwapVM container that wraps a Program and becomes an Aqua Strategy. - [Program Model](/portal/documentation/aqua/swapvm/program-model): bytecode format, registers, and instruction ordering rules. - [Build an AquaApp](/portal/documentation/aqua/getting-started/build-an-aquaapp): implement swap logic in Solidity. - [Patterns & decision tree](/portal/documentation/aqua/swapvm/patterns/patterns-and-decision-tree): recipes for common strategy types. - [Contract Addresses](/portal/documentation/aqua/reference/contract-addresses): the `AquaSwapVMRouter` address across all 13 supported chains. https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/program-order-and-strategy.md 1inch Aqua separates _what_ a maker's liquidity does from _how_ Aqua stores and accounts for it. The same definition moves through three layers before it becomes a live, fillable strategy, and each layer belongs to a different part of the stack. Program -> Order -> Strategy (in Aqua) | Layer | Belongs to | What it is | | ------------ | ---------- | ----------------------------------------------------------------------------------- | | **Program** | SwapVM | An ordered sequence of opcodes, instructions, and parameters | | **Order** | SwapVM | The container that wraps a Program with maker metadata and authorization headers | | **Strategy** | Aqua | The ABI-encoded form of an Order: opaque bytes that Aqua registers and accounts for | Aqua treats the Strategy body as a black box. It does not parse the Program or interpret any instructions; it only manages balance accounting. All execution logic stays inside SwapVM, the Aqua swap engine (the deployed `AquaSwapVMRouter`). --- ## What an Order contains | Field | Purpose | | -------------------- | ------------------------------------------------------------------------------------ | | **Program bytecode** | The ordered sequence of instructions SwapVM executes on every `quote()` and `swap()` | | **MakerTraits** | 256-bit packed header: authorization flags, hook indexes, epoch, nonce, time bounds | | **Maker address** | Identifies the liquidity owner; makes the Order unique per maker | | **Salt** | Optional; allows a maker to have multiple Orders with identical parameters | When `useAquaInsteadOfSignature` is set in `MakerTraits`, Aqua's registry acts as the authorization mechanism, and the Order is treated as valid by virtue of having been shipped. When it is unset, the Order is authorized by an EIP-712 signature and functions as a standalone SwapVM order, independent of Aqua. --- ## How an Order becomes a Strategy 1. Developer composes a **Program** (opcodes + instructions + parameters) 2. Program is wrapped into a **SwapVM Order** with maker metadata and authorization headers 3. The Order is ABI-encoded and passed to `Aqua.ship()`; at that point it becomes an Aqua **Strategy** 4. Aqua derives a `strategyHash = keccak256(abi.encode(strategy))` and stores virtual balances under `balances[maker][app][strategyHash][token]` The hash encodes every parameter, so any parameter change produces a different hash. This enforces immutability by identity: a Strategy cannot be edited in place, only closed with `dock()` and reopened with a fresh `ship()`. :::info Virtual balances are an internal accounting counter inside `Aqua.sol`, not custody. Tokens stay in the maker's wallet under a revocable, per-chain, per-token allowance and move only when a taker fills atomically; the protocol holds zero tokens. ::: --- ## Two authorization paths | Path | Authorization | Balance source | | ------------------------ | ------------------------------------------------- | ------------------------------------------------------------- | | **Aqua (shipped)** | `ship()` call; `useAquaInsteadOfSignature = true` | Aqua virtual balances | | **Standalone signature** | EIP-712 signature from maker | Embedded in Program args (static) or SwapVM storage (dynamic) | The Program bytecode is identical across both paths. Only the authorization mechanism and balance source differ. --- ## Related - The [SwapVM engine](./swapvm-engine) runs the Program inside an Order. - The [Program Model](./program-model) page covers bytecode format, registers, and instruction ordering rules. - The [Strategy](../liquidity-layer/strategy) page explains how Aqua registers and accounts for the ABI-encoded Order. https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/program-model.md A **Program** is an ordered sequence of instructions encoded as bytecode and executed on-chain when a taker calls `swap()` or `quote()`. The Program lives inside a SwapVM **Order**, the container that holds the Program together with maker metadata and authorization headers. Once ABI-encoded, that Order is what Aqua registers as a **Strategy**. ``` Program (opcodes + args) -> SwapVM Order -> Aqua Strategy (ABI-encoded) ``` Aqua treats the Strategy body as opaque bytes; it does not interpret instructions or execute pricing logic. AquaSwapVMRouter executes the Program instructions at swap time. --- ## SwapRegisters Every instruction reads from and writes to the same five registers: | Register | Type | Who sets it | Meaning | | ----------------- | --------- | -------------------------------- | -------------------------------------------- | | `balanceIn` | `uint256` | Balance instruction | Maker's available input token reserve | | `balanceOut` | `uint256` | Balance instruction | Maker's available output token reserve | | `amountIn` | `uint256` | Taker (exactIn) or swap formula | Input amount for this swap | | `amountOut` | `uint256` | Taker (exactOut) or swap formula | Output amount for this swap | | `amountNetPulled` | `uint256` | Fee instructions | Cumulative amount pulled from maker for fees | The taker sets exactly one of `amountIn` or `amountOut` (`isExactIn` flag). The program computes the other. Instructions then adjust both. --- ## Bytecode format Each instruction in the program is encoded as: ``` [opcode_index : 1 byte][args_length : 1 byte][args_data : N bytes] ``` Instructions are executed sequentially. Some instructions (fees, decay) call `ctx.runLoop()` internally: they invoke all subsequent instructions as a nested sub-program, then adjust the returned amounts. This is how wrapping works. **Instruction ordering is security-critical.** Reordering changes pricing, settlement amounts, and invalidation behavior. Fee placement in particular changes which leg the fee applies to and its economic meaning. --- ## Two balance archetypes ### Static balances Currently not supported Only dynamic balances are currently supported. ``` _staticBalancesXD -> [pricing] -> [invalidation] ``` - Balance values are embedded in the program args and never change - Single direction only (e.g. always sell tokenA for tokenB) - No on-chain state read or write for balances - Used for: limit orders, Dutch auctions, TWAP, RFQ, DCA, range orders ### Dynamic balances ``` _dynamicBalancesXD -> [fee] -> [swap formula] ``` - Balances are loaded from SwapVM storage keyed by `(orderHash, token)` - After each swap, consumed amounts are written back: `balanceIn += swapAmountIn`, `balanceOut -= swapAmountOut` - Bidirectional: taker can swap either direction - Used for: constant product AMMs, concentrated liquidity, pegged swaps --- ## Three settlement paths | Path | Setup | Authorization | Balance source | | -------------------------------------------------- | ----------------------------------- | ------------------ | ------------------------------------ | | **Signature + Static** _(not currently supported)_ | Sign order off-chain | EIP-712 signature | Embedded in program args | | **Signature + Dynamic** | Sign order off-chain | EIP-712 signature | SwapVM storage (per-maker isolation) | | **Aqua** | `aqua.ship(token, amount)` on-chain | Aqua balance check | Aqua shared liquidity layer | Set `useAquaInsteadOfSignature = true` in `MakerTraits` for the Aqua path. The program bytecode (instructions, args) is identical; only settlement and authorization differ. :::info On the Aqua path, liquidity is self-custodial. The maker's tokens stay in the maker's wallet under a revocable, per-chain, per-token allowance, and Aqua tracks only a virtual balance in `Aqua.sol`. Tokens move only when a taker fills atomically, and the protocol itself holds no tokens. Smart-contract and approval risk still apply. ::: --- ## Instruction ordering rules 1. **Balance setup first.** `_staticBalancesXD` or `_dynamicBalancesXD` must be the first instruction that sets reserves. Nothing can use `balanceIn`/`balanceOut` before they are set. 2. **Wrapping instructions before the formula.** Fee instructions (`_flatFeeAmountInXD`, etc.) and `_decayXD` call `ctx.runLoop()` to invoke subsequent instructions, so place them before the swap formula so the formula runs inside the nested loop. 3. **Post-swap adjusters after the formula.** `_baseFeeAdjuster1D` and `_oraclePriceAdjuster1D` require `amountIn` and `amountOut` to already be set, so place them after the swap formula. 4. **Invalidators after amounts are computed.** `_invalidateTokenIn1D` and `_invalidateTokenOut1D` need the relevant amount to be non-zero. 5. **Control flow anywhere, but test all paths.** Jump instructions can appear anywhere but introduce execution paths that must each satisfy invariants independently. --- ## Core invariants Every program must maintain these properties: | Invariant | Meaning | | -------------------------- | ------------------------------------------------------------------------- | | **Exact in/out symmetry** | `exactIn(X) -> Y` implies `exactOut(Y) -> X` within rounding tolerance | | **Monotonicity** | Larger swaps get equal or worse prices (price cannot improve with size) | | **Rounding favors maker** | `amountIn` rounds up (ceil), `amountOut` rounds down (floor) | | **Balance sufficiency** | `amountOut <= balanceOut` must hold; revert otherwise | | **Quote/swap consistency** | `quote()` and `swap()` must produce identical amounts for the same inputs | | **Strategy liveness** | When one reserve is depleted, reverse-direction swaps should restore it | --- ## Related - [Program Order Strategy](./program-order-and-strategy): the SwapVM container that wraps a Program and becomes an Aqua Strategy - [SwapVM Instructions](./instruction-set-overview): full instruction reference - Modifiers: how to add fees, MEV protection, and rate guards to any program - [Patterns & decision tree](./patterns/patterns-and-decision-tree): pick a strategy pattern https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/swapvm-vs-uniswap-v4-hooks-vs-algebra.md 1inch Aqua's **SwapVM** is a programmable pricing engine, in the same category as Uniswap v4 hooks, Algebra Integral plugins, and Balancer v3 hooks. The difference is where the liquidity lives and how you extend the pricing. ## At a glance | | SwapVM (1inch Aqua) | Uniswap v4 hooks | Algebra plugins | Balancer v3 hooks | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------- | ------------------------------------------- | | **Custody** | Self-custodial: tokens stay in the maker's wallet under a revocable allowance; the protocol holds 0 tokens | Pooled: tokens deposited into the PoolManager | Pooled: tokens in the pool | Pooled: tokens in the Vault | | **Capital per position** | One wallet balance backs many strategies on a chain (Shared Liquidity Ratio) | One deposit per position | One deposit per position | One deposit per pool | | **Extend pricing** | Compose opcodes (Path B) or embed a contract via `Extruction` (Path C); or a custom `AquaApp` (Path A) | Write a hook contract with before/after callbacks | Write a plugin (AbstractPlugin + factory) | Write a hook or a custom pool (5 functions) | | **No-code path** | Yes: compose deployed opcodes into a program, no new contract (Path B) | No: a hook is a deployed contract | No: a plugin is a deployed contract | No: a hook/pool is a deployed contract | | **Deployment** | No contract to deploy for Path B; ship a strategy from your wallet | Deploy the hook + mine an address with the right permission flags | Deploy the plugin + register with the factory | Deploy the hook/pool + register | | **Safety model** | Opcodes are curated on an immutable router; custom logic via `Extruction` must be non-upgradeable and return identical `quote()`/`swap()` amounts | Hook permissions encoded in the address; hook bugs affect the pool | Plugin flags library; plugin bugs affect the pool | Hook flags; hook bugs affect the pool | ## Which to reach for - **Compose existing opcodes (Path B)** when a constant-product, concentrated, pegged or decay curve -- with fees and access control -- covers your case. No contract, no audit of new code. - **Extruction (Path C)** when you have proprietary pricing in your own non-upgradeable contract and want it priced against wallet-held liquidity. - **Custom AquaApp (Path A)** when you are building a protocol on Aqua as a shared-inventory substrate. See [Write your own opcode](./write-your-own-opcode), Extruction, and [Build an AquaApp](../getting-started/build-an-aquaapp). https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/instruction-set-overview.md Reference for the built-in instruction contracts in `src/instructions/` of the SwapVM engine. Each instruction is a Solidity contract method that SwapVM, the Aqua swap engine, calls while it walks a strategy program during `quote()` and `swap()`. :::info Programs encode opcode numbers, so each `@1inch/swap-vm-sdk` version targets a specific router opcode table, and strategy bytecode is not portable across router versions. The tables below separate the instruction families registered on the deployed Aqua router (`AquaSwapVMRouter` v1.0.1, wired through `AquaOpcodes`) from families that live on other routers, are experimental, or are not registered. ::: --- ## Naming convention The suffix on each instruction function name indicates its execution context and token scope: | Suffix | Meaning | | ------ | ---------------------------------------------------------------------- | | `XD` | Runs in both static (quote) and dynamic (swap) contexts | | `1D` | Dynamic only: reads or writes on-chain state; single-token direction | | `2D` | Dynamic, two-token: operates on both `tokenIn` and `tokenOut` together | --- ## Deployed Aqua opcode set The deployed Aqua router (`AquaSwapVMRouter` v1.0.1, through `AquaOpcodes`) registers exactly seven instruction families: `Controls`, `XYCSwap`, `XYCConcentrate`, `Decay`, `Fee`, `PeggedSwap`, and `Extruction`. Only these are callable on-chain through Aqua today. The subsections below list each family and its functions. ### Swap formulas (terminal) | Instruction | Function | Description | | -------------- | -------------------------------- | ---------------------------------------------------------------- | | XYCSwap | `_xycSwapXD` | Constant-product AMM (`x x y = k`) | | XYCConcentrate | `_xycConcentrateGrowLiquidity2D` | Concentrated liquidity with `sqrtPriceMin`/`sqrtPriceMax` bounds | | PeggedSwap | `_peggedSwapGrowPriceRange2D` | Square-root linear curve for pegged assets | These formulas read and write virtual reserves. Virtual reserves are not one of the seven registered families; they are the router's reserve and accounting state, exposed through a base `Balances` mixin that the swap formulas read and write internally rather than a top-level opcode composed into most programs. The `Balances` mixin exposes: | Instruction | Function | Description | | ----------- | -------------------- | -------------------------------------------------------------- | | Balances | `_staticBalancesXD` | Set fixed virtual reserves from embedded args | | Balances | `_dynamicBalancesXD` | Load or init reserves from storage; persist changes after swap | :::info Virtual reserves are an internal accounting counter. Maker tokens stay in the maker's own wallet under a revocable, per-chain, per-token allowance, and move only when a taker fills a swap atomically. The protocol holds no tokens. ::: ### Pre-swap price modifier | Instruction | Function | Description | | ----------- | ---------- | ---------------------------------------------- | | Decay | `_decayXD` | Mooniswap-style virtual offset with time decay | ### Fees Fee instructions embed the fee into the strategy program and the swap amounts; there is no on-chain fee event. In the MVP only the LP fee (shown in the interface as the "Swap fee") is non-zero. The protocol fee and dynamic protocol fee exist in the contracts but are set to 0 in v1 and are not displayed. LP fees auto-compound through the registry's `Pushed` credit. | Instruction | Function | Applied to | Transfer | | ----------- | ----------------------------------- | ---------- | ------------------------ | | Fee | `_flatFeeAmountInXD` | `amountIn` | None | | Fee | `_protocolFeeAmountInXD` | `amountIn` | ERC-20 | | Fee | `_aquaProtocolFeeAmountInXD` | `amountIn` | Aqua pull | | Fee | `_dynamicProtocolFeeAmountInXD` | `amountIn` | ERC-20 (dynamic rate) | | Fee | `_aquaDynamicProtocolFeeAmountInXD` | `amountIn` | Aqua pull (dynamic rate) | ### Control flow and access guards | Instruction | Function | Description | | ----------- | ------------------------------- | -------------------------------------------- | | Controls | `_salt` | No-op; used for order hash uniqueness | | Controls | `_jump` | Unconditional jump to program counter | | Controls | `_jumpIfTokenIn` | Conditional jump if `tokenIn` matches | | Controls | `_jumpIfTokenOut` | Conditional jump if `tokenOut` matches | | Controls | `_deadline` | Revert if past timestamp | | Controls | `_onlyTakerTokenBalanceNonZero` | Revert if taker holds no balance | | Controls | `_onlyTakerTokenBalanceGte` | Revert if taker balance below threshold | | Controls | `_onlyTakerTokenSupplyShareGte` | Revert if taker supply share below threshold | :::info The launch taker gate is the `_onlyTxOriginTokenBalanceNonZero(token)` control, which reverts with `TxOriginTokenBalanceIsZero` unless `balanceOf(tx.origin) > 0`. The dApp assembler applies it to every strategy by calling `withTxOriginAccessToken(aquaKycToken)`, so the taker must hold the `KycNFT` credential. Because the check is on `tx.origin`, smart accounts, multisigs, and ERC-4337 bundlers cannot pass it. The gate is evaluated at swap time, not at ship time; makers stay permissionless and only takers are gated. Per-strategy taker allow-lists (`_whitelistSingleTaker` / `_whitelistMultipleTakers`) exist in source but are not registered on-chain today, so do not build against them. ::: ### Extruction (external pricing) | Instruction | Function | Description | | ----------- | ------------- | -------------------------------------------------------------------------- | | Extruction | `_extruction` | Delegate to an external contract for proprietary pricing via `IExtruction` | :::info An `Extruction` target must be non-upgradeable, and it must return identical amounts from `quote()` and `swap()`. The canonical spelling is `Extruction` / `IExtruction` (never "extraction"). ::: --- ## Not registered on the deployed Aqua router The following instruction families exist in the engine source but are not part of the deployed Aqua opcode set. Programs on the Aqua router cannot call them. ### Limit Order and Fusion opcode set Order invalidation and the limit, TWAP, and Dutch-auction primitives belong to the Limit Order and Fusion product on a separate router, not to Aqua. Aqua strategies have no order-level partial fills. `Invalidators` and `SeriesEpochManager` are limit-order families and are not Aqua opcodes. | Instruction | Function | Description | | ------------ | --------------------------- | ------------------------------------------------------------- | | Invalidators | `_invalidateBit1D` | One-time execution via bitmap | | Invalidators | `_invalidateTokenIn1D` | Track cumulative `amountIn` against `balanceIn` cap | | Invalidators | `_invalidateTokenOut1D` | Track cumulative `amountOut` against `balanceOut` cap | | LimitSwap | `_limitSwap1D` | Fixed-rate swap; partial fills allowed | | LimitSwap | `_limitSwapOnlyFull1D` | Fixed-rate swap; full fill only | | TWAPSwap | `_twap` | TWAP with linear unlocking and Dutch auction decay | | DutchAuction | `_dutchAuctionBalanceIn1D` | Pre-swap Dutch auction modifier: decays `balanceIn` over time | | DutchAuction | `_dutchAuctionBalanceOut1D` | Pre-swap Dutch auction modifier: grows `balanceOut` over time | ### Additional engine instructions Present in `src/instructions/` but not part of the deployed Aqua opcode set. | Instruction | Function | Description | | ------------------- | ------------------------ | ------------------------------------------------- | | BaseFeeAdjuster | `_baseFeeAdjuster1D` | Improves rate when gas price exceeds baseline | | OraclePriceAdjuster | `_oraclePriceAdjuster1D` | Adjusts amounts toward Chainlink oracle price | | MinRate | `_requireMinRate1D` | Reverts if exchange rate below minimum | | MinRate | `_adjustMinRate1D` | Caps amounts at minimum rate instead of reverting | ### Experimental fees (not shipped) The `FeeExperimental` family is not part of the deployed opcode set. "LP progressive fees" do not exist in the shipped product. | Instruction | Function | Applied to | Transfer | | --------------- | ----------------------------- | ----------- | ----------------------- | | FeeExperimental | `_flatFeeAmountOutXD` | `amountOut` | None | | FeeExperimental | `_progressiveFeeInXD` | `amountIn` | None (volume-sensitive) | | FeeExperimental | `_progressiveFeeOutXD` | `amountOut` | None (volume-sensitive) | | FeeExperimental | `_protocolFeeAmountOutXD` | `amountOut` | ERC-20 | | FeeExperimental | `_aquaProtocolFeeAmountOutXD` | `amountOut` | Aqua pull | ### Debug (Foundry only) Debug instructions are available only in Foundry test builds and are not registered on-chain. | Instruction | Function | Description | | ----------- | ------------------------- | --------------------------------------------------------------------- | | Debug | `_printSwapRegisters` | Log `balanceIn`, `balanceOut`, `amountIn`, `amountOut` | | Debug | `_printSwapQuery` | Log `orderHash`, `taker`, `maker`, `tokenIn`, `tokenOut`, `isExactIn` | | Debug | `_printContext` | Log `nextPC` and `takerArgs` | | Debug | `_printGasLeft` | Log `gasleft()` | | Debug | `_printFreeMemoryPointer` | Log free memory pointer | --- ## Related - [Patterns & decision tree](./patterns/patterns-and-decision-tree): recipes that compose these instructions ## Canonical opcode table The table below is the complete, deployed Aqua instruction set as registered in `src/opcodes/AquaOpcodes.sol` (the opcode contract mixed into `AquaSwapVMRouter`). Every numeric index is taken directly from the `_runOpcode` dispatcher in that file, which is authoritative: the router dispatches by these exact indices. Instructions are grouped by their source contract (family). The _SDK builder method_ column shows how the instruction is emitted with `ProgramBuilder` (`Program memory program = ProgramBuilder.init(_opcodes())`); the _Args helper_ column shows the `*ArgsBuilder` library call that produces the packed argument bytes. :::info Indices are contiguous only within a family. Gaps in the numbering (0-9, and 22-26) map to a reserved `_notInstruction` sentinel and are **not** registered -- 0-9 are held for debugging utilities, 22-26 are held for backward-compatibility slots. Calling an unregistered index reverts. ::: | Family | Function | Opcode index | SDK builder method | Args helper | | -------------- | ----------------------------------- | ------------ | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | Controls | `_jump` | 10 | `program.build(_jump, args)` | `ControlsArgsBuilder.buildJump(uint16 nextPC)` | | Controls | `_jumpIfTokenIn` | 11 | `program.build(_jumpIfTokenIn, args)` | `ControlsArgsBuilder.buildJumpIfToken(address token, uint16 nextPC)` | | Controls | `_jumpIfTokenOut` | 12 | `program.build(_jumpIfTokenOut, args)` | `ControlsArgsBuilder.buildJumpIfToken(address token, uint16 nextPC)` | | Controls | `_deadline` | 13 | `program.build(_deadline, args)` | `ControlsArgsBuilder.buildDeadline(uint40 deadline)` | | Controls | `_onlyTakerTokenBalanceNonZero` | 14 | `program.build(_onlyTakerTokenBalanceNonZero, args)` | `ControlsArgsBuilder.buildTokenBalanceNonZero(address token)` | | Controls | `_onlyTakerTokenBalanceGte` | 15 | `program.build(_onlyTakerTokenBalanceGte, args)` | `ControlsArgsBuilder.buildTakerTokenBalanceGte(address token, uint256 minAmount)` | | Controls | `_onlyTakerTokenSupplyShareGte` | 16 | `program.build(_onlyTakerTokenSupplyShareGte, args)` | `ControlsArgsBuilder.buildTakerTokenSupplyShareGte(address token, uint64 minShareE18)` | | XYCSwap | `_xycSwapXD` | 17 | `program.build(_xycSwapXD)` | -- (no args; the `args` field is ignored) | | XYCConcentrate | `_xycConcentrateGrowLiquidity2D` | 18 | `program.build(_xycConcentrateGrowLiquidity2D, args)` | `XYCConcentrateArgsBuilder.build2D(uint256 sqrtPriceMin, uint256 sqrtPriceMax)` | | Decay | `_decayXD` | 19 | `program.build(_decayXD, args)` | `DecayArgsBuilder.build(uint16 decayPeriod)` | | Controls | `_salt` | 20 | `program.build(_salt, args)` | `ControlsArgsBuilder.buildSalt(uint64 salt)` / `buildSalt(bytes salt)` | | Fee | `_flatFeeAmountInXD` | 21 | `program.build(_flatFeeAmountInXD, args)` | `FeeArgsBuilder.buildFlatFee(uint32 feeBps)` | | Fee | `_protocolFeeAmountInXD` | 27 | `program.build(_protocolFeeAmountInXD, args)` | `FeeArgsBuilder.buildProtocolFee(uint32 feeBps, address to)` | | Fee | `_aquaProtocolFeeAmountInXD` | 28 | `program.build(_aquaProtocolFeeAmountInXD, args)` | `FeeArgsBuilder.buildProtocolFee(uint32 feeBps, address to)` | | Fee | `_dynamicProtocolFeeAmountInXD` | 29 | `program.build(_dynamicProtocolFeeAmountInXD, args)` | `FeeArgsBuilder.buildDynamicProtocolFee(address feeProvider)` | | Fee | `_aquaDynamicProtocolFeeAmountInXD` | 30 | `program.build(_aquaDynamicProtocolFeeAmountInXD, args)` | `FeeArgsBuilder.buildDynamicProtocolFee(address feeProvider)` | | PeggedSwap | `_peggedSwapGrowPriceRange2D` | 31 | `program.build(_peggedSwapGrowPriceRange2D, args)` | `PeggedSwapArgsBuilder.build(PeggedSwapArgsBuilder.Args{x0, y0, linearWidth, rateLt, rateGt})` | | Extruction | `_extruction` | 32 | `program.build(_extruction, args)` | -- (no dedicated builder; `args = abi.encodePacked(address target, bytes extructionArgs)`) | | Controls | `_onlyTxOriginTokenBalanceNonZero` | 33 | `program.build(_onlyTxOriginTokenBalanceNonZero, args)` | `ControlsArgsBuilder.buildTokenBalanceNonZero(address token)` | ### Reserves (Balances) on the Aqua router On the deployed Aqua router there is **no reserve-setting opcode** in the program. Reserves are sourced from Aqua itself: before the program runs, `SwapVM` loads them with `AQUA.safeBalances(maker, router, orderHash, tokenIn, tokenOut)` into `ctx.swap.balanceIn` / `ctx.swap.balanceOut` whenever the order uses `useAquaInsteadOfSignature`. The maker provisions those reserves at ship time via `aqua.ship(swapVM, strategy, tokens, balances)`. Instructions such as `_xycSwapXD`, `_xycConcentrateGrowLiquidity2D` and `_peggedSwapGrowPriceRange2D` then read the reserves already present in `ctx.swap`. :::info The signature-mode `Balances` opcodes -- `_staticBalancesXD`, `_dynamicBalancesXD` and the `BalancesArgsBuilder.build(uint256[2] balances)` helper -- belong to the generic `SwapVMRouter` opcode set (`src/opcodes/Opcodes.sol`), **not** to `AquaOpcodes`. They are not registered on the Aqua router. ::: :::note Opcodes not in this table are not registered on the deployed Aqua router. At program-build time, referencing an instruction the router does not register reverts `OpcodeNotFound` (`ProgramBuilder.findOpcode`); at run time, dispatching any index outside this set reverts `UnknownOpcode(opcode)` from `AquaOpcodes._runOpcode`. ::: https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/opcode-gallery.md This page is a directory of every opcode family registered on the deployed Aqua router, **AquaSwapVMRouter** v1.0.2 (`0x111111338c5091e8440b67b168bae16a668ac0de`). Each family maps to one instruction contract in [1inch/swap-vm](https://github.com/1inch/swap-vm/tree/main/src/instructions). The router dispatches an opcode through `_dispatch`, which calls `_runOpcode` in [AquaOpcodes.sol](https://github.com/1inch/swap-vm/blob/main/src/opcodes/AquaOpcodes.sol): the numeric index in the bytecode selects the instruction function directly. :::info An opcode is one byte in the program, immediately followed by a one-byte args length and then that many argument bytes. The dispatcher (`AquaOpcodes._opcodes()`) is a fixed jump table. Reserved gap indices (0-9 and 22-26) map to `_notInstruction`, an empty function -- reaching one is a **no-op** (execution continues; it neither prices nor reverts). An opcode byte **beyond the registered table** reverts with a Solidity array-out-of-bounds panic (`Panic(0x32)`). There is no named `UnknownOpcode`/`OpcodeNotFound` error and no dynamic registration on the deployed v1.0.2 router. ::: ## Registered opcode summary The table lists every opcode registered in the deployed `_runOpcode` dispatcher, grouped by instruction family. Indices are the exact numeric slots from [AquaOpcodes.sol](https://github.com/1inch/swap-vm/blob/main/src/opcodes/AquaOpcodes.sol). The SDK builder column names the exported `Opcode` from `@1inch/swap-vm` (package path `typescript/swap-vm/src/swap-vm/instructions`). | Family | Opcode index | Function | SDK builder | Args (byte schema) | Source | | -------------- | ------------ | ----------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------- | | Controls | 10 | `_jump` | `jump` | `nextPC` 2 (uint16) | `src/instructions/Controls.sol` | | Controls | 11 | `_jumpIfTokenIn` | `jumpIfTokenIn` | `token` 20 + `nextPC` 2 | `src/instructions/Controls.sol` | | Controls | 12 | `_jumpIfTokenOut` | `jumpIfTokenOut` | `token` 20 + `nextPC` 2 | `src/instructions/Controls.sol` | | Controls | 13 | `_deadline` | `deadline` | `deadline` 5 (uint40) | `src/instructions/Controls.sol` | | Controls | 14 | `_onlyTakerTokenBalanceNonZero` | `onlyTakerTokenBalanceNonZero` | `token` 20 | `src/instructions/Controls.sol` | | Controls | 15 | `_onlyTakerTokenBalanceGte` | `onlyTakerTokenBalanceGte` | `token` 20 + `minAmount` 32 | `src/instructions/Controls.sol` | | Controls | 16 | `_onlyTakerTokenSupplyShareGte` | `onlyTakerTokenSupplyShareGte` | `token` 20 + `minShareE18` 8 (uint64) | `src/instructions/Controls.sol` | | Controls | 20 | `_salt` | `salt` | `salt` 8 (uint64); arbitrary bytes also accepted | `src/instructions/Controls.sol` | | Controls | 33 | `_onlyTxOriginTokenBalanceNonZero` | `onlyTxOriginTokenBalanceNonZero` | `token` 20 | `src/instructions/Controls.sol` | | XYCSwap | 17 | `_xycSwapXD` | `xycSwapXD` | none (0 bytes) | `src/instructions/XYCSwap.sol` | | XYCConcentrate | 18 | `_xycConcentrateGrowLiquidity2D` | `concentrateGrowLiquidity2D` | `sqrtPriceMin` 32 + `sqrtPriceMax` 32 | `src/instructions/XYCConcentrate.sol` | | Decay | 19 | `_decayXD` | `decayXD` | `period` 2 (uint16) | `src/instructions/Decay.sol` | | Fee | 21 | `_flatFeeAmountInXD` | `flatFeeAmountInXD` | `feeBps` 4 (uint32) | `src/instructions/Fee.sol` | | Fee | 27 | `_protocolFeeAmountInXD` | `protocolFeeAmountInXD` | `feeBps` 4 + `to` 20 | `src/instructions/Fee.sol` | | Fee | 28 | `_aquaProtocolFeeAmountInXD` | `aquaProtocolFeeAmountInXD` | `feeBps` 4 + `to` 20 | `src/instructions/Fee.sol` | | Fee | 29 | `_dynamicProtocolFeeAmountInXD` | `dynamicProtocolFeeAmountInXD` | `feeProvider` 20 | `src/instructions/Fee.sol` | | Fee | 30 | `_aquaDynamicProtocolFeeAmountInXD` | `aquaDynamicProtocolFeeAmountInXD` | `feeProvider` 20 | `src/instructions/Fee.sol` | | PeggedSwap | 31 | `_peggedSwapGrowPriceRange2D` | `peggedSwapGrowPriceRange2D` | `x0` 32 + `y0` 32 + `linearWidth` 32 + `rateLt` 32 + `rateGt` 32 (160 total) | `src/instructions/PeggedSwap.sol` | | Extruction | 32 | `_extruction` | `extruction` | `target` 20 + `extructionArgs` N | `src/instructions/Extruction.sol` | :::note Opcodes outside this registered set are **not** executable on the deployed router: the reserved gaps (indices 0-9, 22-26) resolve to the no-op `_notInstruction`, and an index past the table reverts with an array-out-of-bounds panic (`Panic(0x32)`). This includes every experimental fee opcode in `FeeExperimental.sol` (for example `flatFeeAmountOutXD`, `progressiveFeeInXD`, `protocolFeeAmountOutXD`): those SDK builders exist for tooling but their opcodes are not wired into `AquaSwapVMRouter` v1.0.2. ::: ## Families ### Controls Control flow and access-guard instructions. They never compute swap amounts -- they redirect execution (jumps) or revert on a failed state check. `_jump`, `_jumpIfTokenIn` and `_jumpIfTokenOut` set the next program counter (targets limited to uint16, 0-65,535). `_deadline` reverts once `block.timestamp` passes the encoded time. `_onlyTakerTokenBalanceNonZero`, `_onlyTakerTokenBalanceGte` and `_onlyTakerTokenSupplyShareGte` gate the fill on the taker's ERC-20 or NFT holdings; `_onlyTxOriginTokenBalanceNonZero` gates on `tx.origin` instead (a weaker check -- see the source notes on delegation). `_salt` is a no-op that adds uniqueness to the order hash. Registered indices: 10-16, 20, 33. See the [Controls instruction reference](/portal/documentation/aqua/swapvm/aqua-router-opcodes/controls). ### XYCSwap Constant-product AMM (`x x y = k`). Index 17, function `_xycSwapXD`. Terminal swap formula that takes no args and requires both virtual balances to be non-zero; on exact-in it computes `amountOut`, on exact-out it computes `amountIn` with ceil-division to favor the maker. See the [XYCSwap instruction reference](/portal/documentation/aqua/swapvm/aqua-router-opcodes/xyc-swap). ### XYCConcentrate Concentrated-liquidity swap bounded by a sqrt price range (Uniswap-v3-style), for two tokens only. Index 18, function `_xycConcentrateGrowLiquidity2D`. Args are two uint256 sqrt-price bounds in 1e18 fixed point (`sqrtPriceMin`, `sqrtPriceMax`) where price `P = tokenGt / tokenLt`. The instruction recomputes liquidity `L` from the real balances each swap, derives virtual reserves, and performs a single constant-product step; fee reinvestment happens implicitly as real balances grow. See the [XYCConcentrate instruction reference](/portal/documentation/aqua/swapvm/aqua-router-opcodes/xyc-concentrate). ### Decay Mooniswap-style virtual-balance protection. Index 19, function `_decayXD`. Args are a uint16 `period` (2 bytes). It adjusts `balanceIn`/`balanceOut` by decaying offsets stored per order, token and direction, then runs the inner loop; after a real swap it records new offsets so a large swap temporarily worsens the rate and restores linearly over the decay period. Reads offset state in quote mode without updating it, so quote and swap can diverge if state changed between calls. See the [Decay instruction reference](/portal/documentation/aqua/swapvm/aqua-router-opcodes/decay). ### Fee Fee instructions applied to `amountIn`. All use the shared BPS scale where `1e9 = 100%` and `feeBps 1e9` (for example 0.30% = `3000000`). `_flatFeeAmountInXD` (index 21) keeps the fee inside the pool -- this is the only non-zero fee in v1. `_protocolFeeAmountInXD` (27) transfers the fee from the maker to a recipient via `safeTransferFrom`; `_aquaProtocolFeeAmountInXD` (28) pulls it from the maker's Aqua balance instead. `_dynamicProtocolFeeAmountInXD` (29) and `_aquaDynamicProtocolFeeAmountInXD` (30) read `feeBps` and recipient from an external provider via `staticcall`. Registered indices: 21, 27-30. See the [Fee instruction reference](/portal/documentation/aqua/swapvm/aqua-router-opcodes/fee). ### PeggedSwap Square-root linear curve for pegged assets (stablecoin, wrapped and LST/LRT pairs). Index 31, function `_peggedSwapGrowPriceRange2D`. Args are 160 bytes: five uint256 fields `x0`, `y0`, `linearWidth`, `rateLt`, `rateGt` (initial normalization reserves, the linear coefficient A scaled by 1e27, and per-token rate multipliers assigned by address order). The curve `(x/X) + (y/Y) + A(x/X + y/Y) = 1 + A` is solved analytically at fixed curvature p = 0.5. See the [PeggedSwap instruction reference](/portal/documentation/aqua/swapvm/aqua-router-opcodes/pegged-swap). ### Extruction Delegation to external maker logic for advanced strategies. Index 32, function `_extruction`. Args are a 20-byte `target` address followed by N bytes of `extructionArgs` forwarded to the callee. In quote mode it calls `IStaticExtruction.extruction` (view); in swap mode `IExtruction.extruction` (state-modifying). The target returns an updated program counter, a chopped-length of consumed taker args, and updated swap registers. Targets must be deterministic and consistent across both interfaces or quote and swap will diverge. See the [Extruction instruction reference](/portal/documentation/aqua/swapvm/aqua-router-opcodes/extruction). ## Precision details This section pins down four things that trip up program authors reading the summary table above: the exact byte offset of each argument field, how the SDK's raw-price helper maps onto the sqrt-price bytes the opcode actually stores, the difference between the decimal indices shown here and the hex bytes in a program, and why the flat-fee argument is called `feeBps` on-chain but built with `FlatFeeArgs.fromBps` in the SDK. Everything below is verified against [1inch/swap-vm](https://github.com/1inch/swap-vm/tree/v1.0.1) at tag `v1.0.1` and the matching `@1inch/swap-vm` TypeScript coders. ### Argument byte offsets Argument blobs are built with `abi.encodePacked` and read back with fixed `slice(start, end)` calls, so fields are concatenated in declaration order with **no padding and no length prefixes between them**. The one length byte that precedes the blob (see the info panel at the top of this page) is the _total_ args length; individual fields are located purely by offset. The table gives the offset of every field for the opcodes where the layout is non-trivial (more than one field, or a field wider than the opcode's mnemonic implies). | Opcode | Field | Offset (bytes) | Length | Type | | ----------------------------------------------------------------- | -------------- | -------------- | ------ | ------- | | `_jumpIfTokenIn` (11) / `_jumpIfTokenOut` (12) | `token` | 0 | 20 | address | | | `nextPC` | 20 | 2 | uint16 | | `_onlyTakerTokenBalanceGte` (15) | `token` | 0 | 20 | address | | | `minAmount` | 20 | 32 | uint256 | | `_onlyTakerTokenSupplyShareGte` (16) | `token` | 0 | 20 | address | | | `minShareE18` | 20 | 8 | uint64 | | `_flatFeeAmountInXD` (21) | `feeBps` | 0 | 4 | uint32 | | `_protocolFeeAmountInXD` (27) / `_aquaProtocolFeeAmountInXD` (28) | `feeBps` | 0 | 4 | uint32 | | | `to` | 4 | 20 | address | | `_xycConcentrateGrowLiquidity2D` (18) | `sqrtPriceMin` | 0 | 32 | uint256 | | | `sqrtPriceMax` | 32 | 32 | uint256 | | `_peggedSwapGrowPriceRange2D` (31) | `x0` | 0 | 32 | uint256 | | | `y0` | 32 | 32 | uint256 | | | `linearWidth` | 64 | 32 | uint256 | | | `rateLt` | 96 | 32 | uint256 | | | `rateGt` | 128 | 32 | uint256 | :::info The guard opcodes all lead with the 20-byte `token` address and then append their value: `_onlyTakerTokenBalanceGte` reads `minAmount` as a full uint256 at offset 20 (total 52 bytes), while `_onlyTakerTokenSupplyShareGte` reads only an 8-byte `minShareE18` at offset 20 (total 28 bytes). `PeggedSwap` requires the blob to be at least 160 bytes (`5 x 32`) and reverts `PeggedSwapInvalidArgsLength` otherwise; it then casts the calldata pointer directly onto the five fields with no copy. `rateLt`/`rateGt` are assigned to input/output by address order at runtime (`tokenIn < tokenOut` keeps them in order, otherwise they swap), which is why they are stored as an ordered pair rather than an in/out pair. ::: ### rawPrice -> sqrtPrice bridge (XYCConcentrate) The `_xycConcentrateGrowLiquidity2D` opcode stores **sqrt prices**, not prices: two `uint256` fields, `sqrtPriceMin` and `sqrtPriceMax`, each 32 bytes in 1e18 fixed-point, where `P = tokenGt / tokenLt` (higher-address token over lower-address token). The Solidity builder `XYCConcentrateArgsBuilder.build2D` takes those sqrt values directly and requires `0 < sqrtPriceMin < sqrtPriceMax`. The SDK exposes two constructors on `ConcentrateGrowLiquidity2DArgs` so callers can supply whichever form they hold. If you already have sqrt values, use `fromSqrtPrices`; if you hold plain prices, use `fromRawPrices`, which does the sqrt conversion for you before encoding. The bridge is: ``` // raw price P in 1e18 fixed-point -> stored sqrt price in 1e18 fixed-point sqrtPrice = bigintSqrt(rawPrice * 1e18) // so (sqrtPrice / 1e18)^2 == rawPrice / 1e18 // @1inch/swap-vm ConcentrateGrowLiquidity2DArgs.fromRawPrices(rawPriceMin, rawPriceMax) // is equivalent to ConcentrateGrowLiquidity2DArgs.fromSqrtPrices( bigintSqrt(rawPriceMin * 1e18n), bigintSqrt(rawPriceMax * 1e18n), ) ``` Both paths land in the same coder, which writes exactly `addUint256(sqrtPriceMin)` then `addUint256(sqrtPriceMax)` -- the 64-byte layout in the offsets table above. The contract reads them back with `slice(0, 32)` and `slice(32, 64)`. :::note Do not hand a raw price to `fromSqrtPrices` (or a sqrt price to `fromRawPrices`). Either mistake encodes valid-looking bytes that price a completely different range: the opcode never re-derives one form from the other, it trusts the stored sqrt values as-is when it recomputes liquidity `L` from real balances each swap. ::: ### Opcode radix: decimal here, hex in program bytes Every opcode index in this gallery is written in **decimal**, matching the numeric slots in `AquaOpcodes.sol`. In an actual program each opcode is a single byte, so when you inspect or hand-assemble `order.data` (`hooks || program`) you will see the index in **hex**. Convert before you read raw bytes: | Decimal (gallery) | Hex (program byte) | Function | | ----------------- | ------------------ | ---------------------------------- | | 17 | `0x11` | `_xycSwapXD` | | 18 | `0x12` | `_xycConcentrateGrowLiquidity2D` | | 21 | `0x15` | `_flatFeeAmountInXD` | | 27 | `0x1b` | `_protocolFeeAmountInXD` | | 31 | `0x1f` | `_peggedSwapGrowPriceRange2D` | | 32 | `0x20` | `_extruction` | | 33 | `0x21` | `_onlyTxOriginTokenBalanceNonZero` | So a byte reading `0x11` in the program is index 17, `XYCSwap` -- not the decimal value 11 (which is `0x0b`, `_jumpIfTokenIn`). The reserved gap indices are equally worth converting: decimal 0-9 is `0x00`-`0x09` and decimal 22-26 is `0x16`-`0x1a`, all mapping to the no-op `_notInstruction`. ### Fee argument naming: `feeBps` vs `FlatFeeArgs.fromBps` The names collide but describe different scales. On-chain the field is called `feeBps`, yet it is **not** basis points: `Fee.sol` defines `BPS = 1e9` and treats `feeBps` as a value on a 1e9 = 100% scale, requiring `feeBps 1e9`. The SDK stores that same 1e9-scale integer in the field `FlatFeeArgs.fee` (a `uint32`), while the factory `FlatFeeArgs.fromBps` takes **real** basis points (10,000 bps = 100%) and scales up by 100,000 to reach the on-chain units: ```typescript // @1inch/swap-vm -- FlatFeeArgs FlatFeeArgs.fromBps(30); // 30 bps = 0.30% -> fee = 30 * 100000 = 3_000_000 FlatFeeArgs.fromPercent(0.3); // 0.30% -> fromBps(0.3 * 100) = 3_000_000 FlatFeeArgs.fromBps(10000); // 100% -> fee = 10000 * 100000 = 1_000_000_000 (1e9) new FlatFeeArgs(3_000_000n); // raw 1e9-scale constructor, same as fromBps(30) ``` The coder encodes `fee` as a single `uint32` (`addUint32`), which is the 4-byte `feeBps` field the opcode parses with `parseFlatFee` at offset 0. So the summary table's `feeBps 4 (uint32)` and the SDK's `FlatFeeArgs.fromBps(...)` agree on the bytes; only the _input unit_ differs (1e9-scale on-chain and in the `fee` field, real bps at the `fromBps` boundary). The example in the Fee family note above -- `0.30% = 3000000` -- is exactly `FlatFeeArgs.fromBps(30)`. The same 1e9 scale and the same `fromBps`/`fromPercent` convention apply to the protocol-fee opcodes (27-30), whose `feeBps` field shares the identical 4-byte uint32 layout. https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/write-your-own-opcode.md source-verifiedGreen This page shows how to add a custom instruction to a SwapVM router: write the instruction, register it in the router's opcode set, and build a program with `ProgramBuilder`. It also documents the compiler settings the dispatch mechanism depends on, the EIP-170 constraint that forces curation of the native opcode set, the taker-side `TakerTraits` layout and EIP-1271 signature path, and a set of verified tooling footguns that repeatedly bite integrators. :::info Everything below is verified against `1inch/swap-vm` (`src/`, HEAD) and the `1inch/swap-vm-template` reference project. Where a claim comes from an integrator report that could not be confirmed against source it is explicitly labelled _reported, unverified_. ::: ## The instruction model A SwapVM program is a flat byte string. The run loop (`src/libs/VM.sol`, `ContextLib.runLoop`) walks it two bytes at a time: the first byte is the **opcode**, the second is the **argument length** (0-255), followed by that many argument bytes. ``` [opcode:1][argsLen:1][args:argsLen] [opcode:1][argsLen:1][args:argsLen] ... ``` For each instruction the loop calls a dispatcher stored as an internal function pointer on the VM (`ctx.vm.dispatch`), which the router wires to `_runOpcode(ctx, opcode, args)`. Every instruction has exactly this signature: ```solidity function _myInstruction(Context memory ctx, bytes calldata args) internal; ``` The instruction receives the whole execution `Context` (`src/libs/VM.sol`) and may read and mutate it: | Field | Meaning | Writable by an instruction? | | ----------------------------------- | -------------------------------------------------------------- | --------------------------- | | `ctx.query.taker` | The swap caller -- set to `msg.sender` in `SwapVM.swap`/`quote` | read-only | | `ctx.query.maker` | Liquidity provider / order signer | read-only | | `ctx.query.tokenIn` / `tokenOut` | Resolved swap direction | read-only | | `ctx.query.orderHash` | Per-maker strategy/position id | read-only | | `ctx.query.isExactIn` | Exact-in vs exact-out | read-only | | `ctx.swap.balanceIn` / `balanceOut` | Maker balances (Aqua-shipped or injected) | yes | | `ctx.swap.amountIn` / `amountOut` | The swap registers being computed | **yes** | | `ctx.vm.nextPC` | Program counter -- write it to jump | yes | :::info **An opcode can compute**`amountIn`**, not only**`amountOut`**.** The register that gets filled is whichever side is missing: `isExactIn() ? amountOut : amountIn`. In `XYCSwap._xycSwapXD` the exact-out branch writes `ctx.swap.amountIn = Math.ceilDiv(amountOut * balanceIn, balanceOut - amountOut)`, and `Fee._flatFeeAmountInXD` adjusts `ctx.swap.amountIn` directly. Your instruction is free to price either direction. ::: :::info `ctx.query.taker`**is the caller.** Both `SwapVM.swap` and `SwapVM.quote` construct the context with `taker: msg.sender`. Control instructions rely on this -- e.g. `Controls._onlyTakerTokenBalanceGte` reads `IERC20(token).balanceOf(ctx.query.taker)`. ::: ### Reading taker-supplied arguments Two kinds of data reach an instruction. The **program args** (the `args` parameter) are baked into the maker's strategy and are the same for every taker. **Taker args** are supplied per-swap by the caller (the `instructionsArgs` slice of `TakerTraits`) and are consumed front-to-back via `ContextLib`: ```solidity bytes calldata all = ctx.takerArgs(); // remaining taker args bytes calldata chunk = ctx.tryChopTakerArgs(32); // consume up to 32 bytes ``` `Extruction._extruction` is the canonical example: it forwards `ctx.takerArgs()` to an external contract and then `tryChopTakerArgs(choppedLength)` to consume exactly what that contract reports it used. ## Step by step: write, register, build ### 1. Write the instruction contract Create a contract under `src/instructions/` exposing one or more `internal` functions with the instruction signature. Keep the logic deterministic -- the same input must produce the same output in both `quote()` (static) and `swap()` contexts, or quote/swap consistency breaks. ```solidity // src/instructions/MyCap.sol import { Context, ContextLib } from "../libs/VM.sol"; contract MyCap { using ContextLib for Context; error AmountOutExceedsCap(uint256 amountOut, uint256 cap); /// args: [cap: 32 bytes] function _capAmountOut1D(Context memory ctx, bytes calldata args) internal pure { uint256 cap = uint256(bytes32(args)); require(ctx.swap.amountOut <= cap, AmountOutExceedsCap(ctx.swap.amountOut, cap)); } } ``` ### 2. Register it in the opcode set An opcode set contract (`src/opcodes/Opcodes.sol` for the standard router, `src/opcodes/AquaOpcodes.sol` for the Aqua router) inherits every instruction contract and exposes **two parallel structures that must stay in lockstep**: - `_runOpcode(ctx, opcode, args)` -- the runtime `if/else` dispatcher. Add your branch at the next free index. - `_opcodes()` -- a fixed array of internal function pointers. Add your function pointer at the **same** index. This array is what `ProgramBuilder` uses to translate a function into its opcode byte. :::note The index in `_runOpcode` and the slot in `_opcodes()` MUST match exactly -- the source comment says _"Indices MUST mirror_`{_opcodes}`_exactly."_ A mismatch silently dispatches the wrong instruction. Indices `0-9` are reserved (`_notInstruction`, for debug tooling); real opcodes start at `10`. Always **append** new instructions at the end to preserve backward compatibility with already-deployed programs. ::: ``` // in _runOpcode(...) else if (opcode == 52) MyCap._capAmountOut1D(ctx, args); // next free index // in _opcodes() -- append at the SAME position, and bump the array size MyCap._capAmountOut1D ``` Finally, a router (`src/routers/...`) inherits the opcode set and overrides `_dispatch` to call `_runOpcode`. That is the whole wiring: ```solidity function _dispatch(Context memory ctx, uint256 opcode, bytes calldata args) internal override { _runOpcode(ctx, opcode, args); } ``` ### 3. Build a program with ProgramBuilder `ProgramBuilder` (`test/utils/ProgramBuilder.sol`) turns instruction references into bytecode. It resolves an opcode **by internal function-pointer equality** against the set returned by `_opcodes()`: ```solidity Program memory program = ProgramBuilder.init(_opcodes()); bytes memory bytecode = bytes.concat( program.build(_xycConcentrateGrowLiquidity2D, concentrateArgs), program.build(_flatFeeAmountInXD, feeArgs), program.build(_capAmountOut1D, abi.encodePacked(cap)), // your new opcode program.build(_xycSwapXD) // no args ); ``` `findOpcode` loops `_opcodes()` and returns the index where `self.opcodes[i] == targetOpcode`. If the function pointer is not in the set it reverts `OpcodeNotFound` at build time. That equality check is the reason the whole project is compiled through the IR pipeline (next section). The bytecode becomes the `program` field of a maker order via `MakerTraitsLib.build(...)`. The order's `data` is `tokenA tokenB hooks... program`, with `tokenA < tokenB` enforced. ## Required solc settings The instruction dispatch and the `ProgramBuilder` opcode lookup both hinge on **internal function pointers being stable, comparable values** (`self.opcodes[i] == targetOpcode`). The project only ever compiles this under the Yul/IR pipeline; do the same, or the comparison -- and therefore your generated programs -- cannot be trusted. | Setting | `foundry.toml` (repo) | `hardhat.config.ts` (template) | | -------------- | ------------------------------------ | -------------------------------------------------------------- | | solc | `0.8.30` | `0.8.30` | | IR pipeline | `via_ir = true` | `viaIR: true` | | optimizer | `optimizer = true` | `enabled: true` | | optimizer runs | `optimizer_runs = 700` | `runs: 1` (+ yulDetails `optimizerSteps: "dhfoDgvulfnTUtnIf"`) | | EVM version | not overridden (solc 0.8.30 default) | `evmVersion: "cancun"` | :::note **Compile through IR.** `foundry.toml` sets `via_ir = true` in both the `default` and `ci` profiles, and the template sets `viaIR: true`. Function-pointer identity (used by `ProgramBuilder.findOpcode` and the `_opcodes()` table) resolves correctly under the IR codegen the project builds with; do not switch to legacy codegen. _(That the comparison misbehaves specifically under legacy codegen is reported by integrators and consistent with the source's exclusive use of IR, but was not independently reproduced here -- treat "legacy breaks it" as reported, unverified; "build with via_ir" is verified from_`foundry.toml`_.)_ ::: :::note **Target Cancun.** `SwapVM` guards reentrancy with transient storage (`TransientLock`, i.e. `TSTORE`/`TLOAD`), which requires an EVM target of `cancun` or newer. The template makes this explicit with `evmVersion: "cancun"`; `foundry.toml` relies on the solc 0.8.30 default. If you set `evm_version` yourself, do not pick a pre-Cancun target -- the router will not deploy/run. ::: ## EIP-170 and curating the native set A deployed contract may not exceed the EIP-170 bytecode limit (24,576 bytes). Every instruction the router inlines adds to that budget, so the two shipped opcode sets are deliberately different sizes: - `Opcodes` (standard `SwapVMRouter`) registers the full set -- control flow, balances, invalidators, XYC/concentrate/decay, limit, min-rate, Dutch auction, TWAP, fees, pegged, whitelist, extruction -- opcodes `10-51`. - `AquaOpcodes` (deployed `AquaSwapVMRouter`) registers only a curated **subset** -- Controls, XYCSwap, XYCConcentrate, Decay, Fee (amount-_in_ variants), PeggedSwap, Extruction -- opcodes `10-33`. :::note **Out-of-subset opcodes are not universally available.** The deployed Aqua router runs only the `aquaInstructions` subset. An instruction that exists in `Opcodes` but not in `AquaOpcodes` cannot be placed in an Aqua program: `ProgramBuilder.findOpcode` reverts `OpcodeNotFound` at **build time** (the function pointer is absent from that router's `_opcodes()`). If instead you hand-encode a raw opcode byte the router does not implement, there is **no named runtime error**: a reserved gap index resolves to the `_notInstruction` no-op, and an index past the registered `_opcodes()` table reverts with a Solidity array-out-of-bounds `Panic(0x32)`. When you add a custom instruction, decide which router(s) should carry it and register it there -- adding to every router may push a contract over EIP-170. ::: ## TakerTraits & the taker path The taker controls execution through `TakerTraits` (`src/libs/TakerTraits.sol`), a packed `uint176` header (18-byte slice-index table + 2-byte flags) followed by variable-length slices. Build it with `TakerTraitsLib.build(Args)`. The fields that matter to integrators: | Field | Purpose | | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | `isExactIn` | Exact-input vs exact-output pricing | | `isAToB` | Swap direction across the maker's sorted `tokenA/tokenB` | | `threshold` (32 bytes or empty) | Slippage bound: **minReturn** (min `amountOut`) when exact-in, **maxIn** (max `amountIn`) when exact-out | | `isStrictThresholdAmount` | Require the threshold to match exactly instead of as a min/max bound | | `to` | Receiver of `tokenOut`; defaults to the taker when zero/omitted | | `deadline` (uint40) | Expiry timestamp; `0` = no deadline | | `shouldUnwrapWeth` | Deliver ETH instead of WETH to the receiver | | `isFirstTransferFromTaker` | Ordering of the two transfers (taker->maker first, or maker->taker first) | | `useTransferFromAndAquaPush` | Pull the input with `transferFrom` then `AQUA.push` (vs. relying on a callback) | | `hasPreTransferInCallback` / `hasPreTransferOutCallback` | Enable `ITakerCallbacks` hooks around the transfers | | `instructionsArgs` | Per-swap bytes consumed by instructions via `ctx.takerArgs()` | | `signature` | The maker's order signature (non-Aqua orders only) | The threshold and deadline are enforced in `TakerTraitsLib.validate` after the program runs: `amountOut > 0` is required, the deadline is checked against `block.timestamp`, and the min/max (or strict) threshold is applied to the computed amount. :::info There is **no**`permit`**field** in `TakerTraits.Args`. Input-side funding is expressed through `useTransferFromAndAquaPush` and the pre-transfer callbacks, not an EIP-2612 permit blob. Do not expect one. ::: ### The EIP-1271 (smart-contract signature) path For signature-based (non-Aqua) orders, `SwapVM.swap` verifies the maker with: ```solidity bytes calldata signature = takerTraits.signature(takerData); require(order.maker.recoverOrIsValidSignature(orderHash, signature), BadSignature(order.maker, orderHash, signature)); ``` `recoverOrIsValidSignature` (from `@1inch/solidity-utils` `ECDSA`) first attempts EOA `ecrecover` and, failing that, falls back to the **EIP-1271** `isValidSignature(bytes32,bytes)` call on `order.maker`. So a smart-contract wallet or a programmatic maker can sign orders -- the signature simply travels in the `signature` slice of the taker data. Aqua orders (`useAquaInsteadOfSignature`) skip this entirely: authority comes from having shipped liquidity into Aqua under that `orderHash`, and the order is hashed with a plain `keccak256(abi.encode(order))` rather than the EIP-712 typed hash. ## Known tooling caveats & version drift :::note **These are verified footguns. Read before you wire up tooling.** **1. Constructor arity differs across builds -- pin your version.** In the repo (HEAD), `AquaSwapVMRouter`'s constructor takes **5** arguments: `(address aqua, address weth, address owner, string name, string version)` -- verified in `src/routers/AquaSwapVMRouter.sol`. Integrators report the published `npm @1inch/swap-vm` build exposing a **3**-argument constructor with different import paths and different `TakerTraits.Args` fields (_reported, unverified against the npm artifact_). The template itself pins a GitHub tag (`@1inch/swap-vm#0.0.4`) while HEAD is `0.0.6`, so the drift is real. Pin an exact commit/tag and treat the Solidity source you compile against -- not a floating npm range -- as canonical. **2. The**`SwapVMHelpers.ts`**opcode enum is stale -- trust the on-chain table.** The TypeScript `AquaOpcodes` enum in `test/utils/SwapVMHelpers.ts` lists _two_ concentrate entries (`XYC_CONCENTRATE_GROW_LIQUIDITY_XD = 0x12` and `..._2D = 0x13`), but the on-chain `AquaOpcodes._opcodes()` registers only `_xycConcentrateGrowLiquidity2D` at `0x12`, with `Decay` at `0x13`. Everything after the phantom entry is shifted by one, so numbers emitted from the TS enum address the wrong instruction. Derive opcodes from the deployed `_opcodes()` set (via `ProgramBuilder` function-pointer resolution), never from the hand-maintained TS enum. **3.**`TakerTraits.Args`**field drift.** The Solidity `TakerTraits.Args` struct includes `isAToB` and `deadline`; the TS `TakerTraitsArgs` interface in `SwapVMHelpers.ts` omits both (and its `MakerTraitsArgs` carries an `expiration` field with no Solidity counterpart). Encoding taker data from the TS helper can silently drop direction/deadline. Verified against source. **4. The template's test suite skips real swaps.** `test/AquaAMM.test.ts` marks the deadline case with `it.only(...)` (line 708). On a fresh clone, `it.only` makes Mocha run _only_ that test and silently skip the two actual swap tests ("execute swap with resolver contract" and "...with EOA as taker"). Remove `it.only` before trusting a green run. **5.**`AquaAMM.buildProgram`**can revert**`OpcodeNotFound`**.** The template's `AquaAMM.buildProgram` conditionally emits `_aquaProtocolFeeAmountOutXD` when `protocolFeeBpsIn > 0`. That instruction is **not** in `AquaOpcodes._opcodes()` on current `swap-vm` (the Aqua set carries only the amount-_in_ fee variants; the amount-_out_ Aqua fee lives in `FeeExperimental`, which the Aqua router does not inherit). So `ProgramBuilder.findOpcode` reverts `OpcodeNotFound` when a non-zero protocol fee is requested. Every shipped test passes `protocolFeeBpsIn = 0`, so the path is never exercised and the bug stays hidden until someone charges a protocol fee. Verified. **6. Use the canonical router, not the superseded Base build.** The canonical router is `AquaSwapVMRouter` **v1.0.2** at `0x111111338c5091e8440b67b168bae16a668ac0de`, deployed across the 13 Aqua chains, with the Aqua registry at `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a`. An older superseded build at `0x8fdd04dbf6111437b44bbca99c28882434e0958f` (a 28-opcode build seen on Base) is stale -- integrators hit an index-32 out-of-bounds against it. Point resolvers at `0x111111338c...c0de` and ignore `0x8fdd...958f`. ::: https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/aqua-router-opcodes/overview.md The instruction (opcode) families registered on the deployed Aqua router, **AquaSwapVMRouter v1.0.1**. Each child page documents one opcode family -- its purpose, encoding, and on-chain behaviour. For the full directory with opcode numbers see [Opcode Gallery](../opcode-gallery). https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/aqua-router-opcodes/balances.md The `Balances` instruction loads virtual reserves into the swap context, giving the downstream swap-formula instruction the `balanceIn` and `balanceOut` values it prices against. Every SwapVM program must run a Balances instruction before any swap-formula instruction. These reserves are virtual accounting values used by the pricing math. They are not a token deposit: the maker's tokens stay in the maker's own wallet under a revocable allowance and move only when a taker fills a swap. :::info **Engine:** Balances belongs to the SwapVM instruction set (SwapVM is the Aqua swap engine) and executes on the Aqua router (`AquaSwapVMRouter`, the deployed SwapVM router). The static variant embeds reserves in the program args; the dynamic variant persists them in SwapVM storage. ::: **Source:** `src/instructions/Balances.sol` --- ## Static vs dynamic Both variants share one packed args layout but differ in where the reserves live and how they evolve. | Variant | Reserves live in | Typical use | | -------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | | `_staticBalancesXD` | Embedded in the program args (read-only) | Limit order or auction priced from fixed embedded values rather than evolving reserves | | `_dynamicBalancesXD` | SwapVM contract storage, keyed by `(orderHash, token)` | Stateful AMM (XYC constant product, concentrated liquidity) where reserves change with each fill | --- ## Args encoding Both instructions share the same packed args layout: | Field | Size | Description | | ------------------- | ------------------------ | ------------------------------------------- | | `tokensCount` | 2 bytes (`uint16`) | Number of tokens in the list | | `tokens[]` | `20 x tokensCount` bytes | Token addresses | | `initialBalances[]` | `32 x tokensCount` bytes | Initial balance for each token (same order) | Total: `2 + 20N + 32N` bytes. Build with `BalancesArgsBuilder.build(address[] tokens, uint256[] balances)`. --- ## Instructions ### `_staticBalancesXD` ```solidity function _staticBalancesXD(Context memory ctx, bytes calldata args) internal pure ``` Sets `ctx.swap.balanceIn` and `ctx.swap.balanceOut` from the embedded `initialBalances` array. No state is read or written; balances are fixed for the lifetime of this program execution. Use when liquidity reserves do not change between swaps, for example a limit order or auction priced at a fixed rate. **Errors** | Error | Condition | | ---------------------------------------------------------------------- | --------------------------------------------------- | | `SetBalancesExpectZeroBalances(balanceIn, balanceOut)` | balances already set before this instruction | | `StaticBalancesRequiresSettingBothBalances(tokenIn, tokenOut, tokens)` | `tokenIn` or `tokenOut` not found in the token list | --- ### `_dynamicBalancesXD` ```solidity function _dynamicBalancesXD(Context memory ctx, bytes calldata args) internal ``` Loads or initializes balances from SwapVM contract storage keyed by `(orderHash, token)`. After running the nested sub-instructions (via `ctx.runLoop()`), it writes the resulting `swapAmountIn` and `swapAmountOut` back to storage: ``` balances[orderHash][tokenIn] += swapAmountIn balances[orderHash][tokenOut] -= swapAmountOut ``` Use for stateful AMM strategies (XYC constant product, concentrated liquidity) where reserves evolve with each swap. **Quote/swap divergence:** In quote mode (`isStaticContext = true`) balances are read but **not updated** after the nested loop. A quote may succeed while the swap reverts if another transaction fills the order first. Do not use backward jumps to this instruction. **Errors** | Error | Condition | | ------------------------------------------------------------------------------ | --------------------------------------------- | | `DynamicBalancesLoadingRequiresSettingBothBalances(tokenIn, tokenOut, tokens)` | `tokenIn` or `tokenOut` not found during load | | `DynamicBalancesInitRequiresSettingBothBalances(tokenIn, tokenOut, tokens)` | `tokenIn` or `tokenOut` not found during init | **Public state** ```solidity mapping(bytes32 orderHash => mapping(address token => uint256)) public balances; ``` --- ## Related - XYCSwap -- constant product swap, uses these reserves - XYCConcentrate -- concentrated liquidity swap - LimitSwap -- limit order price, uses these reserves - PeggedSwap -- pegged-asset swap, uses these reserves https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/aqua-router-opcodes/controls.md **Controls** is the control-flow and access-guard module of the SwapVM instruction set. Unlike the pricing modules, these instructions never compute a swap amount. They redirect execution or revert based on on-chain state checks. Controls ships as part of `AquaOpcodes` on the deployed Aqua router (`AquaSwapVMRouter`), and SwapVM, the Aqua swap engine, runs it inline while a taker fills a strategy. **Source:** `src/instructions/Controls.sol` --- ## Control flow ### `_salt` ```solidity function _salt(Context memory /* ctx */, bytes calldata /* args */) internal pure ``` No-op. Accepts any bytes as args. Used to make two otherwise identical programs produce different order hashes. --- ### `_jump` ```solidity function _jump(Context memory ctx, bytes calldata args) internal pure ``` Unconditional jump to a program counter position. | Field | Offset | Size | Description | | -------- | ------ | ------------------ | ---------------------------------- | | `nextPC` | 0 | 2 bytes (`uint16`) | Target instruction index (0-65535) | Build with `ControlsArgsBuilder.buildJump(uint16 nextPC)`. **Errors:** `JumpMissingNextPCArg()` if args too short. --- ### `_jumpIfTokenIn` ```solidity function _jumpIfTokenIn(Context memory ctx, bytes calldata args) internal pure ``` Jumps if `ctx.query.tokenIn` equals the specified token address. Otherwise falls through. | Field | Offset | Size | Description | | -------- | ------ | -------------------- | -------------------------------- | | `token` | 0 | 20 bytes (`address`) | Token to match against `tokenIn` | | `nextPC` | 20 | 2 bytes (`uint16`) | Jump target | Build with `ControlsArgsBuilder.buildJumpIfToken(address token, uint16 nextPC)`. --- ### `_jumpIfTokenOut` ```solidity function _jumpIfTokenOut(Context memory ctx, bytes calldata args) internal pure ``` Jumps if `ctx.query.tokenOut` equals the specified token address. Otherwise falls through. Same args layout as `_jumpIfTokenIn`. --- ## Access and validity guards These instructions revert a swap based on transaction validity or on what the taker holds. They constrain **takers** only; supplying liquidity by shipping a strategy stays permissionless. Each guard is a per-strategy instruction evaluated at swap time, not a protocol-wide switch. ### `_deadline` ```solidity function _deadline(Context memory ctx, bytes calldata args) internal view ``` Reverts if `block.timestamp > deadline`. | Field | Offset | Size | Description | | ---------- | ------ | ------------------ | ------------------------------- | | `deadline` | 0 | 5 bytes (`uint40`) | Expiry timestamp (Unix seconds) | Build with `ControlsArgsBuilder.buildDeadline(uint40 deadline)`. **Errors:** `DeadlineReached(taker, deadline)`. --- ### `_onlyTakerTokenBalanceNonZero` ```solidity function _onlyTakerTokenBalanceNonZero(Context memory ctx, bytes calldata args) internal view ``` Reverts if `IERC20(token).balanceOf(taker) == 0`. Natively supports NFTs (ERC-721 balance 1 check). | Field | Offset | Size | Description | | ------- | ------ | -------------------- | -------------------------------- | | `token` | 0 | 20 bytes (`address`) | ERC-20 or ERC-721 token to check | Build with `ControlsArgsBuilder.buildTakerTokenBalanceNonZero(address token)`. **Errors:** `TakerTokenBalanceIsZero(taker, token)`. --- ### `_onlyTakerTokenBalanceGte` ```solidity function _onlyTakerTokenBalanceGte(Context memory ctx, bytes calldata args) internal view ``` Reverts if `IERC20(token).balanceOf(taker) < minAmount`. | Field | Offset | Size | Description | | ----------- | ------ | -------------------- | ------------------------ | | `token` | 0 | 20 bytes (`address`) | Token to check | | `minAmount` | 20 | 32 bytes (`uint256`) | Minimum required balance | Build with `ControlsArgsBuilder.buildTakerTokenBalanceGte(address token, uint256 minAmount)`. **Errors:** `TakerTokenBalanceIsLessThanRequired(taker, token, balance, minAmount)`. --- ### `_onlyTakerTokenSupplyShareGte` ```solidity function _onlyTakerTokenSupplyShareGte(Context memory ctx, bytes calldata args) internal view ``` Reverts if taker holds less than `minShareE18` of the total token supply. Check: `balance x 1e18 >= minShareE18 x totalSupply` | Field | Offset | Size | Description | | ------------- | ------ | -------------------- | ----------------------------------------------- | | `token` | 0 | 20 bytes (`address`) | Token to check | | `minShareE18` | 20 | 8 bytes (`uint64`) | Minimum share in 1e18 scale (e.g., `1e16` = 1%) | Build with `ControlsArgsBuilder.buildTakerTokenSupplyShareGte(address token, uint64 minShareE18)`. **Errors:** `TakerTokenBalanceSupplyShareIsLessThanRequired(taker, token, balance, totalSupply, minShareE18)`. --- ### `_onlyTxOriginTokenBalanceNonZero` :::info **Deployed taker access gate.** This is the guard the Aqua dApp actually attaches to every strategy at launch. Its default-deny behavior is what gates takers; makers stay permissionless, and the gate is evaluated at swap time. ::: ```solidity function _onlyTxOriginTokenBalanceNonZero(Context memory ctx, bytes calldata args) internal view ``` Reverts unless `IERC20(token).balanceOf(tx.origin) > 0`. Like `_onlyTakerTokenBalanceNonZero`, it works with ERC-721 credentials (balance 1). The difference is the account it checks: this guard reads `tx.origin`, the externally owned account that started the transaction, rather than the resolved taker. | Field | Offset | Size | Description | | ------- | ------ | -------------------- | ------------------------------------------------------------------- | | `token` | 0 | 20 bytes (`address`) | Access-credential token to check (the per-chain `KycNFT` at launch) | The Aqua frontend assembler wires this guard onto every strategy through the SwapVM SDK helper `withTxOriginAccessToken(aquaKycToken)` (from `@1inch/swap-vm-sdk`), pointing at the per-chain `KycNFT` access credential. **Errors:** `TxOriginTokenBalanceIsZero`. :::info Because the check is on `tx.origin`, only a swap sent directly from an EOA that holds the credential passes. Smart-contract wallets, multisigs, and ERC-4337 bundlers cannot satisfy it. ::: --- ## Related - Extruction -- delegate pricing or control flow to an external contract via `IExtruction` - Invalidators -- nonce and remaining-amount order invalidation. Part of the limit-order protocol opcode set on a separate router, not the Aqua router. https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/aqua-router-opcodes/fee.md Fee instructions are applied to `amountIn`. All are wrapping instructions: they call `ctx.runLoop()` internally to execute the nested swap formula, then adjust amounts around the result. **Source:** `src/instructions/Fee.sol` (part of the SwapVM instruction set on the Aqua router). Fee scale: `BPS = 1e9` (`1e9` = 100%). A fee of `3000000` (`3e6`) = 0.3%. In the app, the swap fee is displayed to two decimals (for example, 0.30%). :::info **Fees in v1:** only the liquidity-provider fee (shown in the app as the "Swap fee") is non-zero. The protocol fee instructions on this page (`_protocolFeeAmountInXD`, `_aquaProtocolFeeAmountInXD`, `_dynamicProtocolFeeAmountInXD`, and `_aquaDynamicProtocolFeeAmountInXD`) exist in the contracts but are configured to `0` and are not surfaced in the UI. There is no on-chain fee event: fees are encoded as instructions in the strategy program and are reflected directly in the swap amounts. The LP fee auto-compounds into the maker's Aqua balance and is realized through the `Pushed` event. Tokens stay in the maker's wallet (or in the maker's Aqua virtual balance, an internal counter in `Aqua.sol`) until a taker fills; the protocol custodies none. ::: --- ## Constraint shared by all instructions Must be placed **before** swap amounts are computed. That is, both `amountIn` and `amountOut` must be zero when this instruction executes. Violating this reverts with `FeeShouldBeAppliedBeforeSwapAmountsComputation`. **Quote/swap divergence:** All instructions compute fee amounts in quote mode but skip the actual token transfer or Aqua pull. A quote may succeed while the swap reverts due to insufficient maker balance or missing ERC-20 approval. Do not use backward jumps to any of these instructions. --- ## Instructions ### `_flatFeeAmountInXD` ```solidity function _flatFeeAmountInXD(Context memory ctx, bytes calldata args) internal ``` Applies a flat proportional fee on `amountIn`. There is no token transfer; it adjusts amounts only. This is the instruction behind the non-zero LP swap fee. | Field | Offset | Size | Description | | -------- | ------ | ------------------ | ------------------- | | `feeBps` | 0 | 4 bytes (`uint32`) | Fee rate; max `1e9` | | Mode | Behavior | | -------- | -------------------------------------------------------------------------------------------------------------------- | | exactIn | Reduces `amountIn` by `amountIn x feeBps / BPS` before passing to swap formula; restores original `amountIn` after | | exactOut | Passes full `amountIn` to swap formula; adds `amountIn x feeBps / (BPS feeBps)` after | --- ### `_protocolFeeAmountInXD` ```solidity function _protocolFeeAmountInXD(Context memory ctx, bytes calldata args) internal ``` Flat fee on `amountIn` with a live ERC-20 transfer from maker to a recipient. | Field | Offset | Size | Description | | -------- | ------ | -------------------- | ------------------- | | `feeBps` | 0 | 4 bytes (`uint32`) | Fee rate; max `1e9` | | `to` | 4 | 20 bytes (`address`) | Fee recipient | The transfer (`safeTransferFrom(maker, to, feeAmount)`) occurs **during** program execution, before SwapVM's final taker-to-maker `tokenIn` transfer. The maker must hold sufficient `tokenIn` balance and have granted ERC-20 approval before swap execution. --- ### `_aquaProtocolFeeAmountInXD` ```solidity function _aquaProtocolFeeAmountInXD(Context memory ctx, bytes calldata args) internal ``` Same as `_protocolFeeAmountInXD` but pulls fee from the maker's Aqua virtual balance via `IAqua.pull(maker, orderHash, tokenIn, feeAmount, to)` instead of a direct ERC-20 transfer. Increments `ctx.swap.amountNetPulled` by the fee. | Field | Offset | Size | Description | | -------- | ------ | -------------------- | ------------------- | | `feeBps` | 0 | 4 bytes (`uint32`) | Fee rate; max `1e9` | | `to` | 4 | 20 bytes (`address`) | Fee recipient | --- ### `_dynamicProtocolFeeAmountInXD` ```solidity function _dynamicProtocolFeeAmountInXD(Context memory ctx, bytes calldata args) internal ``` Fee rate and recipient are determined at execution time by calling `IProtocolFeeProvider.getFeeBpsAndRecipient(...)` on an external contract via `staticcall`. Transfers fee via `safeTransferFrom`. | Field | Offset | Size | Description | | ------------- | ------ | -------------------- | ----------------------------------------------------------------- | | `feeProvider` | 0 | 20 bytes (`address`) | Must implement `IProtocolFeeProvider`; `address(0)` skips the fee | **Security:** Uses `staticcall`, so the fee provider cannot mutate state. Protected by `TransientLock` at the orderHash level. A malicious provider can return large data causing high gas consumption; takers should verify provider trustworthiness. **Errors:** `FeeDynamicProtocolInvalidRecipient()` if `feeBps != 0` but `to == address(0)`. `FeeProtocolProviderFailedCall()` if staticcall fails or returns unexpected data. `FeeBpsOutOfRange(feeBps)` if returned fee exceeds `BPS`. --- ### `_aquaDynamicProtocolFeeAmountInXD` ```solidity function _aquaDynamicProtocolFeeAmountInXD(Context memory ctx, bytes calldata args) internal ``` Same as `_dynamicProtocolFeeAmountInXD` but uses `IAqua.pull` for the transfer instead of `safeTransferFrom`. | Field | Offset | Size | Description | | ------------- | ------ | -------------------- | ------------------------------------- | | `feeProvider` | 0 | 20 bytes (`address`) | Must implement `IProtocolFeeProvider` | --- ## Related - FeeExperimental: experimental fee variants applied to `amountOut`. Not part of the shipped v1 opcode set. - XYCSwap: a typical swap formula nested inside fee instructions. - LimitSwap: another common nested swap formula. https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/aqua-router-opcodes/fee-experimental.md SwapVM, the Aqua swap engine, evaluates fee instructions as part of a strategy program. This page documents an **experimental** fee family that applies fees to `amountOut`, together with volume-sensitive (progressive) fee formulas. These instructions extend the base `Fee` instruction set. :::info **Experimental: not part of the shipped v1 fee set.** The instructions on this page live in `FeeExperimental.sol` and are not registered on the deployed `AquaSwapVMRouter`. The shipped Aqua opcode set is `Controls`, `XYCSwap`, `XYCConcentrate`, `Decay`, `Fee`, `PeggedSwap`, and `Extruction`; the production `Fee` opcode applies fees to `amountIn` only. The `amountOut` and progressive (volume-based) variants below are not available in Aqua v1, and there is no "LP Progressive Fees" product feature. In v1 only the LP swap fee is non-zero; protocol fees are 0 and are not shown. Treat this reference as a design record for review, not a description of on-chain behavior. ::: **Source:** `src/instructions/FeeExperimental.sol` Fee scale: `BPS = 1e9` (1e9 = 100%). `feeBps` is expressed on this scale, so `feeBps = 1e9` is 100%. --- ## Constraint shared by all instructions Must be placed before swap amounts are computed (both `amountIn` and `amountOut` must be zero). Violating this reverts with `FeeShouldBeAppliedBeforeSwapAmountsComputation`. **Quote and swap divergence:** in quote mode, token transfers and Aqua pulls are skipped. Quotes may succeed while swaps revert due to insufficient balance or missing approval. Do not use backward jumps to any of these instructions. --- ## Instructions ### `_flatFeeAmountOutXD` ```solidity function _flatFeeAmountOutXD(Context memory ctx, bytes calldata args) internal ``` Flat proportional fee deducted from `amountOut`. No token transfer. | Field | Offset | Size | Description | | -------- | ------ | ------------------ | ------------------- | | `feeBps` | 0 | 4 bytes (`uint32`) | Fee rate; max `1e9` | | Mode | Behavior | | -------- | ---------------------------------------------------------------------------------------------- | | exactIn | Runs swap formula, then deducts `amountOut x feeBps / BPS` from result | | exactOut | Inflates `amountOut` by fee before passing to swap formula; restores taker-defined value after | --- ### `_progressiveFeeInXD` ```solidity function _progressiveFeeInXD(Context memory ctx, bytes calldata args) internal ``` Volume-sensitive fee on `amountIn`. The effective fee increases as `amountIn` grows relative to `balanceIn`. Large swaps pay proportionally more. | Field | Offset | Size | Description | | -------- | ------ | ------------------ | ------------------------------ | | `feeBps` | 0 | 4 bytes (`uint32`) | Base fee rate (``); max `1e9` | **exactIn formula:** `amountIn_eff = (BPS x amountIn x balanceIn) / (BPS x balanceIn + feeBps x amountIn)` **exactOut formula (inverse):** `amountIn = (BPS x amountIn_eff x balanceIn) / (BPS x balanceIn feeBps x amountIn_eff)` --- ### `_progressiveFeeOutXD` ```solidity function _progressiveFeeOutXD(Context memory ctx, bytes calldata args) internal ``` Volume-sensitive fee on `amountOut`. The effective fee increases as `amountOut` grows relative to `balanceOut`. | Field | Offset | Size | Description | | -------- | ------ | ------------------ | ------------------------------ | | `feeBps` | 0 | 4 bytes (`uint32`) | Base fee rate (``); max `1e9` | **exactIn formula:** `amountOut_eff = (BPS x amountOut x balanceOut) / (BPS x balanceOut + feeBps x amountOut)` **exactOut formula (inverse):** `amountOut = (BPS x amountOut_eff x balanceOut) / (BPS x balanceOut feeBps x amountOut_eff)` --- ### `_protocolFeeAmountOutXD` ```solidity function _protocolFeeAmountOutXD(Context memory ctx, bytes calldata args) internal ``` Flat fee on `amountOut` with a live ERC-20 transfer from maker to a recipient. Protocol fees are configured to 0 in Aqua v1, and this `amountOut` variant is not registered on the deployed router. It is documented here for design review only. | Field | Offset | Size | Description | | -------- | ------ | -------------------- | ------------------- | | `feeBps` | 0 | 4 bytes (`uint32`) | Fee rate; max `1e9` | | `to` | 4 | 20 bytes (`address`) | Fee recipient | Runs the swap formula, deducts fee from `amountOut`, then calls `safeTransferFrom(maker, to, feeAmount)` on `tokenOut`. --- ### `_aquaProtocolFeeAmountOutXD` ```solidity function _aquaProtocolFeeAmountOutXD(Context memory ctx, bytes calldata args) internal ``` Same as `_protocolFeeAmountOutXD` but uses `IAqua.pull(maker, orderHash, tokenOut, feeAmount, to)` instead of a direct ERC-20 transfer. | Field | Offset | Size | Description | | -------- | ------ | -------------------- | ------------------- | | `feeBps` | 0 | 4 bytes (`uint32`) | Fee rate; max `1e9` | | `to` | 4 | 20 bytes (`address`) | Fee recipient | --- ## Related - Fee: the shipped fee instructions, applied to `amountIn` - XYCSwap: typical swap formula nested inside fee instructions - LimitSwap: another common nested swap formula https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/aqua-router-opcodes/xyc-swap.md XYCSwap is the constant-product AMM instruction (`x * y = k`) in the SwapVM instruction set, the swap engine behind 1inch Aqua. It is a terminal instruction that computes `amountIn` or `amountOut` directly from the current virtual reserves. :::info XYCSwap ships in the deployed Aqua opcode set (`AquaOpcodes`) on `AquaSwapVMRouter`, alongside `Controls`, `XYCConcentrate`, `Decay`, `Fee`, `PeggedSwap`, and `Extruction`. It backs the `xyc` (constant-product) strategy type. ::: **Source:** `src/instructions/XYCSwap.sol` --- ## Instructions ### `_xycSwapXD` ```solidity function _xycSwapXD(Context memory ctx, bytes calldata /* args */) internal pure ``` **Args:** none (0 bytes) **Behavior** | Mode | Computation | | -------- | ---------------------------------------------------------------------- | | exactIn | `amountOut = amountIn * balanceOut / (balanceIn + amountIn)` (floor) | | exactOut | `amountIn = amountOut * balanceIn / (balanceOut amountOut)` (ceil) | Reverts if either virtual balance is zero. Also reverts if `amountOut` (exactIn) or `amountIn` (exactOut) is already set; recompute detection prevents inconsistent state. **Errors** | Error | Condition | | ----------------------------------------------------------- | -------------------------------------------------------- | | `XYCSwapRequiresBothBalancesNonZero(balanceIn, balanceOut)` | `balanceIn == 0 \|\| balanceOut == 0` | | `XYCSwapRecomputeDetected()` | `amountOut != 0` (exactIn) or `amountIn != 0` (exactOut) | **Constraints** - Must be preceded by an instruction that sets `ctx.swap.balanceIn` and `ctx.swap.balanceOut` (e.g., `_staticBalancesXD`, `_dynamicBalancesXD`) - Suffix `XD`: runs identically in quote (static) and swap (dynamic) contexts --- ## Related - XYCConcentrate: concentrated liquidity variant with price bounds - Balances: sets virtual reserves before this instruction - Fee: apply before this instruction to reduce the effective amountIn https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/aqua-router-opcodes/xyc-concentrate.md The `XYCConcentrate` instruction implements a concentrated liquidity AMM swap in SwapVM, the Aqua swap engine. It computes virtual reserves from real balances and a price range (`sqrtPriceMin`, `sqrtPriceMax`), then performs a constant-product swap within those virtual reserves. Bounding the price range concentrates a maker's real balances so they provide deeper liquidity inside that range than an unbounded constant-product curve would. `XYCConcentrate` is part of the `AquaOpcodes` instruction set on the `AquaSwapVMRouter` (the deployed SwapVM router), alongside `Controls`, `XYCSwap`, `Decay`, `Fee`, `PeggedSwap`, and `Extruction`. **Source:** `src/instructions/XYCConcentrate.sol` --- ## Args encoding | Field | Offset | Size | Description | | -------------- | ------ | -------------------- | ---------------------------------------------------------------- | | `sqrtPriceMin` | 0 | 32 bytes (`uint256`) | `sqrt(P_min)` in 1e18 fixed-point, where `P = tokenGt / tokenLt` | | `sqrtPriceMax` | 32 | 32 bytes (`uint256`) | `sqrt(P_max)` in 1e18 fixed-point | Total: 64 bytes. Build with `XYCConcentrateArgsBuilder.build2D(uint256 sqrtPriceMin, uint256 sqrtPriceMax)`. Requires `0 < sqrtPriceMin < sqrtPriceMax`. **Token ordering:** `tokenLt` is the token with the lower address; `tokenGt` is the token with the higher address. Price `P` is always expressed as `tokenGt / tokenLt`. --- ## Instructions ### `_xycConcentrateGrowLiquidity2D` ```solidity function _xycConcentrateGrowLiquidity2D(Context memory ctx, bytes calldata args) internal pure ``` **Behavior** 1. Parses `sqrtPriceMin` and `sqrtPriceMax` 2. Determines which of `balanceIn`/`balanceOut` corresponds to `tokenLt` vs `tokenGt` 3. Computes liquidity `L` from real balances and price bounds: = bLt x sqrtPriceMin / 1e18 + bGt x 1e18 / sqrtPriceMax disc = + 4 x (sqrtPriceMax sqrtPriceMin) x bLt x bGt / sqrtPriceMax L = ( + disc) x sqrtPriceMax / (2 x (sqrtPriceMax sqrtPriceMin)) 4. Computes virtual reserves by adding virtual offsets to real balances: - If `tokenIn` is `tokenLt`: - `virtualBalanceIn = balanceIn + L / sqrtPriceMax` - `virtualBalanceOut = balanceOut + L x sqrtPriceMin / 1e18` - If `tokenIn` is `tokenGt`: - `virtualBalanceIn = balanceIn + L x sqrtPriceMin / 1e18` - `virtualBalanceOut = balanceOut + L / sqrtPriceMax` 5. Applies constant-product swap formula on virtual reserves: - exactIn: `amountOut = amountIn x virtualBalanceOut / (virtualBalanceIn + amountIn)` (floor) - exactOut: `amountIn = amountOut x virtualBalanceIn / (virtualBalanceOut amountOut)` (ceil) `ctx.swap.balanceIn` and `ctx.swap.balanceOut` are **not mutated**; only `amountIn` and `amountOut` are written. Fee reinvestment is automatic: as real balances grow from each swap, `L` increases on the next swap. **Errors** | Error | Condition | | --------------------------------------------------- | -------------------------------------------------------- | | `ConcentrateRecomputeDetected(amountIn, amountOut)` | `amountOut != 0` (exactIn) or `amountIn != 0` (exactOut) | **Constraints** - Requires `balanceIn` and `balanceOut` set by a preceding `_dynamicBalancesXD` (so real reserves update each swap) - Suffix `2D`: operates on both tokens; pure (no state read/write in the instruction itself) --- ## Related - XYCSwap: unbounded constant-product AMM - Balances: provides `balanceIn`/`balanceOut` from storage - Fee: apply fee before this instruction to reduce the effective `amountIn` https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/aqua-router-opcodes/pegged-swap.md PeggedSwap is a square-root linear swap curve optimized for pegged assets (stablecoins, wrapped tokens, and LSTs with fixed ratios). It provides minimal slippage near the peg and uses an analytical solution, so no iterative solver is required. PeggedSwap is one of the instructions in the Aqua opcode set (`AquaOpcodes`), executed by SwapVM, the Aqua swap engine, on the `AquaSwapVMRouter`. The full deployed set is `Controls`, `XYCSwap`, `XYCConcentrate`, `Decay`, `Fee`, `PeggedSwap`, and `Extruction`. **Source:** `src/instructions/PeggedSwap.sol` **Curve formula:** `(x/X) + (y/Y) + A(x/X + y/Y) = 1 + A` (curvature `p = 0.5`, hardcoded) --- ## Args encoding All fields are 32-byte `uint256` values, packed via `abi.encodePacked`: | Field | Offset | Size | Description | | ------------- | ------ | -------- | --------------------------------------------------------------------------------------- | | `x0` | 0 | 32 bytes | Initial reserve normalization factor for `tokenLt` (token with lower address) | | `y0` | 32 | 32 bytes | Initial reserve normalization factor for `tokenGt` (token with higher address) | | `linearWidth` | 64 | 32 bytes | Linear component coefficient `A` in 1e27 scale; max `5000 x 1e27`; higher = tighter peg | | `rateLt` | 96 | 32 bytes | Decimal scale multiplier for `tokenLt` | | `rateGt` | 128 | 32 bytes | Decimal scale multiplier for `tokenGt` | Total: 160 bytes. Build with `PeggedSwapArgsBuilder.build(PeggedSwapArgsBuilder.Args memory args)`. **Rate multipliers** normalize tokens to a common precision. When token addresses determine `lt`/`gt`, assign rates so that `balance x rate` is in the same scale for both tokens. Example: USDC (6 decimals, lower address) and DAI (18 decimals, higher address): ``` rateLt = 1e12 // scales 1000e6 USDC -> 1000e18 rateGt = 1 // 1000e18 DAI stays as-is x0 = 1000e18 // normalized initial USDC reserve y0 = 1000e18 // normalized initial DAI reserve ``` **When to use:** Suitable for pairs that maintain a fixed or near-fixed ratio (USDC/USDT, WETH/stETH with hardcoded rate). **Not suitable** for pairs where the peg ratio drifts over time (e.g. WETH/wstETH with an ever-increasing exchange rate), because the curve has finite reserves and a hard price boundary. --- ## Instructions ### `_peggedSwapGrowPriceRange2D` ```solidity function _peggedSwapGrowPriceRange2D(Context memory ctx, bytes calldata args) internal pure ``` **Behavior** 1. Parses args and determines `(rateIn, rateOut, x0_init, y0_init)` based on `tokenIn < tokenOut` 2. Normalizes real balances: `x0 = balanceIn x rateIn`, `y0 = balanceOut x rateOut` 3. Computes the curve invariant from `(x0, y0, x0_init, y0_init, linearWidth)` 4. exactIn: finds `y1` (new normalized output reserve) that satisfies the invariant given `x1 = x0 + amountIn x rateIn`, then converts back: `amountOut = (y0 y1) / rateOut` 5. exactOut: finds `x1` from `y1 = y0 amountOut x rateOut`, then: `amountIn = (x1 x0) / rateIn` All rounding protects the maker: `amountOut` rounds down, `amountIn` rounds up. **Errors** | Error | Condition | | ------------------------------------------- | -------------------------------------------------------- | | `PeggedSwapRecomputeDetected()` | `amountOut != 0` (exactIn) or `amountIn != 0` (exactOut) | | `PeggedSwapBothBalancesZero()` | Both `balanceIn` and `balanceOut` are zero | | `PeggedSwapInvalidArgsLength(length)` | Args shorter than 160 bytes | | `PeggedSwapInvalidLinearWidth(linearWidth)` | `linearWidth > 5000e27` (MAX_LINEAR_WIDTH = 5000 x 1e27) | | `PeggedSwapInvalidInitialBalances(x0, y0)` | Either `x0` or `y0` is zero | | `PeggedSwapInvalidRates(rateLt, rateGt)` | Either rate is zero | --- ## Related - XYCSwap: constant-product AMM for non-pegged pairs - XYCConcentrate: concentrated liquidity for non-pegged pairs - Balances: provides `balanceIn`/`balanceOut` https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/aqua-router-opcodes/decay.md `Decay` is an instruction in SwapVM, the swap engine behind 1inch Aqua. It is one of the opcodes exposed by the deployed Aqua swap router (`AquaSwapVMRouter`). Decay applies a Mooniswap-style virtual balance adjustment: after a swap it temporarily inflates `balanceIn` and deflates `balanceOut`, then gradually restores them over a decay period. This creates a spread that discourages sandwich attacks. :::info `Decay` is one of the SwapVM opcodes registered on the Aqua router (`AquaOpcodes`): `Controls`, `XYCSwap`, `XYCConcentrate`, `Decay`, `Fee`, `PeggedSwap`, and `Extruction`. ::: **Source:** `src/instructions/Decay.sol` --- ## Args encoding | Field | Offset | Size | Description | | -------- | ------ | ------------------ | ----------------------- | | `period` | 0 | 2 bytes (`uint16`) | Decay period in seconds | Build with `DecayArgsBuilder.build(uint16 decayPeriod)`. --- ## Instructions ### `_decayXD` ```solidity function _decayXD(Context memory ctx, bytes calldata args) internal ``` Applies time-decaying offsets to virtual balances before running the nested swap formula. **Behavior** 1. Reads `period` from args 2. Reads decaying offsets from storage: - `_offsets[orderHash][tokenIn][true]` -> added to `balanceIn` (making input look more expensive) - `_offsets[orderHash][tokenOut][false]` -> subtracted from `balanceOut` (making output look smaller) 3. Calls `ctx.runLoop()` to execute the nested swap formula with adjusted balances 4. In swap mode, records new offsets for the amounts transacted: - Adds `swapAmountIn` to `_offsets[orderHash][tokenIn][false]` - Adds `swapAmountOut` to `_offsets[orderHash][tokenOut][true]` **Offset decay formula** (per stored `DecayingOffset { offset, timestamp }`): ``` timeLeft = (timestamp + period) - block.timestamp // 0 if expired currentOffset = offset * timeLeft / period ``` Offsets decay linearly to zero over `period` seconds. Expired offsets return zero. **Constraint:** Must be placed before swap amounts are computed (`amountIn == 0 || amountOut == 0`). **Quote/swap divergence:** In quote mode, offsets are read but not updated. A quote reflects the current spread; the swap applies and records new offsets. Do not use backward jumps to this instruction. **Errors:** `DecayShouldBeCalledBeforeSwapAmountsComputation(amountIn, amountOut)`. --- ## Storage layout ```solidity mapping(bytes32 orderHash => mapping(address token => mapping(bool buyOrSell => DecayingOffset))) internal _offsets; struct DecayingOffset { uint216 offset; uint40 timestamp; } ``` Packed into a single storage slot (216 + 40 = 256 bits). Uses assembly for single-slot SLOAD/SSTORE. --- ## Related - Balances: sets the initial reserves; `_decayXD` adjusts them further. - XYCSwap: the typical swap formula nested after decay. https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/aqua-router-opcodes/extruction.md Extruction is a SwapVM instruction that delegates swap logic to an external, maker-controlled contract. It lets makers implement arbitrary pricing and control-flow logic that cannot be expressed with the built-in SwapVM instruction set, for example a proprietary pricing model that stays in the maker's own contract. **Source:** `src/instructions/Extruction.sol` :::info Extruction is one of the opcodes in the Aqua instruction set (`Controls`, `XYCSwap`, `XYCConcentrate`, `Decay`, `Fee`, `PeggedSwap`, `Extruction`) inherited by the deployed `AquaSwapVMRouter`. It corresponds to the "external pricing" authoring path: instead of composing existing opcodes, the maker embeds custom logic behind `IExtruction` / `IStaticExtruction`. ::: --- ## Args encoding | Field | Offset | Size | Description | | ---------------- | ------ | -------------------- | ------------------------------------------------------------------ | | `target` | 0 | 20 bytes (`address`) | External contract implementing `IExtruction` / `IStaticExtruction` | | `extructionArgs` | 20 | N bytes | Passed verbatim to the external contract's `extruction()` function | --- ## Instructions ### `_extruction` ```solidity function _extruction(Context memory ctx, bytes calldata args) internal ``` Calls the external `target` contract and applies its returned state to the swap context. **In quote mode** (`isStaticContext = true`): calls `IStaticExtruction.extruction(...)` via a regular call (view function). **In swap mode** (`isStaticContext = false`): calls `IExtruction.extruction(...)`. Both interfaces have the same signature: ```solidity function extruction( bool isStaticContext, uint256 nextPC, SwapQuery calldata query, SwapRegisters calldata swap, bytes calldata args, bytes calldata takerData ) external [view] returns ( uint256 updatedNextPC, uint256 choppedLength, SwapRegisters memory updatedSwap ); ``` The returned values replace `ctx.vm.nextPC` and `ctx.swap`. `choppedLength` bytes are consumed from `takerData`. **Errors** | Error | Condition | | ----------------------------------------------------- | ------------------------------------------------ | | `ExtructionMissingTargetArg()` | args shorter than 20 bytes | | `ExtructionChoppedExceededLength(chopped, requested)` | `takerData` has fewer bytes than `choppedLength` | --- ## External contract requirements Extruction relies on the SwapVM invariant that a static quote and the executed swap return identical amounts. Both `IExtruction` and `IStaticExtruction` **must** produce the same swap amounts for the same inputs, so that `quote()` and `swap()` agree. Non-deterministic behavior causes quote/swap inconsistency. The `target` contract **must** be non-upgradeable. Upgradeable logic can change between the quote and the swap, which would break this invariant. **Security considerations for takers:** - The target is a maker-controlled address; verify it is non-upgradeable or has trusted governance - Test quote/swap consistency before routing significant volume - Slippage protection in the outer swap provides a backstop but cannot prevent all inconsistencies --- ## Related - Controls -- built-in control flow (jumps, deadlines) ## Extruction additions These additions expand the `Extruction` opcode reference with the exact register types it operates on, a minimal compilable target contract, and a definitive answer to the `IExtruction` vs `IStaticExtruction` question. Every type and signature below is taken verbatim from `1inch/swap-vm` at `src/instructions/Extruction.sol` and `src/libs/VM.sol`. ### Type reference: SwapQuery and SwapRegisters Both structs are declared in `src/libs/VM.sol` and passed to the target as `calldata`. `SwapQuery` is read-only swap context; `SwapRegisters` holds the mutable amounts the instruction computes. The register the instruction must fill is the _missing amount_: `isExactIn ? amountOut : amountIn`. ```solidity // src/libs/VM.sol /// @dev Read-only swap information struct SwapQuery { bytes32 orderHash; // per-maker position/strategy identifier address maker; // liquidity provider address taker; // swap initiator address tokenIn; // input token address tokenOut; // output token bool isExactIn; // true => amountIn fixed, compute amountOut } /// @dev Registers used to compute the missing amount: /// isExactIn ? amountOut : amountIn struct SwapRegisters { uint256 balanceIn; // current balance of input token uint256 balanceOut; // current balance of output token uint256 amountIn; // input amount being swapped uint256 amountOut; // output amount being swapped uint256 amountNetPulled; // net pulled from maker (fee calculations) } ``` :::info `SwapQuery` has six fields (the trailing `isExactIn` is easy to miss) and `SwapRegisters` has five, all `uint256`. The instruction receives `query` as `calldata` and receives `swap` as `calldata`, but returns an updated `SwapRegisters memory` -- you copy, mutate the copy, and return it. ::: ### Target call signature The dispatcher in `Extruction._extruction` reads the first 20 bytes of `args` as the target address, slices the remainder as `extructionArgs`, and calls the target with this exact signature (identical for both interfaces): ```solidity function extruction( bool isStaticContext, uint256 nextPC, SwapQuery calldata query, SwapRegisters calldata swap, bytes calldata args, // args.slice(20): everything after the 20-byte target bytes calldata takerData // ctx.takerArgs() ) external returns ( uint256 updatedNextPC, // written back to ctx.vm.nextPC (arbitrary uint256 jump) uint256 choppedLength, // taker bytes to consume; MUST NOT exceed remaining takerData SwapRegisters memory updatedSwap ); ``` :::note `choppedLength` is validated after the call: `_extruction` reverts with `ExtructionChoppedExceededLength` if fewer than `choppedLength` taker bytes remain. Return `0` unless your strategy genuinely consumes taker-supplied data. ::: ### Reference IExtruction target A minimal, compilable target that reads `amountIn` from the registers, applies a proprietary (here: fixed) price, and writes the missing amount back. It obeys both invariants: the price is `immutable` (the target is **non-upgradeable**, so logic cannot change between quote and swap), and the logic is a single `view` function -- one implementation serves both the quote (STATICCALL) and swap (CALL) paths, which is the tightest possible guarantee that `quote() == swap()` return identical amounts. ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.30; // Illustrative import paths (in-repo these are relative to src/): import { IExtruction, IStaticExtruction } from "./instructions/Extruction.sol"; import { SwapQuery, SwapRegisters } from "./libs/VM.sol"; /// @notice Fixed-price target. `priceE18` is immutable => non-upgradeable logic. /// A single `view` implementation satisfies BOTH interface selectors, /// so the quote path and the swap path are provably identical. contract FixedPriceExtruction is IExtruction, IStaticExtruction { error RecomputeDetected(); uint256 public immutable priceE18; // tokenOut per tokenIn, 1e18-scaled constructor(uint256 priceE18_) { priceE18 = priceE18_; } function extruction( bool, // isStaticContext (unused: logic is identical either way) uint256 nextPC, SwapQuery calldata query, SwapRegisters calldata swap, bytes calldata, // args bytes calldata // takerData ) external view override(IExtruction, IStaticExtruction) returns (uint256 updatedNextPC, uint256 choppedLength, SwapRegisters memory updatedSwap) { updatedSwap = swap; // copy read-only registers into memory if (query.isExactIn) { require(swap.amountOut == 0, RecomputeDetected()); // no backward-jump recompute updatedSwap.amountOut = swap.amountIn * priceE18 / 1e18; // round down: maker-favorable } else { require(swap.amountIn == 0, RecomputeDetected()); // round up amountIn: maker-favorable updatedSwap.amountIn = (swap.amountOut * 1e18 + priceE18 - 1) / priceE18; } updatedNextPC = nextPC; // linear fall-through, no jump choppedLength = 0; // consume no taker data } } ``` :::info The `require(amount == 0)` recompute guard mirrors the pattern in first-party instructions such as `PeggedSwap` (`PeggedSwapRecomputeDetected`). Because `Extruction` alone can set an arbitrary `uint256 nextPC`, a maker _could_ jump backward into this instruction; the guard makes any such re-entry with an already-populated register revert instead of silently double-pricing, preserving quote/swap consistency. ::: ### IExtruction vs IStaticExtruction: one interface or two? **They are two distinct interfaces, both declared in**`src/instructions/Extruction.sol`**.** `IStaticExtruction` is a real source symbol -- not a typo or an alias for `IExtruction`. The two differ only in state mutability: | Interface | Mutability | Called during | Call type | | ------------------- | -------------------------- | ------------- | ------------ | | `IExtruction` | non-view (state-modifying) | `swap()` | `CALL` | | `IStaticExtruction` | `view` | `quote()` | `STATICCALL` | The dispatcher selects between them at runtime on `ctx.vm.isStaticContext`: ``` // src/instructions/Extruction.sol -- _extruction(...) if (ctx.vm.isStaticContext) { (ctx.vm.nextPC, choppedLength, ctx.swap) = IStaticExtruction(target).extruction(/* ...view path (quote)... */); } else { (ctx.vm.nextPC, choppedLength, ctx.swap) = IExtruction(target).extruction(/* ...state-modifying path (swap)... */); } ``` Both declarations have the identical function name and parameter list, so they compute the **same 4-byte selector** -- the `view` keyword does not affect selector calculation. This is the mechanism that lets one deployed contract serve both paths: a single `extruction` implementation answers both the `IStaticExtruction` STATICCALL and the `IExtruction` CALL. :::note Whether a single target implements both a view (quote) path and a non-view (swap) path is a **choice left to the target author, and the safe choice is to implement exactly one**`view`**function** (as the reference above does). A `view` implementation is callable under both STATICCALL and CALL and cannot diverge, so it cannot violate the identical-amounts invariant. If instead you deploy a genuinely state-modifying `IExtruction` path alongside a separate `view` `IStaticExtruction` path, _you_ become responsible for proving the two return bit-identical amounts for every input -- the source header of `Extruction.sol` states this as a CRITICAL SECURITY REQUIREMENT, and any drift breaks quote/swap consistency. ::: ## Custom pricing completeness (Path C) Extruction is the _external pricing_ authoring path -- Path C -- for makers whose pricing cannot be expressed by composing the built-in AMM opcodes (`XYCSwap`, `XYCConcentrate`, `Decay`, `PeggedSwap`). Instead of writing a curve as an opcode program, the maker embeds arbitrary Solidity behind `IExtruction` / `IStaticExtruction` and points one opcode at it. This section is the shipping checklist for a Path C strategy: the two hard requirements, the minimal interface surface, where the opcode sits in the deployed table, how it composes with native opcodes, and the invariants to fuzz. Every claim below is verified against `src/instructions/Extruction.sol` and `src/opcodes/AquaOpcodes.sol` at `v1.0.1`. ### The two hard requirements :::note Both requirements are load-bearing. Violating either breaks the SwapVM guarantee that a static `quote()` and the executed `swap()` return identical amounts, and the source header of `Extruction.sol` marks them a CRITICAL SECURITY REQUIREMENT. ::: | Requirement | Why it is mandatory | | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | The external `target` contract **must be non-upgradeable** | Upgradeable logic can change between the quote and the swap. The taker quotes against one implementation and settles against another, so the amounts diverge. Pin pricing behind `immutable` state or an already-frozen implementation. | | `quote()` and `swap()`**must return identical amounts** | The `IStaticExtruction` path (quote, `STATICCALL`) and the `IExtruction` path (swap, `CALL`) must compute the same `SwapRegisters` for the same inputs. Any non-determinism -- block state reads, mutable storage, divergent code paths -- is an inconsistency the router cannot detect for you. | The tightest way to satisfy both at once is the single-`view`-function pattern shown in the reference target above: one `view` implementation is callable under both `STATICCALL` and `CALL` and cannot diverge, so it satisfies the identical-amounts invariant by construction. ### Minimal interface surface A Path C target implements exactly one function. `IExtruction` (non-`view`) and `IStaticExtruction` (`view`) are two distinct interfaces declared in `Extruction.sol`; they share an identical name and parameter list, so they compute the same 4-byte selector, and a single implementation answers both. The minimal surface is: ```solidity function extruction( bool isStaticContext, // false => swap (CALL), true => quote (STATICCALL) uint256 nextPC, // current program counter; return updatedNextPC SwapQuery calldata query, // read-only swap context (6 fields) SwapRegisters calldata swap,// current registers (5 x uint256) bytes calldata args, // extructionArgs: args after the 20-byte target bytes calldata takerData // remaining taker args ) external [view] returns ( uint256 updatedNextPC, uint256 choppedLength, // taker bytes consumed; 0 unless you read takerData SwapRegisters memory updatedSwap ); ``` Fill the _missing amount_ only: `query.isExactIn ? updatedSwap.amountOut : updatedSwap.amountIn`. See the type reference and reference target above for the full `SwapQuery`/`SwapRegisters` layout and a compilable example. ### Opcode index 32: the register fold In the deployed `AquaSwapVMRouter`'s opcode table (`AquaOpcodes._opcodes()`), `Extruction._extruction` is **opcode index 32**. When the VM reaches that opcode, `_extruction`: 1. reads the first 20 bytes of `args` as the `target` address (reverts `ExtructionMissingTargetArg` if `args` is shorter); 2. calls the target -- `IStaticExtruction` in quote mode, `IExtruction` in swap mode -- selected on `ctx.vm.isStaticContext`; 3. **folds the return values into the live VM state**: `updatedSwap` replaces `ctx.swap`, `updatedNextPC` replaces `ctx.vm.nextPC`, and `choppedLength` bytes are consumed from the taker args (reverts `ExtructionChoppedExceededLength` if fewer remain). Because the returned `SwapRegisters` becomes the VM's working registers, whatever your target writes into `amountIn`/`amountOut` is what the rest of the program -- and the final settlement -- sees. The target does not return a price; it returns the swap registers. ### Composition: native opcodes stay reachable Extruction is one entry in the same opcode table as every first-party instruction -- it does not replace the VM, it plugs custom pricing into it. A single program (the program half of `order.data = hooks || program`) can interleave the Extruction opcode with native opcodes in one run: - **Controls** -- `_jump`, `_deadline`, and the taker-balance guards remain available for control flow and preconditions around your pricing call. - **Fee** -- the flat, protocol, and dynamic-protocol fee opcodes run in the same program, so a Path C strategy can price with custom logic and still take fees through native opcodes. :::note Extruction is the only opcode that can set an arbitrary `uint256 nextPC`. Makers **must not** use a backward jump into an Extruction instruction: re-executing the pricing on already-populated registers breaks consistency between `quote()` and `swap()`. Defend against it in the target with a recompute guard (revert when the register you are about to fill is already non-zero), mirroring `PeggedSwap`'s `PeggedSwapRecomputeDetected`. ::: ### Invariant and fuzz-test checklist Fuzz these properties against your target before routing significant volume. Only the first is enforced by the protocol's call structure; the rest are author responsibilities that the router cannot check for you. | Property | What to assert | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `quote == swap` | For identical `(isStaticContext-aside, nextPC, query, swap, args, takerData)`, the `IStaticExtruction` and `IExtruction` paths return bit-identical `(updatedNextPC, choppedLength, updatedSwap)`. The single-`view`-function pattern makes this hold by construction; assert it anyway if you split the paths. | | Monotonicity | A larger `amountIn` never yields a smaller `amountOut` (exact-in), and a larger `amountOut` never demands a smaller `amountIn` (exact-out). A sane pricing curve is non-decreasing in the fixed leg. | | No wrapped / spurious output | Amounts are `uint256` and cannot be negative -- so guard every subtraction and division against underflow and against silently rounding to `0`. Round consistently in the maker-favorable direction (down for `amountOut`, up for `amountIn`). | | Reverts on bad args | Malformed input must revert rather than return garbage: short `args` (< 20 bytes) surface as `ExtructionMissingTargetArg` upstream; a `choppedLength` exceeding remaining taker bytes surfaces as `ExtructionChoppedExceededLength`; out-of-range `extructionArgs` should revert inside your target. | | Recompute guard | Re-entry with an already-populated register reverts, so a backward jump into the instruction cannot double-price the same leg. | :::info The router guarantees only that the quote and the swap dispatch to the same target with the same inputs. Correctness of the pricing math, its monotonicity, its rounding direction, and its revert behavior on bad input are all the target author's responsibility -- Path C is an advanced, use-at-your-own-risk surface, and takers are expected to validate a strategy's quote/swap consistency before routing through it. ::: https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/modifiers-limit-order-fusion-router/overview.md Modifier opcodes used by the 1inch Limit Order / Fusion router -- they adjust price or gate execution around a swap opcode. Child pages: the Modifiers overview, plus MinRate, OraclePriceAdjuster, and BaseFeeAdjuster. https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/modifiers-limit-order-fusion-router/modifiers.md Modifiers are cross-cutting SwapVM instructions that extend a strategy. SwapVM is the Aqua swap engine; a modifier layers onto an existing program without changing its core swap formula. Each entry below lists which archetype it applies to, where in the program it goes, and what it changes. :::info On-chain, these modifiers are opcodes drawn from two different router opcode tables. The Aqua router (`AquaSwapVMRouter`) inherits `Controls, XYCSwap, XYCConcentrate, Decay, Fee, PeggedSwap, Extruction`. Fee, decay (MEV), and access-control (Controls) modifiers run on the Aqua router. Rate guards (MinRate), the oracle and base-fee adjusters, and invalidators belong to the Limit Order and Fusion router opcode set, not the Aqua router. They are documented here for completeness and are called out as such. ::: --- ## Modifiers on the Aqua router ### Fees All fee instructions are **wrapping**: they call `ctx.runLoop()` internally. Place them **before** the swap formula. They execute the formula inside the nested loop and adjust the returned amounts. A fee instruction must be placed before swap amounts are computed. Both `amountIn` and `amountOut` must be zero when the fee instruction starts. :::info In the v1 MVP only the LP fee (surfaced in the app as the "Swap fee", instruction `_flatFeeAmountInXD`) is non-zero. The protocol-fee instructions exist in the contracts but are set to **0** in v1 and are not displayed. The fee scale is `1e9` and `feePercent` is a decimal in `[0,1]` (for example `0.0030` = 0.30%). There is no on-chain fee event; fees are instructions embedded in the program and reflected in swap amounts. LP fees auto-compound to the maker balance via the `Pushed` event. ::: | Instruction | Leg | Transfer | When to use | | ----------------------------------- | ----------- | ----------------- | ---------------------------------------------------------------------------------------- | | `_flatFeeAmountInXD` | `amountIn` | None | The LP swap fee (the only non-zero fee in v1). Reduces effective input before pricing. | | `_flatFeeAmountOutXD` | `amountOut` | None | LP fee deducted from the taker's received amount. | | `_protocolFeeAmountInXD` | `amountIn` | ERC-20 from maker | Protocol revenue on the input leg; requires maker pre-approval. Set to 0 in v1. | | `_protocolFeeAmountOutXD` | `amountOut` | ERC-20 from maker | Protocol revenue on the output leg. Set to 0 in v1. | | `_aquaProtocolFeeAmountInXD` | `amountIn` | Aqua pull | Protocol fee on the input leg, settled from Aqua balances. Set to 0 in v1. | | `_aquaProtocolFeeAmountOutXD` | `amountOut` | Aqua pull | Protocol fee on the output leg, settled from Aqua balances. Set to 0 in v1. | | `_dynamicProtocolFeeAmountInXD` | `amountIn` | ERC-20 from maker | Fee rate read from an external `IProtocolFeeProvider` at execution time. Set to 0 in v1. | | `_aquaDynamicProtocolFeeAmountInXD` | `amountIn` | Aqua pull | Dynamic protocol fee settled via `IAqua.pull()`. Set to 0 in v1. | Corresponding `Fee.sol` builders: `buildFlatFee`, `buildProtocolFee`, `buildDynamicProtocolFee`. **Applies to:** all archetypes. --- ### MEV protection (decay) The `Decay` opcode adds a post-swap spread that discourages sandwich attacks. | Instruction | Where in program | What it does | | ----------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `_decayXD` | After `_dynamicBalancesXD`, before the swap formula | Temporarily inflates `balanceIn` and deflates `balanceOut` after each swap, then decays back over `period` seconds. Creates a spread that discourages sandwich attacks. | **Applies to:** bidirectional AMM strategies (requires dynamic balances). **Program position:** ``` _dynamicBalancesXD -> _decayXD -> [fee] -> swap formula ``` --- ### Access control Access-control checks are `Controls` opcodes. Place them at the start of the program, before balance setup or swap logic. Makers are permissionless; only takers are gated. At launch the gate is applied per strategy, not as a protocol-wide switch, and it is checked at swap time. | Instruction | What it checks | When to use | | ---------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `_deadline` | `block.timestamp <= deadline` | All strategies. Bounds maker exposure to stale orders. | | `_onlyTxOriginTokenBalanceNonZero` | `balanceOf(tx.origin) > 0`; reverts `TxOriginTokenBalanceIsZero` otherwise | The deployed launch gate. The dApp assembler calls `withTxOriginAccessToken(aquaKycToken)` on every strategy, gating fills to KycNFT holders. | | `_onlyTakerTokenBalanceGte` | Holder balance `minAmount` | Gate fills to holders above a minimum balance. | | `_onlyTakerTokenSupplyShareGte` | Holder balance X% of total supply | Gate fills to large holders. | note The launch gate checks `tx.origin`, so smart accounts, multisigs, and ERC-4337 bundlers cannot pass it. Per-strategy taker allow-lists (`_whitelistSingleTaker` / `_whitelistMultipleTakers`) exist in source but are not registered on-chain today, so do not treat them as an available feature. The launch gate checks `tx.origin`, so smart accounts, multisigs, and ERC-4337 bundlers cannot pass it. Per-strategy taker allow-lists (`_whitelistSingleTaker` / `_whitelistMultipleTakers`) exist in source but are not registered on-chain today, so do not treat them as an available feature. **Applies to:** all archetypes. --- ## Modifiers on the Limit Order and Fusion router The instructions in this section are part of the Limit Order and Fusion router opcode set. They are not in the Aqua router opcode table (`Controls, XYCSwap, XYCConcentrate, Decay, Fee, PeggedSwap, Extruction`). Several also depend on static balances, which are not available. ### Rate guards Applied **after** the swap formula (amounts must already be computed). | Instruction | Behavior | When to use | | ------------------- | ----------------------------------------------------- | -------------------------------- | | `_requireMinRate1D` | Reverts if `amountIn / amountOut < rateIn / rateOut` | Hard floor. Reject the swap. | | `_adjustMinRate1D` | Caps amounts to the minimum rate instead of reverting | Soft floor. Adjust and continue. | --- ### Dynamic pricing (post-swap adjusters) Applied **after** the swap formula. Both require `amountIn > 0 && amountOut > 0`. | Instruction | What it does | When to use | | ------------------------ | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | `_oraclePriceAdjuster1D` | Improves the taker's rate toward the Chainlink oracle price, capped at `maxPriceDecay` | Oracle-anchored limit orders; keep the rate tracking the market. | | `_baseFeeAdjuster1D` | Improves the taker's rate when `block.basefee` exceeds a baseline, proportional to gas cost | Gas-responsive orders; compensate the taker for high-gas periods. | **Applies to:** single-direction strategies with static balances. Both work with 1->0 swaps (token1 -> token0). Static balances are not available. --- ### Invalidators Invalidators (for example `_invalidateBit1D`) mark an order as used after amounts are known. They belong to the Limit Order and Fusion router opcode set, not the Aqua router. --- ## Combining modifiers Ordering within a program follows the rules described in Program Model. note The single-direction example below is a Limit Order and Fusion router program. It requires static balances, which are not available. The single-direction example below is a Limit Order and Fusion router program. It requires static balances, which are not available. A fully composed single-direction limit order with a fee, a gas adjustment, and an access check: ``` _deadline <- access guard first _staticBalancesXD <- set rate _flatFeeAmountInXD <- wrapping: fee before formula _limitSwap1D <- formula runs inside fee's runLoop _baseFeeAdjuster1D <- post-swap: adjust for gas cost _invalidateBit1D <- invalidate after amounts known ``` An Aqua router 2D AMM with MEV protection and a flat fee: ``` _dynamicBalancesXD <- load reserves _decayXD <- wrapping: MEV spread before fee _flatFeeAmountInXD <- wrapping: fee before formula _xycSwapXD <- formula runs inside nested loops ``` --- ## Related - Instruction set overview: full reference for every instruction. - Program Model: ordering rules and invariants. https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/modifiers-limit-order-fusion-router/min-rate.md MinRate is a rate-guard instruction for **SwapVM**, the Aqua swap engine. It enforces or adjusts a minimum exchange rate after the nested swap formula runs, protecting makers from accepting a rate below a configured floor. **Source:** `src/instructions/MinRate.sol` :::info MinRate is not part of the deployed Aqua router opcode set (`Controls`, `XYCSwap`, `XYCConcentrate`, `Decay`, `Fee`, `PeggedSwap`, `Extruction`). It is a SwapVM rate-guard modifier that runs on the limit-order router, where it composes with rate-setting instructions such as `LimitSwap`. ::: --- ## Args encoding Rates are stored in token-address order (lower address first): | Field | Offset | Size | Description | | -------- | ------ | ------------------ | ------------------------------------------ | | `rateLt` | 0 | 8 bytes (`uint64`) | Rate for the token with the lower address | | `rateGt` | 8 | 8 bytes (`uint64`) | Rate for the token with the higher address | Total: 16 bytes. Build with `MinRateArgsBuilder.build(address tokenA, address tokenB, uint64 rateA, uint64 rateB)`, which automatically assigns `rateLt`/`rateGt` based on address comparison. At execution time, the instruction re-maps to `(rateIn, rateOut)` based on `tokenIn < tokenOut`. **Rate interpretation:** The minimum acceptable rate is `rateIn / rateOut`, that is, the number of `tokenIn` units per `tokenOut` unit. The swap is valid when: ``` amountIn x rateOut >= rateIn x amountOut ``` --- ## Instructions ### `_requireMinRate1D` ```solidity function _requireMinRate1D(Context memory ctx, bytes calldata args) internal ``` Runs the nested swap formula via `ctx.runLoop()`, then reverts if the resulting rate is below the minimum. Must be placed before swap amounts are computed. **Errors** | Error | Condition | | --------------------------------------------------------------- | ----------------------------------------- | | `MinRateExpectedBeforeSwapAmountsComputed(amountIn, amountOut)` | Amounts already set | | `MinRateFailed(swapAmountIn, swapAmountOut, rateIn, rateOut)` | `amountIn x rateOut < rateIn x amountOut` | --- ### `_adjustMinRate1D` ```solidity function _adjustMinRate1D(Context memory ctx, bytes calldata args) internal ``` Runs the nested swap formula via `ctx.runLoop()`. If the resulting rate is below the minimum, caps the amounts to exactly the minimum rate rather than reverting. | Mode | Adjustment when rate below minimum | | -------- | ------------------------------------------- | | exactIn | `amountOut = amountIn x rateOut / rateIn` | | exactOut | `amountIn = amountOut x rateIn / rateOut` | Must be placed before swap amounts are computed. Requires the nested loop to produce non-zero amounts. **Errors** | Error | Condition | | --------------------------------------------------------------- | -------------------------- | | `MinRateExpectedBeforeSwapAmountsComputed(amountIn, amountOut)` | Amounts already set | | `MinRateRunLoopExpectToComputeSwapAmounts(amountIn, amountOut)` | Loop produced zero amounts | --- ## Related - LimitSwap: sets a fixed rate; MinRate can enforce a floor on top. - OraclePriceAdjuster: adjusts amounts toward an oracle price after the swap. https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/modifiers-limit-order-fusion-router/oracle-price-adjuster.md OraclePriceAdjuster is an instruction for SwapVM, the Aqua swap engine. It nudges swap amounts toward a Chainlink oracle price: when the oracle shows a better rate than the current swap amounts, the taker's fill is improved, up to a configured maximum. **Source:** `src/instructions/OraclePriceAdjuster.sol` Compatible with 1->0 swaps (token1 -> token0) only. :::info OraclePriceAdjuster is not part of the deployed Aqua router opcode set. The `AquaSwapVMRouter` opcode table (`AquaOpcodes`) ships `Controls`, `XYCSwap`, `XYCConcentrate`, `Decay`, `Fee`, `PeggedSwap`, and `Extruction`. OraclePriceAdjuster is available as a post-swap price modifier on the relevant SwapVM router, not as a live opcode on the deployed Aqua router. ::: --- ## Args encoding | Field | Offset | Size | Type | Description | | ---------------- | ------ | -------- | --------- | ---------------------------------------------------------------------------------------------------- | | `maxPriceDecay` | 0 | 8 bytes | `uint64` | Maximum adjustment coefficient in 1e18 scale; must be `< 1e18` (e.g. `0.95e18` = max 5% improvement) | | `maxStaleness` | 8 | 2 bytes | `uint16` | Max oracle data age in seconds; `0` disables the staleness check | | `oracleDecimals` | 10 | 1 byte | `uint8` | Oracle price decimal places; `0` triggers a live `oracle.decimals()` call | | `oracleAddress` | 11 | 20 bytes | `address` | Chainlink AggregatorV3-compatible price feed | Total: 31 bytes. Build with `OraclePriceAdjusterArgsBuilder.build(uint64 maxPriceDecay, uint16 maxStaleness, uint8 oracleDecimals, address oracleAddress)`. --- ## Instructions ### `_oraclePriceAdjuster1D` ```solidity function _oraclePriceAdjuster1D(Context memory ctx, bytes calldata args) internal view ``` Must be placed **after** a swap instruction so that both `amountIn` and `amountOut` are already computed. **Behavior** 1. Calls `oracle.latestRoundData()` to get `answer` and `updatedAt` 2. Checks staleness: reverts if `block.timestamp > updatedAt + maxStaleness` (when `maxStaleness > 0`) 3. Normalizes oracle price to 1e18 scale using `oracleDecimals` 4. Computes current swap price: `currentPrice = amountOut x 1e18 / amountIn` 5. If `oraclePrice > currentPrice` (oracle favors the taker): - exactIn: increases `amountOut` proportionally, capped at `2e18 maxPriceDecay` - exactOut: decreases `amountIn` proportionally, floored at `maxPriceDecay` 6. If `oraclePrice <= currentPrice`: no adjustment **Errors** | Error | Condition | | --------------------------------------------------------------------------- | ----------------------------------- | | `OraclePriceAdjusterShouldBeAppliedAfterSwap()` | `amountIn == 0 \|\| amountOut == 0` | | `OraclePriceAdjusterOraclePriceStale(currentTime, updatedAt, maxStaleness)` | Oracle data too old | --- ## Related - LimitSwap: sets base amounts before this instruction - BaseFeeAdjuster: adjusts amounts based on gas price (also post-swap) https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/modifiers-limit-order-fusion-router/base-fee-adjuster.md BaseFeeAdjuster is a SwapVM (the Aqua swap engine) price-modifier instruction. It adjusts swap amounts based on the current network gas price: when `block.basefee` exceeds the maker's configured baseline, the maker improves the effective price to compensate the taker for elevated transaction costs. **Source:** `src/instructions/BaseFeeAdjuster.sol` Compatible with 1->0 swaps (token1 -> token0) only. :::info **Router scope:** BaseFeeAdjuster is not part of the shipped Aqua router opcode set (`Controls`, `XYCSwap`, `XYCConcentrate`, `Decay`, `Fee`, `PeggedSwap`, `Extruction`). It is a SwapVM price-modifier instruction offered on the relevant router, alongside the other post-swap modifiers listed under Related (for example OraclePriceAdjuster), rather than on the Aqua router. ::: --- ## Args encoding | Field | Offset | Size | Type | Description | | ------------------ | ------ | -------- | -------- | --------------------------------------------------------------------------------- | | `baseGasPrice` | 0 | 8 bytes | `uint64` | Baseline gas price (wei); adjustment triggers when `block.basefee > baseGasPrice` | | `ethToToken1Price` | 8 | 12 bytes | `uint96` | ETH price in token1 units (1e18 scale, e.g. `3000e18` for 1 ETH = 3000 USDC) | | `gasAmount` | 20 | 3 bytes | `uint24` | Estimated gas units to compensate (e.g. `150000`) | | `maxPriceDecay` | 23 | 8 bytes | `uint64` | Maximum allowed price adjustment coefficient in 1e18 scale | Total: 31 bytes. Build with `BaseFeeAdjusterArgsBuilder.build(uint64 baseGasPrice, uint96 ethToToken1Price, uint24 gasAmount, uint64 maxPriceDecay)`. --- ## Instructions ### `_baseFeeAdjuster1D` ```solidity function _baseFeeAdjuster1D(Context memory ctx, bytes calldata args) internal view ``` Must be placed **after** a swap instruction so that both `amountIn` and `amountOut` are already computed. **Behavior** When `block.basefee > baseGasPrice`: 1. Calculates extra cost: `extraGasCost = (block.basefee baseGasPrice) x gasAmount` 2. Converts to token1: `extraCostInToken1 = extraGasCost x ethToToken1Price / 1e18` 3. Computes a price improvement ratio capped by `maxPriceDecay`: - exactIn: increases `amountOut` (taker receives more token0) - exactOut: decreases `amountIn` (taker pays less token1) When `block.basefee <= baseGasPrice`: no adjustment is made. **Errors:** `BaseFeeAdjusterShouldBeAppliedAfterSwap()` if `amountIn == 0 || amountOut == 0`. --- ## Related - LimitSwap: typical predecessor that sets the base amounts. - OraclePriceAdjuster: oracle-based price adjustment, also applied post-swap. https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/limit-order-fusion-opcodes/invalidators.md Invalidators is a SwapVM instruction module that provides order invalidation primitives. It prevents replay, enforces one-time execution, and tracks partial fills against a cap. :::info **Router scope:** These instructions belong to the limit-order opcode set, which is compiled into the limit-order router. They are not part of the deployed Aqua router. The Aqua router (`AquaSwapVMRouter`) inherits the `AquaOpcodes` set (`Controls`, `XYCSwap`, `XYCConcentrate`, `Decay`, `Fee`, `PeggedSwap`, `Extruction`) and does not include `Invalidators`. Both routers are compiled by the same SwapVM engine, so the encoding described here does not appear in an Aqua strategy program. ::: **Source:** `src/instructions/Invalidators.sol` --- ## Public state ```solidity mapping(address maker => mapping(uint256 slotIndex => uint256 bitmap)) public bitInvalidators; mapping(address maker => mapping(bytes32 orderHash => mapping(address token => uint256 filled))) public tokenInInvalidators; mapping(address maker => mapping(bytes32 orderHash => mapping(address token => uint256 filled))) public tokenOutInvalidators; ``` --- ## External invalidation functions Makers can pre-cancel orders by calling these directly (no program required): | Function | Effect | | --------------------------------------------------------- | -------------------------------------------------------------------------------- | | `invalidateBit(uint256 bitIndex)` | Sets bit at `bitIndex` in `bitInvalidators[msg.sender]` | | `invalidateTokenIn(bytes32 orderHash, address tokenIn)` | Sets `tokenInInvalidators[msg.sender][orderHash][tokenIn] = type(uint256).max` | | `invalidateTokenOut(bytes32 orderHash, address tokenOut)` | Sets `tokenOutInvalidators[msg.sender][orderHash][tokenOut] = type(uint256).max` | --- ## Instructions ### `_invalidateBit1D` ```solidity function _invalidateBit1D(Context memory ctx, bytes calldata args) internal ``` One-time execution guard. Checks and sets a single bit in a packed 256-bit bitmap keyed by `maker`. | Field | Offset | Size | Description | | ---------- | ------ | ------------------ | ------------------------------------------------------------------- | | `bitIndex` | 0 | 4 bytes (`uint32`) | Bit index; slot = `bitIndex >> 8`, bit position = `bitIndex & 0xFF` | Build with `InvalidatorsArgsBuilder.buildInvalidateBit(uint32 bitIndex)`. **Behavior:** If the bit is not set, proceeds and sets it (in swap mode). If the bit is already set, reverts. In quote mode, checks but does not set. **Errors:** `InvalidatorsBitAlreadySet(maker, bitIndex, bitmap)`. --- ### `_invalidateTokenIn1D` ```solidity function _invalidateTokenIn1D(Context memory ctx, bytes calldata /* args */) internal ``` Partial fill tracking against `balanceIn`. Accumulates `amountIn` into `tokenInInvalidators[maker][orderHash][tokenIn]`. Reverts if the cumulative total would exceed `ctx.swap.balanceIn`. **Args:** none (0 bytes) **Behavior:** 1. If `amountIn == 0`, calls `ctx.runLoop()` first (waits for exactOut computation) 2. Reads `prefilled = tokenInInvalidators[maker][orderHash][tokenIn]` 3. Requires `prefilled + amountIn <= balanceIn` 4. In swap mode, stores `newFilled` **Errors** | Error | Condition | | ------------------------------------------------------------- | ----------------------------------- | | `InvalidateTokenInExpectsAmountInToBeComputed()` | `amountIn` still zero after runLoop | | `InvalidatorsTokenInExceeded(prefilled, amountIn, balanceIn)` | Fill would exceed balance | --- ### `_invalidateTokenOut1D` ```solidity function _invalidateTokenOut1D(Context memory ctx, bytes calldata /* args */) internal ``` Partial fill tracking against `balanceOut`. Accumulates `amountOut` into `tokenOutInvalidators[maker][orderHash][tokenOut]`. Reverts if cumulative total would exceed `ctx.swap.balanceOut`. **Args:** none (0 bytes) **Behavior:** Mirrors `_invalidateTokenIn1D` but tracks `amountOut` / `tokenOut` / `balanceOut`. **Errors** | Error | Condition | | --------------------------------------------------------------- | ------------------------------------ | | `InvalidateTokenOutExpectsAmountOutToBeComputed()` | `amountOut` still zero after runLoop | | `InvalidatorTokenOutExceeded(prefilled, amountOut, balanceOut)` | Fill would exceed balance | **Quote/swap divergence (all instructions):** In quote mode, state is read but not updated. A quote may succeed while the swap reverts if the order was filled between the two calls. Do not use backward jumps to any of these instructions. --- ## Related - LimitSwap: sets `balanceIn`/`balanceOut` that define fill caps - Controls: alternative access guards (deadline, token balance) https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/limit-order-fusion-opcodes/limit-swap.md Fixed-rate swap at a maker-defined price. Computes `amountIn` or `amountOut` from the fixed ratio `balanceOut / balanceIn`. **Source:** `src/instructions/LimitSwap.sol` :::info **Scope:** LimitSwap is an instruction in the Limit Order Protocol opcode set, executed by SwapVM (the Aqua swap engine) on the limit-order router. It is **not** part of the deployed 1inch Aqua opcode set (`Controls`, `XYCSwap`, `XYCConcentrate`, `Decay`, `Fee`, `PeggedSwap`, `Extruction`) served by the `AquaSwapVMRouter`. Aqua strategies do not use LimitSwap; order-level partial fills belong to Limit Order and Fusion, not to Aqua pools. ::: --- ## Args encoding | Field | Size | Description | | ------------------ | --------------- | ------------------------------------------------ | | `makerDirectionLt` | 1 byte (`bool`) | `true` if `tokenIn < tokenOut` at order creation | Build with `LimitSwapArgsBuilder.build(address tokenIn, address tokenOut)`, which encodes `tokenIn < tokenOut` automatically. --- ## Instructions ### `_limitSwap1D` ```solidity function _limitSwap1D(Context memory ctx, bytes calldata args) internal pure ``` Computes swap amounts at the fixed rate `balanceOut / balanceIn`. Supports partial fills. | Mode | Computation | | -------- | -------------------------------------------------------- | | exactIn | `amountOut = amountIn * balanceOut / balanceIn` (floor) | | exactOut | `amountIn = amountOut * balanceIn / balanceOut` (ceil) | **Direction check:** The `makerDirectionLt` arg encodes the token ordering when the maker shipped the order. At execution time the taker's `tokenIn < tokenOut` must match the recorded value. This prevents the order from being filled in the wrong direction. **Errors** | Error | Condition | | ------------------------------------------------------------- | -------------------------------------------------------- | | `LimitSwapRequiresBothBalancesNonZero(balanceIn, balanceOut)` | Either balance is zero | | `LimitSwapDirectionMismatch()` | `makerDirectionLt != (tokenIn < tokenOut)` | | `LimitSwapRecomputeDetected()` | `amountOut != 0` (exactIn) or `amountIn != 0` (exactOut) | --- ### `_limitSwapOnlyFull1D` ```solidity function _limitSwapOnlyFull1D(Context memory ctx, bytes calldata args) internal pure ``` Same direction check and args as `_limitSwap1D`, but requires the swap to consume the **entire** available balance. Partial fills revert. | Mode | Requirement | Result | | -------- | ------------------------- | ------------------------ | | exactIn | `amountIn == balanceIn` | `amountOut = balanceOut` | | exactOut | `amountOut == balanceOut` | `amountIn = balanceIn` | **Errors** (in addition to those of `_limitSwap1D`) | Error | Condition | | ------------------------------------------------------------------------- | ----------------------------------- | | `LimitSwapFullyRequiresAmountInToMatchBalanceIn(amountIn, balanceIn)` | exactIn: `amountIn != balanceIn` | | `LimitSwapFullyRequiresAmountOutToMatchBalanceOut(amountOut, balanceOut)` | exactOut: `amountOut != balanceOut` | --- ## Related - Balances sets `balanceIn` and `balanceOut` before this instruction runs. - DutchAuction applies time-based price decay on top of LimitSwap. - Invalidators tracks partial fills against `balanceIn` and `balanceOut`. - TWAPSwap inherits LimitSwap for per-chunk price calculation. https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/limit-order-fusion-opcodes/twap-swap.md TWAPSwap is a time-weighted average price (TWAP) selling instruction. It combines linear liquidity unlocking, exponential Dutch-auction price decay, and automatic price improvement after periods of insufficient liquidity. :::info TWAPSwap belongs to the limit-order router's opcode set (alongside instructions such as `Invalidators`), not the Aqua opcode set. Like every 1inch swap instruction, it runs on SwapVM, the shared swap engine. ::: **Source:** `src/instructions/TWAPSwap.sol` Inherits `LimitSwap` and calls `_limitSwap1D` internally for per-chunk price computation. --- ## Args encoding Encoded with `abi.encode(TwapArgs)` (ABI-encoded struct, not packed): | Field | ABI slot | Description | | --------------------------- | ----------------- | ----------------------------------------------------------------------------------------- | | `balanceIn` | slot 0 (32 bytes) | Expected `tokenIn` amount that defines the initial exchange rate | | `balanceOut` | slot 1 (32 bytes) | Total `tokenOut` available for the full TWAP duration | | `startTime` | slot 2 (32 bytes) | TWAP start timestamp (Unix seconds) | | `duration` | slot 3 (32 bytes) | Total duration in seconds | | `priceBumpAfterIlliquidity` | slot 4 (32 bytes) | Price improvement factor in 1e18 scale after an illiquidity period (e.g. `1.1e18` = +10%) | | `minTradeAmountOut` | slot 5 (32 bytes) | Minimum output per swap; enforced during the TWAP window | Total: 192 bytes. Build with `TWAPSwapArgsBuilder.build(TwapArgs memory args)`. --- ## Instructions ### `_twap` ```solidity function _twap(Context memory ctx, bytes calldata argsData) internal ``` **Behavior** 1. **Linear unlocking**: `unlocked = balanceOut x min(elapsed, duration) / duration`; `available = unlocked totalSold` 2. **Auction parameters for this chunk**: on the first swap, uses `(balanceIn, balanceOut, startTime)` as base. On subsequent swaps, uses `(lastSwap.amountIn, lastSwap.amountOut, lastSwap.timestamp)`. 3. **Illiquidity bump**: If the previous swap interval had insufficient liquidity to cover `minTradeAmountOut`, a proportional bump is applied to `baseAmountIn` (makes the price better for the next taker) and the effective `auctionStartTime` is adjusted. 4. **Dutch auction decay**: `decay = 0.9999e18 ^ (block.timestamp auctionStartTime)` (hardcoded 0.01% per second decay). Sets `balanceIn = baseAmountIn` and `balanceOut = baseAmountOut x decay / 1e18`, then calls `ctx.runLoop()` which invokes the nested `_limitSwap1D`. 5. **Validation**: - During TWAP window: `amountOut >= minTradeAmountOut` - Always: `amountOut <= available` 6. **State update** (swap mode only): stores `(amountIn, amountOut, block.timestamp, totalSold + amountOut)` in `twapLastSwaps[orderHash]`. **Quote/swap divergence:** In quote mode, last swap data is read but not updated. Another fill between quote and swap may cause the swap to revert. Do not use backward jumps to this instruction. **Errors** | Error | Condition | | --------------------------------------------------------- | --------------------------------------------------- | | `TWAPSwapMinTradeAmountNotReached(amountIn, minAmount)` | During TWAP window: `amountOut < minTradeAmountOut` | | `TWAPSwapTradeAmountExceedLiquidity(amountIn, available)` | `amountOut > available` | --- ## Public state ```solidity struct LastSwap { uint256 amountIn; uint256 amountOut; uint256 timestamp; uint256 totalSold; } mapping(bytes32 orderHash => LastSwap) public twapLastSwaps; ``` --- ## Configuration guidelines `minTradeAmountOut` should be 1000x the expected gas cost in output token value: - Ethereum mainnet (~$50 gas) -> set equivalent of $50,000+ - Arbitrum/Optimism (~$0.50 gas) -> set equivalent of $500+ `priceBumpAfterIlliquidity` compensates for the mandatory wait between chunks: | `minTradeAmountOut` as % of `balanceOut` | Recommended bump | | ---------------------------------------- | ---------------- | | 0.1% | 1.05-1.10 x 1e18 | | 1% | 1.10-1.20 x 1e18 | | 5% | 1.30-1.50 x 1e18 | | 10% | 1.50-2.00 x 1e18 | --- ## Related - LimitSwap: provides the per-chunk price logic. - DutchAuction: standalone Dutch auction without linear unlocking. - Invalidators: alternative partial fill tracking (also in the limit-order opcode set). https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/limit-order-fusion-opcodes/dutch-auction.md `DutchAuction` is a SwapVM pre-swap price modifier that applies exponential, time-based price decay to virtual reserves before the swap formula runs. The longer the auction runs, the more the price improves for the taker. :::info **Scope:** `DutchAuction` is the Dutch-auction modifier that belongs to the Limit Order and Fusion opcode set (it is what distinguishes a Fusion order from a plain limit order). It is not part of the Aqua AMM router opcode set (`Controls`, `XYCSwap`, `XYCConcentrate`, `Decay`, `Fee`, `PeggedSwap`, `Extruction`). Both routers run on the same engine, SwapVM. ::: **Source:** `src/instructions/DutchAuction.sol` --- ## Args encoding | Field | Offset | Size | Type | Description | | ------------- | ------ | ------- | -------- | ------------------------------------------------------------ | | `startTime` | 0 | 5 bytes | `uint40` | Auction start (Unix timestamp, seconds) | | `duration` | 5 | 2 bytes | `uint16` | Auction length in seconds; expires at `startTime + duration` | | `decayFactor` | 7 | 8 bytes | `uint64` | Per-second decay multiplier in 1e18 scale; must be `< 1e18` | Total: 15 bytes. Build with `DutchAuctionArgsBuilder.build(uint40 startTime, uint16 duration, uint64 decayFactor)`. **Decay factor examples** | `decayFactor` | Effect per second | | ------------- | ----------------- | | `0.9999e18` | 0.01% decay/s | | `0.999e18` | 0.1% decay/s | | `0.99e18` | ~1% decay/s | | `0.9e18` | ~10% decay/s | --- ## Instructions Both instructions must be placed **before** any swap formula instruction (balances must not have amounts computed yet). They modify `balanceIn` or `balanceOut` so the downstream swap formula sees the decayed value. Decay is computed as `decay = decayFactor ^ elapsed` using fixed-point exponentiation. ### `_dutchAuctionBalanceIn1D` ```solidity function _dutchAuctionBalanceIn1D(Context memory ctx, bytes calldata args) internal view ``` Shrinks `balanceIn` by the decay factor. The downstream swap instruction prices the same `amountOut` against a smaller `balanceIn`, giving a lower effective price for the taker (the maker accepts less input). ``` ctx.swap.balanceIn = balanceIn * decay / 1e18 ``` --- ### `_dutchAuctionBalanceOut1D` ```solidity function _dutchAuctionBalanceOut1D(Context memory ctx, bytes calldata args) internal view ``` Expands `balanceOut` by the inverse of the decay factor. The downstream swap instruction computes a larger `amountOut` for the same `amountIn` (the taker receives more output over time). ``` ctx.swap.balanceOut = balanceOut * 1e18 / decay ``` --- ## Shared behavior and errors Both instructions: - Revert if `block.timestamp > startTime + duration`. - Require that neither `amountIn` nor `amountOut` is set yet, so they must precede the swap formula. - Carry the `1D` suffix, meaning they read dynamic chain state (`block.timestamp`) and are therefore declared `view`, not pure. | Error | Condition | | --------------------------------------------------------------------------- | ---------------------------------------- | | `DutchAuctionExpired(currentTime, deadline)` | `block.timestamp > startTime + duration` | | `DutchAuctionShouldBeAppliedBeforeSwapAmountsComputed(amountIn, amountOut)` | Either amount already set | --- ## Related - LimitSwap: typical predecessor that sets the base rate. - Balances: sets reserves before this instruction. https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/patterns/patterns-and-decision-tree.md A **Strategy** is the ABI-encoded form of a SwapVM **Order**. The Order wraps a **Program** (an ordered sequence of instructions) that SwapVM, the Aqua swap engine, executes on every `quote()` and `swap()` call. Aqua does not interpret the Strategy body; it treats it as opaque bytes and only manages balance accounting. All pricing and execution logic lives inside the Program. See [Program Model](../program-model) for how programs are structured, and Modifiers for cross-cutting extensions. :::info **Two routers.** The stateful AMM archetypes (constant product, concentrated, pegged, decay) and the advanced access and branching patterns run on the Aqua swap router, `AquaSwapVMRouter`, whose deployed opcode set is `Controls, XYCSwap, XYCConcentrate, Decay, Fee, PeggedSwap, Extruction`. The fixed-rate archetypes (limit order, RFQ, TWAP/DCA, Dutch auction) belong to the 1inch Limit Order and Fusion product, which shares the SwapVM engine but runs on a separate limit-order router with its own opcode sets (`Invalidators`, `SeriesEpochManager`). Each section below notes which router applies. ::: --- ## Decision tree ``` Is liquidity stateless (fixed rate, one direction)? Yes -> Fixed-rate strategies [Limit Order / Fusion router] One-shot execution needed? -> Limit Order (bitmap invalidator) Partially fillable? -> Limit Order (token-out invalidator) Off-chain quoted price? -> RFQ Price decays over time? -> Dutch Auction Drip over a fixed schedule? -> TWAP / DCA Is liquidity stateful (bidirectional AMM)? Yes -> AMM strategies [Aqua router] Volatile pair (x*y=k)? -> Constant Product Concentrated into a range? -> Concentrated Liquidity Pegged / stable pair? -> Pegged Swap MEV protection needed? -> Decay AMM Need runtime branching or access control? Yes -> Advanced strategies [Aqua router] Gate by holder status? -> Conditional Access Pick best of N sub-strategies? -> Best Route Selector ``` --- ## Fixed rate (Limit Order / Fusion router) Single-direction swaps from a fixed set of embedded balances. These archetypes are part of the 1inch Limit Order and Fusion product, a separate backend that shares the SwapVM engine but runs on its own limit-order router. Fusion adds only a Dutch-auction price modifier. RFQ and resolver-inventory fills route through the Aggregation Router and Limit Order Protocol, not Aqua pools. Order-level partial fills belong to this product, not to Aqua strategies. | Strategy | Formula | Invalidation | Use case | | ------------- | ------------------------------ | ---------------- | ----------------------------------------- | | Limit Order | `_limitSwap1D` | Bit or token-out | Fill once or partially fill to exhaustion | | RFQ | `_limitSwap1D` | Bit | Off-chain quoted price, single fill | | Dutch Auction | `_limitSwap1D` + balance decay | Token-out | Price improves over time until filled | | TWAP / DCA | `_twap` | Built-in | Drip liquidity over a schedule | --- ## AMM (Aqua router) Stateful bidirectional pools; balances persist across swaps in SwapVM storage. | Strategy | Formula | Key feature | | ---------------------- | ----------------------------------------------- | ------------------------------------- | | Constant Product | `_xycSwapXD` | Classic `x x y = k` | | Concentrated Liquidity | `_xycConcentrateGrowLiquidity2D` + `_xycSwapXD` | Capital-efficient price range | | Pegged Swap | `_peggedSwapGrowPriceRange2D` | Low-slippage stable pairs | | Decay AMM | `_decayXD` + `_xycSwapXD` | Virtual-balance spread, MEV-resistant | v1.0 ships two-token strategies only, and one router serves all three AMM types; the strategy type is set by the Program bytes, not by a separate app address. Concentrated liquidity is efficient, not leveraged: any effective-depth multiplier is a theoretical ceiling capped by the maker's real wallet balance, and tokens are never borrowed. --- ## Advanced (Aqua router) Runtime branching, external delegation, and access gating. | Strategy | Key instruction | Use case | | ------------------- | ---------------------------------- | ----------------------------------------------- | | Conditional Access | `_onlyTxOriginTokenBalanceNonZero` | Restrict fills to token/NFT holders | | Best Route Selector | `_extruction` | Pick best of N sub-strategies at execution time | Conditional Access is enforced by the `Controls` opcode set. The deployed launch gate is `_onlyTxOriginTokenBalanceNonZero(token)`, which reverts unless `balanceOf(tx.origin) > 0`; because it checks `tx.origin`, smart accounts, multisigs, and 4337 bundlers cannot pass it. Per-strategy taker allow-lists exist in source but are not registered on-chain today. Best Route Selector uses the `Extruction` opcode (`IExtruction`): the external pricing target must be non-upgradeable, and `quote()` and `swap()` must return identical amounts. --- ## Related - [Program Model](../program-model) -- registers, bytecode format, ordering rules, invariants - Modifiers -- fees, MEV protection, rate guards, access control - [Instruction set overview](../instruction-set-overview) -- full instruction reference https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/patterns/constant-product.md The constant product AMM is the classic `x x y = k` pool: a bidirectional, stateful strategy where takers can swap in either direction and reserves persist across swaps in SwapVM storage. It is the Aqua analog of Uniswap V2, though the internal math and fee composition differ. This is the `xyc` strategy type. It shares a single on-chain engine, the AquaSwapVMRouter, with the other archetypes (concentrated and pegged); the strategy type is selected by the instruction program, not by a separate contract. --- ## Core instructions | Instruction | Role | | -------------------- | ------------------------------------------------------------------- | | `_dynamicBalancesXD` | Load reserves from storage; persist updated reserves after the swap | | `_xycSwapXD` | Compute `x x y = k` pricing | --- ## Program ```solidity Program memory program = ProgramBuilder.init(_opcodes()); bytes memory bytecode = bytes.concat( program.build(_dynamicBalancesXD, BalancesArgsBuilder.build( dynamic([tokenA, tokenB]), dynamic([uint256(1_000e18), uint256(1_000e18)]) )), program.build(_xycSwapXD) ); ``` Initial `balanceIn` / `balanceOut` values seed the on-chain reserves. After the first swap, storage is updated; subsequent calls load the updated state. --- ## Pricing **Exact in:** `amountOut = amountIn x balanceOut / (balanceIn + amountIn)` **Exact out:** `amountIn = amountOut x balanceIn / (balanceOut amountOut)` Rounding always favors the maker: output floors, input ceilings. --- ## Liquidity paths The same program bytecode works across both liquidity paths; only `MakerTraits` changes. | Path | `useAquaInsteadOfSignature` | Balance source | | ------------------- | --------------------------- | ------------------------------ | | Signature + Dynamic | `false` | SwapVM storage per `orderHash` | | Aqua-backed | `true` | Aqua shared liquidity layer | :::info In the Aqua-backed path the maker's tokens stay in their own wallet under a revocable, per-chain, per-token allowance and move only when a taker fills atomically. The protocol holds no tokens; the shared liquidity layer tracks virtual balances as an internal counter in `Aqua.sol`. Smart-contract and approval risk still apply. ::: --- ## Optional modifiers Add any of these without changing `_xycSwapXD`. Placement relative to the swap opcode matters, so follow the order shown. | Goal | Add | | --------------------- | ---------------------------------------------- | | Swap fee (LP) | `_flatFeeAmountInXD` before `_xycSwapXD` | | Protocol fee (ERC-20) | `_protocolFeeAmountInXD` before `_xycSwapXD` | | MEV protection | `_decayXD` before the fee, before `_xycSwapXD` | | Expiry | `_deadline` before balance setup | :::info In the v1 MVP only the LP swap fee is non-zero. The `_protocolFeeAmountInXD` opcode exists in the contracts but the protocol fee is set to 0 and is not displayed. ::: --- ## Invariants to verify - **Exact in/out symmetry** -- `exactIn(X) -> Y` implies `exactOut(Y) -> X` within rounding. - **Monotonicity** -- larger swaps get equal or worse price. - **Strategy liveness** -- when one reserve is depleted, reverse swaps restore it. - **Quote/swap consistency** -- `quote()` and `swap()` return identical amounts. --- ## Related - XYCSwap -- instruction args and formula - Balances -- dynamic balance args - [Concentrated Liquidity](./concentrated-liquidity-amm) -- bounded price range variant - [Decay AMM](./decay-amm) -- same curve with MEV protection https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/patterns/concentrated-liquidity-amm.md A constant-product AMM whose liquidity is concentrated into a bounded price range `[sqrtPriceMin, sqrtPriceMax]`. Inside the range the pool prices swaps on the `x x y = k` curve; outside it the pool stops pricing. This archetype is analogous to Uniswap V3, though the internal math and fee composition differ. A concentrated strategy is one instruction program priced by SwapVM (the Aqua swap engine) and run through the single `AquaSwapVMRouter`. The archetype is selected by the program bytes below, not by a dedicated contract. :::info The "pool" here is a virtual accounting construct, not a custodial vault. Each maker's tokens stay in their own wallet under a revocable, per-chain, per-token allowance; the protocol holds no tokens and moves them only when a taker fills a swap atomically. ::: --- ## Core instructions | Instruction | Role | | -------------------------------- | ------------------------------------------------ | | `_dynamicBalancesXD` | Load reserves; persist after swap | | `_xycConcentrateGrowLiquidity2D` | Apply price-range bounds to reserves | | `_xycSwapXD` | Compute `x x y = k` pricing within bounded state | `_xycConcentrateGrowLiquidity2D` is a pre-swap modifier. It transforms `balanceIn` and `balanceOut` before the swap formula runs, and `_xycSwapXD` then operates on the adjusted reserves. --- ## Program ```solidity Program memory program = ProgramBuilder.init(_opcodes()); bytes memory bytecode = bytes.concat( program.build(_dynamicBalancesXD, BalancesArgsBuilder.build( dynamic([tokenLt, tokenGt]), dynamic([uint256(1_000e18), uint256(1_000e18)]) )), program.build(_xycConcentrateGrowLiquidity2D, XYCConcentrateArgsBuilder.build2D( sqrtPriceMin, sqrtPriceMax )), program.build(_xycSwapXD) ); ``` **Token ordering:** `tokenLt` is the token with the lower address (price denominator); `tokenGt` is the token with the higher address. P = tokenGt / tokenLt. --- ## Args (from XYCConcentrate) | Field | Size | Type | Description | | -------------- | -------- | --------- | -------------------------------------- | | `sqrtPriceMin` | 32 bytes | `uint256` | Lower bound of price range (sqrt of P) | | `sqrtPriceMax` | 32 bytes | `uint256` | Upper bound of price range (sqrt of P) | Total: 64 bytes, packed via `abi.encodePacked`. --- ## Price range behavior | Condition | Behavior | | ----------------------------------------- | --------------------------------------------------------------- | | P inside `[sqrtPriceMin, sqrtPriceMax]` | Normal swap pricing | | P at or beyond lower bound | Pool is fully in `tokenLt`; no more sells of `tokenGt` accepted | | P at or beyond upper bound | Pool is fully in `tokenGt`; no more sells of `tokenLt` accepted | --- ## Optional modifiers | Goal | Add | | -------------- | ------------------------------------------------------------ | | Flat fee | `_flatFeeAmountInXD` before `_xycConcentrateGrowLiquidity2D` | | MEV protection | `_decayXD` before the fee | | Expiry | `_deadline` before balance setup | --- ## Related - XYCConcentrate: instruction args and bounds math - XYCSwap: underlying constant-product formula - [Constant Product](./constant-product): unbounded variant - [Pegged Swap](./pegged-swap): low-slippage alternative for stable pairs https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/patterns/pegged-swap.md `PeggedSwap` is the **pegged** strategy archetype in 1inch Aqua, one of three archetypes (`xyc`, `concentrated`, `pegged`) served by a single router through SwapVM, the Aqua swap engine. It targets correlated or pegged assets such as stablecoins, wrapped tokens, and liquid staking tokens. The archetype applies a square-root linear curve that holds near-zero slippage within a narrow price band and degrades gracefully outside it. The design intent is analogous to Curve StableSwap; the internal math and fee integration differ. :::info One router (`AquaSwapVMRouter`) serves all three strategy archetypes. The archetype is selected by the program bytes shipped with the strategy, not by a separate contract address. Maker tokens stay in the maker's wallet under a revocable, per-chain, per-token allowance and move only when a taker fills a swap atomically; Aqua holds no tokens itself. ::: --- ## Core instructions | Instruction | Role | | ----------------------------- | ---------------------------------------------- | | `_dynamicBalancesXD` | Load reserves; persist after swap | | `_peggedSwapGrowPriceRange2D` | Square-root linear pricing with peg parameters | `_peggedSwapGrowPriceRange2D` is a terminal instruction. It computes both the reserve transformation and the swap amounts in one step. --- ## Program ```solidity Program memory program = ProgramBuilder.init(_opcodes()); bytes memory bytecode = bytes.concat( program.build(_dynamicBalancesXD, BalancesArgsBuilder.build( dynamic([tokenLt, tokenGt]), dynamic([uint256(1_000e18), uint256(1_000e18)]) )), program.build(_peggedSwapGrowPriceRange2D, PeggedSwapArgsBuilder.build( PeggedSwapArgsBuilder.Args({ x0: x0, y0: y0, linearWidth: linearWidth, rateLt: rateLt, rateGt: rateGt }) )) ); ``` **Token ordering:** same convention as concentrated liquidity. `tokenLt` has the lower address and `tokenGt` the higher. Price P = tokenGt / tokenLt. --- ## Args (from PeggedSwap) | Field | Size | Type | Description | | ------------- | -------- | --------- | ----------------------------------------------- | | `x0` | 32 bytes | `uint256` | Reserve offset for `tokenLt` (peg anchor) | | `y0` | 32 bytes | `uint256` | Reserve offset for `tokenGt` (peg anchor) | | `linearWidth` | 32 bytes | `uint256` | Linear-term coefficient A, scaled by 1e27 (ONE) | | `rateLt` | 32 bytes | `uint256` | Rate numerator for `tokenLt` (peg ratio) | | `rateGt` | 32 bytes | `uint256` | Rate denominator for `tokenGt` (peg ratio) | Total: 160 bytes, packed via `abi.encodePacked`. --- ## Peg ratio Set `rateLt` and `rateGt` to express the target exchange rate. For a 1:1 peg (e.g. USDC/USDT): `rateLt = 1, rateGt = 1`. For a 1:1.001 peg: `rateLt = 1000, rateGt = 1001`. `linearWidth` controls how wide the near-zero-slippage band is before the curve steepens. --- ## Optional modifiers | Goal | Add | | -------- | --------------------------------------------------------- | | Flat fee | `_flatFeeAmountInXD` before `_peggedSwapGrowPriceRange2D` | | Expiry | `_deadline` before balance setup | --- ## Related - PeggedSwap: instruction args and curve math - [Constant Product](./constant-product): volatile pair variant (`xyc`) - [Concentrated Liquidity](./concentrated-liquidity-amm): bounded price range ## Pegged curve (verified from source) The claims below are confirmed against `1inch/swap-vm` at `src/instructions/PeggedSwap.sol` and `src/libs/PeggedSwapMath.sol`. One documented value is corrected. ### Function signature (verified) ```solidity function _peggedSwapGrowPriceRange2D(Context memory ctx, bytes calldata args) internal pure ``` The name `_peggedSwapGrowPriceRange2D`, the `Context memory ctx, bytes calldata args` parameters, and the `internal pure` mutability all match source exactly. ### Args layout (verified) Packed via `abi.encodePacked` in `PeggedSwapArgsBuilder.build`; parsed by zero-copy cast in `PeggedSwapArgsBuilder.parse`, which requires `data.length >= 160` (`5 * 32 bytes`). | Name | Type | Size | Offset | Meaning (from source) | | ------------- | --------- | -------- | ------ | ------------------------------------------------------------------------------------ | | `x0` | `uint256` | 32 bytes | 0 | Initial X reserve / normalization factor (token with LOWER address) | | `y0` | `uint256` | 32 bytes | 32 | Initial Y reserve / normalization factor (token with GREATER address) | | `linearWidth` | `uint256` | 32 bytes | 64 | Linear-term coefficient `A`, scaled by `1e27` (`ONE`); must be `<= MAX_LINEAR_WIDTH` | | `rateLt` | `uint256` | 32 bytes | 96 | Decimal-scale multiplier for token with LOWER address | | `rateGt` | `uint256` | 32 bytes | 128 | Decimal-scale multiplier for token with GREATER address | **Total: 160 bytes.** Confirmed. **Scale:** `1e27`**.** Confirmed -- `PeggedSwapMath.ONE = 1e27`, chosen for precision (source: "Uses 1e27 scale ... reduces rounding error by ~10^9"). ### Curve family (verified) **Square-root linear** -- confirmed. Contract title: "Square-root linear swap curve for pegged assets". Invariant, with `u = x/X` and `v = y/Y` both scaled by `ONE`: ``` (x/X) + (y/Y) + A(x/X + y/Y) = 1 + A ``` Curvature `p = 0.5` is hardcoded (enables the closed-form quadratic solve in `PeggedSwapMath.solve`: `aw + w = c u au`, solved with the numerically stable root `w = 2R / (1 + D)`). No iterative solver is used. **One-line behavior:** the linear term `A(u+v)` flattens the curve near the 1:1 peg (higher `A` -> tighter peg, lower slippage), while the square-root terms dominate at the extremes to provide smooth price protection. :::note **Correction -- max** `linearWidth` **is 5000xONE, not 2xONE.** The parameter table and error table state a maximum of `2 x 1e27` and an error condition of `linearWidth > 2e27`. Source defines `MAX_LINEAR_WIDTH = 5000 * ONE = 5000e27` (`PeggedSwapMath.sol`). The `PeggedSwapInvalidLinearWidth` error therefore fires when `linearWidth > 5000e27`, and `A = 0` is valid (inclusive range `0` to `5000e27`). The `2e27` figure is contradicted by the in-source parameter guide, which recommends `A 100e27-300e27` for tight stablecoin pairs. ::: ### Depeg behavior (derived from source) Note that `linearWidth` is the coefficient `A` on the linear term, not a literal price "band" -- there is no explicit width boundary in the math. "Leaving the band" corresponds to the pool moving out of the linear-dominated near-peg region into the square-root-dominated regime. The invariant fixes finite reserves. At the initial anchor `u = v = 1`, the constant is `C = 2 + 2A`. Draining one side to `v = 0` gives `u* + Au* = 2 + 2A`, so the opposite normalized reserve grows to at most `u* = 4` when `A = 0`, and strictly less as `A` increases. This matches the source overflow comments (`u u* 4ONE for any A 0`). Consequently, as one token is depleted toward zero its marginal price rises steeply toward a **hard boundary**: the incoming token's reserve can grow to at most ~4x its initial normalization value (exactly 4x at `A = 0`, tighter for larger `A`), after which the pool is exhausted. Because the anchor `X/Y` is fixed at build time and reserves are finite, a genuine drifting depeg is not tracked -- the pool simply gets drained of the richer asset. This is why the curve is **not suitable for drifting-peg assets** (e.g. an ever-increasing LST exchange rate); it fits fixed or hardcoded-ratio pairs. ### Verified vs unverified - **Verified:** function name/signature/mutability; five `uint256` args in order `x0, y0, linearWidth, rateLt, rateGt`; 32 bytes each; 160-byte total; `1e27` scale; square-root linear curve family with hardcoded `p = 0.5`; maker-favorable rounding (`amountOut` down, `amountIn` up); finite-reserve hard boundary at `u* 4`. - **Corrected:** maximum `linearWidth` is `5000e27` (not `2e27`); `A = 0` is permitted. ## Worked example & scope This section walks one concrete `PeggedSwap` parameterization end to end and states what the v1.0.1 pegged strategy does and does not cover. It builds on the args layout and curve verified above; it does not restate them. ### linearWidth scale and bound `linearWidth` is the linear-term coefficient `A`, scaled by `1e27`. The scale and the upper bound come straight from source: | Constant | Value | Source | | ------------------ | ------------------------ | ---------------------------- | | `ONE` | `1e27` | `PeggedSwapMath.sol` line 12 | | `MAX_LINEAR_WIDTH` | `5000 * ONE` = `5000e27` | `PeggedSwapMath.sol` line 14 | The instruction rejects any strategy whose coefficient exceeds the bound. In `PeggedSwap.sol` the validation is: ``` require(args.linearWidth <= PeggedSwapMath.MAX_LINEAR_WIDTH, PeggedSwapInvalidLinearWidth(args.linearWidth)); ``` So the admissible range is inclusive: `0 <= linearWidth <= 5000e27`. Passing `A` as a plain integer (e.g. `100` instead of `100e27`) does not revert, but it collapses the linear term to ~zero and the pool prices as a bare square-root curve. Always pre-scale `A` by `1e27`. :::info The `NatSpec` on `linearWidth` reads: _"Linear component coefficient A scaled by 1e27 (e.g., 100e27 for A=100); must be \<= PeggedSwapMath.MAX_LINEAR_WIDTH"_. For tight stablecoin pairs the in-source guidance sits well inside the bound (order of `100e27`-`300e27`), leaving the `5000e27` ceiling as a hard limit rather than a working value. ::: ### A USDC/DAI 1:1 parameterization Take a maker quoting a 1:1 peg between USDC (6 decimals) and DAI (18 decimals), seeding roughly 1,000 of each and choosing a tight coefficient `A = 100`. The rate multipliers normalize both tokens to a common 18-decimal base, exactly as the source NatSpec example does (USDC scaled up by `1e12`, DAI left as-is): | Field | Value | How it is derived | | ------------- | ------------------------- | ---------------------------------------------------- | | `rateLt` | `1e12` | USDC (lower-address token here) scaled 6->18 decimals | | `rateGt` | `1` | DAI (higher-address token) already 18 decimals | | `x0` | `1000e6 * 1e12 = 1000e18` | USDC seed x `rateLt` | | `y0` | `1000e18 * 1 = 1000e18` | DAI seed x `rateGt` | | `linearWidth` | `100 * 1e27 = 100e27` | `A = 100`, pre-scaled by `ONE` | Which token is `tokenLt` versus `tokenGt` is fixed by raw address ordering, not by symbol. The maker assigns `rateLt` / `rateGt` and the matching `x0` / `y0` to whichever token holds the lower address; swap the assignments if DAI is the lower-address token on your chain. The instruction re-orients rates per fill (`rateIn = tokenIn < tokenOut ? rateLt : rateGt`), so the pair is symmetric once the assignment is correct. Packed into the program, the five words follow the verified 160-byte layout: ```solidity program.build(_peggedSwapGrowPriceRange2D, PeggedSwapArgsBuilder.build( PeggedSwapArgsBuilder.Args({ x0: 1000e18, // USDC seed, normalized y0: 1000e18, // DAI seed, normalized linearWidth: 100e27, // A = 100, scaled by ONE (1e27) rateLt: 1e12, // USDC 6 -> 18 decimals rateGt: 1 // DAI already 18 decimals }) )); // abi.encodePacked -> 5 x 32 bytes = 160 bytes ``` All three `require` guards in `PeggedSwap.sol` pass for these values: `x0 > 0 && y0 > 0`, `linearWidth (100e27) <= 5000e27`, and `rateLt > 0 && rateGt > 0`. ### Scope: two-token strategies only The v1.0.1 pegged instruction is strictly pairwise. `_peggedSwapGrowPriceRange2D` is a 2D instruction operating on a single `tokenLt` / `tokenGt` pair with exactly two reserves (`x0`, `y0`) and two rate multipliers. There is no native multi-asset pegged pool in this release -- a three-or-more-stablecoin basket is not expressible as one `PeggedSwap` strategy. A maker who wants basket coverage deploys independent two-token strategies (one per pair) sharing the same wallet allowances. ### Scope: a depeg is not auto-guarded :::note The pegged curve has **no oracle, price feed, or circuit breaker**. The instruction args are only `x0, y0, linearWidth, rateLt, rateGt` -- nothing references an external price. The curve prices purely from its own reserves and the fixed `rateLt` / `rateGt` ratio. If one asset depegs, the pool does not pause or reprice on its own; it keeps quoting along the square-root linear curve and gets arbitraged -- the drained-reserve boundary described above is the only intrinsic stop. ::: Bounding depeg exposure is therefore the maker's responsibility, expressed through the same build-time parameters: - `rateLt` **/** `rateGt` pin the target ratio. They are set once at build time and do not drift, so the strategy fits fixed or hardcoded-ratio pairs, not a moving exchange rate. - `linearWidth` **(**`A`**)** sets how tightly the curve hugs the peg. A larger `A` keeps quotes near 1:1 across a wider reserve range but also tightens the finite-reserve ceiling; a smaller `A` gives more room before the square-root regime dominates. - **Seed size / range** (`x0`, `y0`) caps the notional a taker can extract before the richer asset is drained. Because the maker's tokens stay in their own wallet under a revocable per-token allowance, the fastest response to a confirmed depeg is off-curve: revoke the allowance to halt fills, then rebuild the strategy with new rates if desired. The curve itself will not do this automatically. https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/patterns/decay-amm.md The Decay AMM is a constant-product (xyc) automated market maker with a Mooniswap-style virtual-balance decay modifier. You author it as a program for SwapVM (the Aqua swap engine) and register it on the Aqua router (`AquaSwapVMRouter`). After each swap, `_decayXD` temporarily widens the spread by inflating `balanceIn` and deflating `balanceOut`, then decays back to true reserves over `period` seconds. This discourages sandwich attacks by making back-runs land at a worse price immediately after the initial swap. :::info A Decay AMM is a maker strategy on Aqua: liquidity stays in the maker's wallet under a revocable, per-chain, per-token allowance and moves only when a taker fills a swap atomically. The Aqua registry holds no tokens of its own. ::: --- ## Core instructions A Decay AMM strategy composes three instructions from the SwapVM instruction set: | Instruction | Role | | -------------------- | ---------------------------------------------------------------------- | | `_dynamicBalancesXD` | Load reserves; persist after swap | | `_decayXD` | Apply virtual offset; call `ctx.runLoop()` for subsequent instructions | | `_xycSwapXD` | Compute `x x y = k` pricing inside the decay loop | `_decayXD` is a wrapping instruction; it calls `ctx.runLoop()` internally, so the swap formula executes inside its nested loop. Place it **before** `_xycSwapXD`. --- ## Program ```solidity Program memory program = ProgramBuilder.init(_opcodes()); bytes memory bytecode = bytes.concat( program.build(_dynamicBalancesXD, BalancesArgsBuilder.build( dynamic([tokenA, tokenB]), dynamic([uint256(1_000e18), uint256(1_000e18)]) )), program.build(_decayXD, DecayArgsBuilder.build(300)), // 300-second decay period program.build(_xycSwapXD) ); ``` --- ## Args (from Decay) | Field | Size | Type | Description | | -------- | ------- | -------- | ----------------------------------------------------- | | `period` | 2 bytes | `uint16` | Seconds until the virtual offset fully decays to zero | --- ## How decay works 1. After a swap, the storage records `(amountIn, amountOut, timestamp)` of the last swap. 2. On the next quote/swap, `_decayXD` computes elapsed time since the last swap. 3. If `elapsed < period`, it applies a proportional virtual offset: `balanceIn` is inflated and `balanceOut` is deflated by `offset x (1 elapsed/period)`. 4. The offset is largest immediately after a swap and approaches zero as `elapsed -> period`. This means a sandwich attacker's back-run sees a wider spread proportional to how quickly it follows the victim swap. --- ## Program position ``` _dynamicBalancesXD -> _decayXD -> [fee] -> _xycSwapXD ``` If adding a fee, place it between `_decayXD` and `_xycSwapXD`; both are wrapping instructions and will nest correctly. ``` _dynamicBalancesXD -> _decayXD -> _flatFeeAmountInXD -> _xycSwapXD ``` See Modifiers for the full wrapping order example. --- ## Optional modifiers | Goal | Add | | -------- | -------------------------------------------------------- | | Flat fee | `_flatFeeAmountInXD` between `_decayXD` and `_xycSwapXD` | | Expiry | `_deadline` before balance setup | --- ## Related - Decay -- instruction args and virtual offset math - XYCSwap -- constant-product formula - [Constant Product](./constant-product) -- same curve without MEV protection - Modifiers -- wrapping instruction ordering rules https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/patterns/limit-order.md Limit Order is a fixed-rate, single-direction swap program. The maker embeds a price ratio as static balances, and takers fill at exactly that rate. The program supports one-shot execution (bitmap invalidator) or partial fills up to a cap (token-out invalidator). :::info Limit Order runs on the 1inch Limit Order Protocol router, not the Aqua router. It shares one backend with Fusion; the two differ only by Fusion's Dutch-auction price modifier. The order itself is a SwapVM instruction program that composes Balances (static), then LimitSwap, then an Invalidator. It is self-custodial. The maker's tokens stay in the maker's wallet under a revocable, per-chain, per-token allowance, the protocol holds no tokens, and a fill moves tokens atomically at the fixed rate. ::: --- ## Core instructions | Instruction | Role | | ---------------------- | -------------------------------------------------- | | `_staticBalancesXD` | Embed fixed `balanceIn` / `balanceOut` price ratio | | `_limitSwap1D` | Compute `amountIn` / `amountOut` at the fixed rate | | `_limitSwapOnlyFull1D` | Same, but reverts on partial fills | --- ## Program A: one-time execution (bitmap invalidator) Fills at most once. The bitmap slot `123` is flipped atomically with the swap; a second call reverts. ```solidity Program memory program = ProgramBuilder.init(_opcodes()); bytes memory bytecode = bytes.concat( program.build(_invalidateBit1D, InvalidatorsArgsBuilder.buildInvalidateBit(123)), program.build(_staticBalancesXD, BalancesArgsBuilder.build( dynamic([tokenA, tokenB]), dynamic([uint256(1000e18), uint256(2000e18)]) )), program.build(_limitSwap1D, LimitSwapArgsBuilder.build(tokenA, tokenB)) ); ``` **Why** `_invalidateBit1D` **first:** it must run before amounts are computed; it writes state at the start so a swap that later reverts still flips the bit (preventing replay on retry). --- ## Program B: partial fills (token-out invalidator) Multiple takers can fill against the same order until `balanceOut` is exhausted. Each fill decrements the on-chain counter. ```solidity Program memory program = ProgramBuilder.init(_opcodes()); bytes memory bytecode = bytes.concat( program.build(_staticBalancesXD, BalancesArgsBuilder.build( dynamic([tokenA, tokenB]), dynamic([uint256(1000e18), uint256(2000e18)]) )), program.build(_limitSwap1D, LimitSwapArgsBuilder.build(tokenA, tokenB)), program.build(_invalidateTokenOut1D) ); ``` **Why** `_invalidateTokenOut1D` **last:** it needs `amountOut > 0`, which `_limitSwap1D` sets. It reverts if the cumulative fill would exceed `balanceOut`. --- ## Optional modifiers Add modifiers without changing the core swap formula. Placement matters; follow the ordering shown in the table below. | Goal | Add | | ----------------- | ------------------------------------------------------- | | Expiry | `_deadline` before balance setup | | Maker fee | `_flatFeeAmountInXD` before `_limitSwap1D`, wrapping it | | Gas compensation | `_baseFeeAdjuster1D` after `_limitSwap1D` | | Oracle anchor | `_oraclePriceAdjuster1D` after `_limitSwap1D` | | Hard price floor | `_requireMinRate1D` after `_limitSwap1D` | | Holder-only fills | `_onlyTakerTokenBalanceNonZero` before balance setup | --- ## Invariants to verify - **Quote/swap consistency:** `quote()` and `swap()` must return the same amounts. - **Rounding:** `amountIn` rounds up (ceil), `amountOut` rounds down (floor); the maker is never short. - **No replay:** the bitmap invalidator blocks re-execution; the token-out invalidator blocks overfill. - **Balance sufficiency:** `amountOut <= balanceOut` must hold; revert otherwise. --- ## Related - LimitSwap: instruction args and behavior - Invalidators: bitmap and token-amount invalidation - [Dutch Auction](./dutch-auction): the same program plus a time-decaying price modifier, which is what turns a limit order into a Fusion order - [RFQ](./rfq-request-for-quote): off-chain quoted price using the same instruction set https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/patterns/rfq-request-for-quote.md An RFQ (request for quote) order is a limit order whose price is quoted off-chain by the maker, typically a market maker responding to a quote request, and signed for a one-shot fill. It uses the same instruction set as a standard limit order with a bitmap invalidator; the distinction is operational, not structural. :::info RFQ orders are part of the 1inch Limit Order Protocol. They are quoted and signed off-chain and filled through the Aggregation Router and the Limit Order Protocol, not through Aqua's AMM pools. The program is encoded with SwapVM, the shared swap engine, but the opcodes shown below (bitmap invalidation via Invalidators) belong to the limit-order opcode set, which runs on a separate router from the Aqua AMM router (AquaSwapVMRouter). ::: --- ## How it differs from a limit order | Aspect | Limit order | RFQ | | ------------ | --------------------------------- | --------------------------------------------------- | | Price source | Maker sets rate at order creation | Market maker quotes on demand | | Fill model | May sit open for hours or days | Intended for immediate single fill | | Expiry | Optional `_deadline` | Short `_deadline` required (price is stale quickly) | | Invalidation | Bitmap or token-out | Bitmap only (one-shot) | --- ## Program ```solidity Program memory program = ProgramBuilder.init(_opcodes()); bytes memory bytecode = bytes.concat( program.build(_deadline, ControlsArgsBuilder.buildDeadline(block.timestamp + 30)), program.build(_invalidateBit1D, InvalidatorsArgsBuilder.buildInvalidateBit(nonce)), program.build(_staticBalancesXD, BalancesArgsBuilder.build( dynamic([tokenIn, tokenOut]), dynamic([uint256(quoteAmountIn), uint256(quoteAmountOut)]) )), program.build(_limitSwap1D, LimitSwapArgsBuilder.build(tokenIn, tokenOut)) ); ``` Instruction order matters. `_deadline` runs first, rejecting stale quotes before any state is touched. `_invalidateBit1D` runs second, providing replay protection before amounts are computed. The short deadline (30 seconds in the example) prevents a quote from being held and replayed later at an advantageous moment. --- ## Optional modifiers | Goal | Add | | ------------------ | ------------------------------------------------- | | Holder-gated fills | `_onlyTakerTokenBalanceNonZero` after `_deadline` | | Protocol fee | `_protocolFeeAmountInXD` before `_limitSwap1D` | --- ## Related - [Limit Order](./limit-order): general limit order patterns - Controls: `_deadline` args and behavior - Invalidators: bitmap invalidation https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/patterns/dutch-auction.md A Dutch auction is a limit order whose effective price improves over time until a taker fills it. It is built by placing a balance-decay modifier before the swap formula: `_dutchAuctionBalanceIn1D` shrinks `balanceIn` (the maker gives more in), so the order grows progressively cheaper for the taker. The maker's tokens stay in their wallet until a taker fills the order. Programs are encoded for SwapVM, the shared swap engine; this Dutch auction archetype belongs to the 1inch Aqua Limit Order and Fusion product, not to the Aqua AMM router. :::info Dutch Auction is a Limit Order archetype. The instructions on this page (the `_dutchAuction*` balance modifiers, `_limitSwap1D`, and the `_invalidate*` guards) belong to the limit-order opcode set and run on the limit-order router. They are separate from the Aqua AMM router opcode set (`Controls, XYCSwap, XYCConcentrate, Decay, Fee, PeggedSwap, Extruction`). ::: --- ## Core instructions | Instruction | Role | | --------------------------- | ------------------------------------------------- | | `_staticBalancesXD` | Embed starting balances | | `_dutchAuctionBalanceIn1D` | Decay `balanceIn` over time (taker pays less) | | `_dutchAuctionBalanceOut1D` | Grow `balanceOut` over time (taker receives more) | | `_limitSwap1D` | Compute amounts at the decayed rate | Use `_dutchAuctionBalanceIn1D` **or** `_dutchAuctionBalanceOut1D` (or both) depending on which side you want to move. --- ## Program: balanceIn decay `balanceIn` decays from its initial value toward zero over `duration`. The effective input required from the taker decreases, making the order cheaper as time passes. ```solidity Program memory program = ProgramBuilder.init(_opcodes()); bytes memory bytecode = bytes.concat( program.build(_staticBalancesXD, BalancesArgsBuilder.build( dynamic([tokenA, tokenB]), dynamic([uint256(1000e18), uint256(2000e18)]) )), program.build(_dutchAuctionBalanceIn1D, DutchAuctionArgsBuilder.build( startTime, // uint40 -- auction start (unix seconds) duration, // uint16 -- seconds until full decay decayFactor // uint64 -- decay multiplier (BPS = 1e9) )), program.build(_limitSwap1D, LimitSwapArgsBuilder.build(tokenA, tokenB)), program.build(_invalidateTokenOut1D) ); ``` **Instruction ordering:** decay instructions are pre-swap modifiers. Place them **after** `_staticBalancesXD` and **before** `_limitSwap1D`. --- ## Decay direction reference | Instruction | Effect | Taker outcome | | --------------------------- | -------------------------------- | ---------------------------------- | | `_dutchAuctionBalanceIn1D` | `balanceIn` decreases over time | Taker pays less as time passes | | `_dutchAuctionBalanceOut1D` | `balanceOut` increases over time | Taker receives more as time passes | --- ## Args (from DutchAuction) | Field | Size | Type | Description | | ------------- | ------- | -------- | ----------------------------- | | `startTime` | 5 bytes | `uint40` | Auction start timestamp | | `duration` | 2 bytes | `uint16` | Seconds until decay completes | | `decayFactor` | 8 bytes | `uint64` | Rate of decay; BPS = 1e9 | Total: 15 bytes. --- ## Optional modifiers | Goal | Add | | --------------------- | -------------------------------------------- | | Expiry at auction end | `_deadline` before balance setup | | One-shot fill | `_invalidateBit1D` before balance setup | | Partial fills | `_invalidateTokenOut1D` after `_limitSwap1D` | | Hard price floor | `_requireMinRate1D` after `_limitSwap1D` | --- ## Related - DutchAuction: instruction args and decay math - [Limit Order](./limit-order): base pattern without time decay - [TWAP / DCA](./twap-dca): scheduled drip variant https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/patterns/twap-dca.md Time-Weighted Average Price (TWAP) and Dollar-Cost Averaging (DCA) are order archetypes that release liquidity over a fixed schedule. They are authored as SwapVM programs (SwapVM is the Aqua swap engine) and execute on the limit-order router, not on the Aqua swap router. Both are implemented by the `_twap` instruction, which combines a linear unlock schedule with a Dutch auction decay on each interval's sub-order. A TWAP sells a fixed total amount evenly over `duration`. DCA is the same program executed periodically, where each execution fills the currently unlocked tranche. :::info The maker's tokens stay in their own wallet under a revocable, per-chain, per-token allowance and are pulled per tranche only as fills occur. Fills are not guaranteed: a tranche can be skipped when it is illiquid, and the schedule expresses intended pacing, not a promised outcome. ::: --- ## Core instruction | Instruction | Role | | ----------- | ------------------------------------------------------------------------------------- | | `_twap` | Manages the unlock schedule, the Dutch auction per tranche, and internal invalidation | `_twap` is a terminal instruction. It replaces the balance-and-swap formula combination used in ordinary limit orders. --- ## Program ```solidity Program memory program = ProgramBuilder.init(_opcodes()); bytes memory bytecode = bytes.concat( program.build(_twap, TWAPArgsBuilder.build(TwapArgs({ balanceIn: totalAmountIn, // Total input to sell over duration balanceOut: totalAmountOut, // Total output expected startTime: startTime, // uint40 -- start of schedule duration: duration, // uint40 -- total schedule length (seconds) priceBumpAfterIlliquidity: bumpBps, // uint64 -- price improvement after a missed interval minTradeAmountOut: minOut // uint256 -- minimum output per tranche (dust guard) }))) ); ``` `_twap` encodes args via `abi.encode` (6 x 32 = 192 bytes). --- ## Args (from TWAPSwap) | Field | Type | Description | | --------------------------- | --------- | --------------------------------------------------------------- | | `balanceIn` | `uint256` | Total input amount over full duration | | `balanceOut` | `uint256` | Total expected output | | `startTime` | `uint40` | Schedule start (unix seconds) | | `duration` | `uint40` | Total schedule length in seconds | | `priceBumpAfterIlliquidity` | `uint64` | BPS rate improvement after a missed slot (BPS = 1e9) | | `minTradeAmountOut` | `uint256` | Minimum output per tranche; reverts if the tranche is too small | --- ## How it works 1. At execution time, `_twap` computes the currently unlocked fraction of `balanceIn` and `balanceOut` based on elapsed time. 2. It runs a Dutch auction within the current tranche. The price improves within the slot while no fill has occurred. 3. `priceBumpAfterIlliquidity` makes the next tranche more attractive when the previous one was skipped. 4. Internal invalidation tracks cumulative fills; no separate invalidator instruction is needed. --- ## DCA usage To implement periodic DCA: - Set `duration` to the total campaign length (for example, 30 days). - Each execution fills the tranche for the current time slot. - The maker signs once; the program handles scheduling automatically. --- ## Optional modifiers | Goal | Add | | ---------------------- | ---------------------------------------------- | | Expiry | `_deadline` before `_twap` | | Holder-gated execution | `_onlyTakerTokenBalanceNonZero` before `_twap` | --- ## Related - TWAPSwap: instruction args and internal schedule math - [Limit Order](./limit-order): simpler fixed-rate pattern - [Dutch Auction](./dutch-auction): time-decay on a single tranche https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/patterns/conditional-access.md Conditional access turns a swap into a permissioned one: a strategy can require the taker to hold a specific token or NFT before a fill is allowed. The access guard runs as the first instruction in the SwapVM program (SwapVM is the Aqua swap engine), so an ineligible taker is rejected and the transaction reverts before any pricing logic executes. Makers stay open and permissionless. Only takers are gated, and the gate is a per-strategy opcode rather than a protocol-wide switch. :::info **Deployed reality.** The launch taker gate is the `_onlyTxOriginTokenBalanceNonZero(token)` opcode in the `Controls` set on the deployed `AquaSwapVMRouter`. It reverts `TxOriginTokenBalanceIsZero` unless `balanceOf(tx.origin) > 0`. In the 1inch Aqua dApp, the program assembler applies `withTxOriginAccessToken(aquaKycToken)` to every strategy, so at launch each fill requires the taker to hold the Aqua `KycNFT` access credential, and all fills route through KYB-verified 1inch Resolvers. ::: **Use cases:** institutional whitelisting, NFT-gated liquidity, loyalty-token holders, and DAO-member-only fills. These apply when contracts are called directly and a maker sets a custom token or NFT as the gate. --- ## How the gate works There are two ways a fill reaches the gate: - **In the dApp:** the assembler applies `withTxOriginAccessToken(aquaKycToken)` to every strategy, so the taker must hold the Aqua `KycNFT` (the taker access credential, symbol `RES`, reused from Fusion). Fills are routed only through 1inch Resolvers, which are KYB-verified. - **Direct contract calls:** a maker can require the taker to hold any specific token or NFT. A Conditional Access rule can gate on a non-zero balance (hold any balance, or at least one NFT), a minimum balance, or a minimum share of total supply. The check runs on-chain first, ahead of pricing. The guard reads `tx.origin`, not `msg.sender`. As a result, smart-contract wallets and ERC-4337 accounts cannot currently pass the gate as takers, because they execute through a contract, so `msg.sender` differs from `tx.origin`. An MPC wallet signs as an externally owned account, so `tx.origin` is that EOA and it can pass the gate. --- ## Guard instruction reference The launch gate deployed on `AquaSwapVMRouter` is a single opcode in the `Controls` set: | Instruction | Args | Check | | ---------------------------------- | ------------------ | --------------------------------------------------------------------- | | `_onlyTxOriginTokenBalanceNonZero` | `token (20 bytes)` | `balanceOf(tx.origin) > 0`, else reverts `TxOriginTokenBalanceIsZero` | When the contracts are called directly, a Conditional Access rule can also gate takers by a minimum balance or a minimum share of total supply. These rule types come from the `Controls` source, with the arg encodings below. The on-chain check resolves the taker via `tx.origin`. | Instruction | Args | Check | | ------------------------------- | ------------------------------------------------- | -------------------------------------------- | | `_onlyTakerTokenBalanceGte` | `token (20 bytes) + minAmount (32 bytes)` | `balanceOf(taker, token) >= minAmount` | | `_onlyTakerTokenSupplyShareGte` | `token (20 bytes) + minShareE18 (8 bytes uint64)` | `balanceOf(taker) / totalSupply >= minShare` | At launch the dApp applies only the non-zero balance gate, against the Aqua `KycNFT`. The minimum-balance and supply-share rule types are available when the contracts are called directly. --- ## Program: token-holder gate on a strategy In the dApp, the SwapVM SDK (`@1inch/swap-vm-sdk`) attaches the KYC gate for you: ``` // @1inch/swap-vm-sdk (dApp path) // Applied to every strategy by the assembler: program.withTxOriginAccessToken(aquaKycToken); ``` At the opcode level, the guard is simply the first instruction, ahead of balance setup and pricing. The underlying strategy can be any Aqua strategy (XYCSwap, XYCConcentrate, or PeggedSwap): ```solidity Program memory program = ProgramBuilder.init(_opcodes()); bytes memory bytecode = bytes.concat( program.build(_onlyTxOriginTokenBalanceNonZero, gateToken), // guard first (token: 20 bytes) program.build(_staticBalancesXD, BalancesArgsBuilder.build( dynamic([tokenIn, tokenOut]), dynamic([uint256(1_000e18), uint256(2_000e18)]) )), strategyBytecode // any Aqua strategy program (XYCSwap / XYCConcentrate / PeggedSwap) ); ``` **Guard first:** the access instruction must precede balance setup so unauthorized callers are rejected before any state is touched. --- ## Not available on-chain: per-strategy taker allow-lists The source includes per-strategy taker allow-list opcodes, `_whitelistSingleTaker` and `_whitelistMultipleTakers`, which would let a maker name specific taker addresses on a strategy. These are **not registered on-chain today** and should be treated as not available rather than a shipped feature. --- ## Combining with any underlying strategy Access guards compose with any program. The gate runs before the underlying strategy (after any expiry check): ``` _deadline <- expiry first _onlyTxOriginTokenBalanceNonZero <- then the taker gate _dynamicBalancesXD <- then balance setup _flatFeeAmountInXD <- wrapping fee _xycSwapXD <- formula inside fee loop ``` --- ## Related - Controls: full args for the access instructions - [Constant Product](./constant-product): an underlying XYC (AMM) strategy https://business.1inch.com/portal/assets/docs-v2/aqua/swapvm/patterns/best-route-selector.md The **Best Route Selector** is an Advanced preset that evaluates multiple sub-strategy branches at execution time and runs the one that produces the best output for the taker. It is a composition pattern built on the `Extruction` instruction of SwapVM (the Aqua swap engine), not a registered core opcode of its own. At runtime, `_extruction` delegates to an external selector contract that runs each branch's `quote()` and returns the winning branch. This pattern lets a single signed order expose multiple AMM curves (or any mix of strategies) and automatically route to the best one for current market conditions. It is one of the Advanced presets, alongside Conditional Access. :::info Best Route Selector is a preset program, not a core opcode. It is assembled from the deployed `Extruction` instruction in the SwapVM opcode set; there is no dedicated selector opcode registered on-chain. ::: > **Warning:** Programs with `_extruction` and conditional branches are the most complex to reason about. Test every branch path independently before production use. --- ## Underlying instruction Best Route Selector is not itself a core opcode; it reuses the `Extruction` core instruction to delegate branch selection. | Instruction | Role | | ------------- | ---------------------------------------------------------- | | `_extruction` | Delegate to external contract with packed branch bytecodes | --- ## Program: XYC vs Pegged selector ```solidity Program memory program = ProgramBuilder.init(_opcodes()); bytes memory strategy1 = program.build(_xycSwapXD); bytes memory strategy2 = program.build( _peggedSwapGrowPriceRange2D, PeggedSwapArgsBuilder.build(PeggedSwapArgsBuilder.Args({ x0: 50e18, y0: 50e18, linearWidth: 0.02e9, rateLt: 1, rateGt: 1 })) ); bytes memory selectorArgs = abi.encodePacked( address(bestRouteSelectorTarget), // External selector contract uint8(2), // Number of branches uint16(strategy1.length), strategy1, uint16(strategy2.length), strategy2 ); bytes memory bytecode = bytes.concat( program.build(_dynamicBalancesXD, BalancesArgsBuilder.build( dynamic([tokenA, tokenB]), dynamic([uint256(100e18), uint256(100e18)]) )), program.build(_extruction, selectorArgs) ); ``` Reference test: `test/RunLoop.t.sol` -> `test_BestRouteSelector_XYC_vs_Pegged`. --- ## How it works 1. `_extruction` calls `bestRouteSelectorTarget` with the packed branch data. 2. The selector contract runs `quote()` for each branch against the current state. 3. It returns the branch index with the best output. 4. `_extruction` executes only the winning branch via `ctx.runLoop()`. --- ## `_extruction` args format ``` [target: 20 bytes][N: 1 byte uint8][branch_0_len: 2 bytes][branch_0: N bytes]...[branch_N_len][branch_N] ``` - `target`: address of the external selector implementing `IExtruction` or `IStaticExtruction`. - `N`: number of branches. - Each branch: `uint16` length prefix followed by the branch bytecode. --- ## Invariants to verify - **Branch determinism:** same inputs always select the same branch (no randomness or block-state dependence unless intentional). - **Quote/swap path consistency:** `quote()` and `swap()` must select the same branch and return identical amounts. - **All branches individually safe:** each sub-program must satisfy its own invariants (monotonicity, rounding, balance sufficiency). - **No dead paths:** every branch must be reachable and must terminate correctly. - **Authorization correctness:** the selector contract must not be manipulable by the taker. - **Non-upgradeable selector:** the external selector target must be non-upgradeable, so its pricing logic cannot change between quote and fill. --- ## Related - Extruction: instruction interface and args - [Constant Product](./constant-product): XYC branch - [Pegged Swap](./pegged-swap): Pegged branch - [Program Model](../program-model): ordering rules and invariants https://business.1inch.com/portal/assets/docs-v2/aqua/reference/smart-contract.md Complete function reference for `Aqua.sol` (the Aqua registry) and `AquaApp` (the base contract that swap applications inherit). In production, the deployed `AquaSwapVMRouter` is the `AquaApp` that drives fills against strategies registered here. It is the router for SwapVM, the Aqua swap engine. --- ## Aqua.sol registry ### Liquidity lifecycle #### `ship()` ```solidity function ship( address app, bytes calldata strategy, address[] calldata tokens, uint256[] calldata amounts ) external returns (bytes32 strategyHash); ``` Registers a strategy and allocates its virtual balances so it can be filled. No tokens move: only the maker's ERC-20 allowance is consumed. The tokens stay in the maker's wallet under a revocable, per-chain, per-token allowance. | Parameter | Description | | ---------- | ------------------------------------------------------------- | | `app` | The `AquaApp` contract governing swap logic for this strategy | | `strategy` | ABI-encoded strategy struct (shape defined by the AquaApp) | | `tokens` | Ordered list of token addresses to allocate | | `amounts` | Virtual amounts to allocate per token (parallel to `tokens`) | Returns the `strategyHash`, computed as `keccak256(abi.encode(strategy))`. Emits `Shipped(maker, app, strategyHash, strategy)`, plus one `Pushed` event per token for the initial allocation. --- #### `dock()` ```solidity function dock( address app, bytes32 strategyHash, address[] calldata tokens ) external; ``` Revokes the strategy's virtual balances and stops it from accepting swaps. No tokens move, because the balances were always held in the maker's wallet. | Parameter | Description | | -------------- | ----------------------------------------------------------- | | `app` | The `AquaApp` contract this strategy belongs to | | `strategyHash` | Hash identifying the strategy to deactivate | | `tokens` | All tokens to zero out (must match tokens used at `ship()`) | Emits `Docked(maker, app, strategyHash)`. --- ### Swap execution > These functions are called **only by AquaApp contracts during swap execution**, such as the production `AquaSwapVMRouter`. Makers and takers never call them directly. A taker's fill is atomic. On the router, `swap()` performs a `pull()` followed by a `push()` in a single transaction, all or nothing. `quote()` is a static simulation that returns the same amounts without moving any tokens. #### `pull()` ```solidity function pull( address maker, bytes32 strategyHash, address token, uint256 amount, address to ) external; ``` Transfers output tokens from the maker's wallet to the taker and decrements the virtual balance. | Parameter | Description | | -------------- | ---------------------------------------- | | `maker` | Maker whose tokens are being transferred | | `strategyHash` | Strategy the swap is executing against | | `token` | Token to transfer (the output token) | | `amount` | Amount to transfer | | `to` | Recipient (usually the taker) | Reverts if the maker's real wallet balance is insufficient. Emits `Pulled(maker, app, strategyHash, token, amount)`. --- #### `push()` ```solidity function push( address maker, address app, bytes32 strategyHash, address token, uint256 amount ) external; ``` Transfers input tokens from the taker into the maker's wallet and increments the virtual balance, so received tokens immediately add to the strategy's available liquidity. | Parameter | Description | | -------------- | -------------------------------------- | | `maker` | Maker receiving the tokens | | `app` | AquaApp contract calling push | | `strategyHash` | Strategy the swap is executing against | | `token` | Token to transfer (the input token) | | `amount` | Amount to transfer | Emits `Pushed(maker, app, strategyHash, token, amount)`. Any accrued LP swap fees auto-compound here into the maker's balance. --- ### Queries #### `rawBalances()` ```solidity function rawBalances( address maker, address app, bytes32 strategyHash, address token ) external view returns (uint248 balance, uint8 tokensCount); ``` Returns the raw on-chain storage value. Does **not** validate that `token` belongs to the strategy. Use for debugging or querying arbitrary slots. | Return | Description | | ------------- | ------------------------------------------------------------- | | `balance` | Virtual balance in token base units | | `tokensCount` | Number of tokens registered in this strategy at `ship()` time | --- #### `safeBalances()` ```solidity function safeBalances( address maker, address app, bytes32 strategyHash, address token0, address token1 ) external view returns (uint256 balance0, uint256 balance1); ``` Returns balances for two tokens with active strategy validation. Reverts if either token was not registered in the strategy at `ship()` time. Use in production integrations and before executing swaps. --- ## AquaApp base contract Base contract for all swap applications. Inherit to build AMMs, limit orders, auctions, and other strategy types. ```solidity // Immutable reference to the Aqua registry IAqua public immutable AQUA; // Transient reentrancy locks per (maker, strategyHash) mapping(address maker => mapping(bytes32 strategyHash => TransientLock)) internal _reentrancyLocks; ``` --- ### `nonReentrantStrategy` modifier ```solidity modifier nonReentrantStrategy(address maker, bytes32 strategyHash); ``` Locks the `(maker, strategyHash)` pair using transient storage for the duration of a swap. Prevents the callback pattern from being re-entered mid-swap. Two different strategies owned by the same maker can execute concurrently; the same `(maker, strategyHash)` pair cannot. --- ### `_safeCheckAquaPush()` ```solidity function _safeCheckAquaPush( address maker, bytes32 strategyHash, address token, uint256 expectedBalance ) internal view; ``` Verifies that the taker deposited the expected input tokens by comparing the current virtual balance against `expectedBalance`. Requires the `nonReentrantStrategy` modifier to be active, since it relies on the locked state to detect the push. Used in the callback-based swap pattern. See [Build an AquaApp](../getting-started/build-an-aquaapp) for usage. --- ## Events | Event | Signature | Emitted by | | --------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | `Shipped` | `Shipped(address maker, address app, bytes32 strategyHash, bytes strategy)` | `ship()` | | `Docked` | `Docked(address maker, address app, bytes32 strategyHash)` | `dock()` | | `Pulled` | `Pulled(address maker, address app, bytes32 strategyHash, address token, uint256 amount)` | `pull()` | | `Pushed` | `Pushed(address maker, address app, bytes32 strategyHash, address token, uint256 amount)` | `push()` | | `Swapped` | `Swapped(bytes32 orderHash, address maker, address taker, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut)` | `AquaSwapVMRouter.swap()` | `Shipped`, `Docked`, `Pulled`, and `Pushed` are emitted by the Aqua registry. In the `Pulled` event, `app` is the calling AquaApp (`msg.sender`), since `pull()` takes no `app` argument. `Swapped` is emitted by the router on each fill, where `Swapped.orderHash` equals the `strategyHash` from `Shipped`. No parameters on `Swapped` are indexed. --- ## Strategy hash formula ```solidity bytes32 strategyHash = keccak256(abi.encode(strategy)); ``` The hash encodes all strategy parameters. The same inputs always produce the same hash, and any parameter change produces a different hash, which enforces immutability at the identity level. --- ## Related - [Build an AquaApp](../getting-started/build-an-aquaapp): implement a swap function using pull and push. - [Liquidity provider & taker guide](../getting-started/liquidity-provider-and-taker-guide): approve, ship, dock, and swap in Solidity. - [Strategy](../liquidity-layer/strategy): how programs encode into strategies registered on Aqua. - [Virtual Balances](../liquidity-layer/virtual-balances): on-chain storage layout and accounting model. - [Contract Addresses](./contract-addresses): deployed addresses, uniform across the 13 supported chains. :::note **Taker access gate (checked at swap time).** At launch every dApp strategy carries the Controls opcode `_onlyTxOriginTokenBalanceNonZero`, which reverts `TxOriginTokenBalanceIsZero` unless `balanceOf(tx.origin) > 0`. It is evaluated at _swap time, not ship time_, so a strategy can be live yet untradeable until a permitted taker holds the credential. Because it reads `tx.origin`, smart-contract wallets, multisigs and ERC-4337 bundlers cannot pass it as takers today. Permitted takers at launch are KYB-verified 1inch Resolvers. See [Access & resolvers](../liquidity-layer/access-resolvers-and-pathfinder). ::: https://business.1inch.com/portal/assets/docs-v2/aqua/reference/events-and-interfaces.md source-verified This page is the canonical reference for the Aqua smart-contract event log and its Solidity interfaces. Every signature, event, and error below is copied verbatim from source: the registry surface from `1inch/aqua` (`src/interfaces/IAqua.sol`, `src/Aqua.sol`, `src/AquaApp.sol`) and the router/swap surface from `1inch/swap-vm` (`src/SwapVM.sol`, `src/interfaces/ISwapVM.sol`, `src/interfaces/ITakerCallbacks.sol`). :::info **Two contracts, two event streams.** The Aqua _registry_ (`AquaRouter`, canonical deployment `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a`) emits `Shipped` / `Docked` / `Pulled` / `Pushed` for balance accounting. The _swap router_ (`AquaSwapVMRouter` v1.0.2, `0x111111338c5091e8440b67b168bae16a668ac0de`) emits `Swapped` for executed swaps -- and, in the current build only, `ProtocolFeeSkipped`. They are separate addresses; index them independently. (These are the 2026-07 vanity deployments; the previous `0xe8026b...139b` / `0x016b41...b070` pair is superseded -- see [Contract addresses](/portal/documentation/aqua/reference/contract-addresses).) ::: ## Events :::note **No event on Aqua uses**`indexed`**parameters.** Every field of `Shipped`, `Docked`, `Pulled`, `Pushed` -- and `Swapped` -- is a _data_ field, not a _topic_. Consequently each log carries exactly one topic: `topics[0]` = the event's signature hash. You cannot pre-filter by maker, app, or token at the RPC `eth_getLogs` level. Filter by `{ address, topics[0] }` and ABI-decode _all_ fields out of the `data` blob, then match maker/app/token client-side. ::: | Event | Canonical signature (hash source) | Indexed params | Emitted by | topic0 = keccak256(signature) | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------- | | `Shipped` | `Shipped(address,address,bytes32,bytes)` (maker, app, strategyHash, strategy) | None -- all 4 fields in `data` | Aqua registry, on `ship()` | `0xdc3622e06fb145651f567d421c9ef261d71d43e3778b761907bc0d70d42e52b0` | | `Docked` | `Docked(address,address,bytes32)` (maker, app, strategyHash) | None -- all 3 fields in `data` | Aqua registry, on `dock()` | `0xd173a1d140c154eb1ce9298d251d5eb8c4089cc2d16e70f1067bdc810c6fe004` | | `Pulled` | `Pulled(address,address,bytes32,address,uint256)` (maker, app, strategyHash, token, amount) | None -- all 5 fields in `data` | Aqua registry, on `pull()` | `0x3ad61047071575417c75e3311e5d46ff042e292b5dd8769ff18b4b254098ca7a` | | `Pushed` | `Pushed(address,address,bytes32,address,uint256)` (maker, app, strategyHash, token, amount) | None -- all 5 fields in `data` | Aqua registry, on `push()` | `0x3f18354abbd5306dd1665c2c90f614a4559e39dd620d04fbe5458e613b6588f3` | | `Swapped` | `Swapped(bytes32,address,address,address,address,uint256,uint256)` (orderHash, maker, taker, tokenIn, tokenOut, amountIn, amountOut) | None -- all 7 fields in `data` | Swap router (`SwapVM`), on `swap()` | `0x54bc5c027d15d7aa8ae083f994ab4411d2f223291672ecd3a344f3d92dcaf8b2` | | `ProtocolFeeSkipped` | `ProtocolFeeSkipped(bytes32,address,address,uint256)` (orderHash, token, to, amount) | None -- all 4 fields in `data` | Swap router, on `swap()` when an Aqua protocol fee could not be collected | `0x0b295783a78ac3079d7d7eafbca862bd69a8961a5e6e5115ab301dcbc0536e97` | :::info These `topic0` values were computed as keccak256 of the exact canonical signature strings in the second column (parameter names stripped, no spaces). They are verifiable independently: the same routine reproduces the well-known `Transfer(address,address,uint256)` hash `0xddf252ad...523b3ef`. If you prefer to recompute, hash the exact strings shown -- e.g. `cast keccak "Shipped(address,address,bytes32,bytes)"`. ::: :::note **Correction to an earlier draft:** `Pulled` has **five** fields -- `(maker, app, strategyHash, token, amount)` -- and does **not** carry a `to` recipient field in the event. The recipient is an argument to the `pull(...)` function but is not logged. Any decoder or ABI that adds a sixth `to` field to `Pulled` is wrong and will misalign the `amount` word. ::: :::note `ProtocolFeeSkipped` **exists only on the current router build** (`0x111111338c...c0de`, deployed 2026-07-26 from the `v1.0.2` git tag, declared in `src/instructions/Fee.sol`). It fires when a strategy charges an Aqua protocol fee that could not be collected: the swap proceeds anyway and the uncollected fee stays with the maker, so this log is the **only signal** that protocol revenue was skipped. Earlier routers (`0x016b41...b070`, v1.0.1) never emit it -- do not scan for it in their history. ::: ### Field semantics - `maker` -- the liquidity provider whose wallet balances are affected. - `app` -- the app/strategy contract authorized to pull from the maker (the strategy's `app` implementation). - `strategyHash` -- `bytes32` key identifying the shipped strategy. Returned by `ship()` and used as the lookup key everywhere. - `strategy` (Shipped only) -- the full ABI-encoded strategy/program bytes (see Decoding the Shipped strategy bytes). Presented in full rather than pre-hashed specifically for data availability. - `token` / `amount` (Pulled/Pushed) -- the ERC-20 moved and its raw amount. `Pushed` increases the maker's strategy balance; `Pulled` decreases it. ## interface IAqua Consolidated and copy-pasteable. Declarations are verbatim from `1inch/aqua/src/interfaces/IAqua.sol`; NatSpec comments are trimmed for brevity -- signatures, types, and mutability are unchanged. ```solidity // SPDX-License-Identifier: LicenseRef-Degensoft-Aqua-Source-1.1 pragma solidity ^0.8.0; interface IAqua { // ---- Errors ---- error MaxNumberOfTokensExceeded(uint256 tokensCount, uint256 maxTokensCount); error StrategiesMustBeImmutable(address app, bytes32 strategyHash); error DockingShouldCloseAllTokens(address app, bytes32 strategyHash); error PushToNonActiveStrategyPrevented(address maker, address app, bytes32 strategyHash, address token); error SafeBalancesForTokenNotInActiveStrategy(address maker, address app, bytes32 strategyHash, address token); // ---- Events (no indexed params) ---- event Shipped(address maker, address app, bytes32 strategyHash, bytes strategy); event Docked(address maker, address app, bytes32 strategyHash); event Pulled(address maker, address app, bytes32 strategyHash, address token, uint256 amount); event Pushed(address maker, address app, bytes32 strategyHash, address token, uint256 amount); // ---- Views ---- function rawBalances(address maker, address app, bytes32 strategyHash, address token) external view returns (uint248 balance, uint8 tokensCount); function safeBalances(address maker, address app, bytes32 strategyHash, address token0, address token1) external view returns (uint256 balance0, uint256 balance1); // ---- State-changing ---- function ship( address app, bytes calldata strategy, address[] calldata tokens, uint256[] calldata amounts ) external returns (bytes32 strategyHash); function dock(address app, bytes32 strategyHash, address[] calldata tokens) external; function pull(address maker, bytes32 strategyHash, address token, uint256 amount, address to) external; function push(address maker, address app, bytes32 strategyHash, address token, uint256 amount) external; } ``` :::info **Note on**`rawBalances`**:** it returns a packed `(uint248 balance, uint8 tokensCount)`. A `tokensCount` of `0` means "never shipped"; a sentinel value (`_DOCKED`) means "docked / closed". Use `safeBalances` when you need the revert-on-inactive guarantee for two tokens at once. ::: ## App integration surface (AquaApp) An Aqua "app" (the strategy implementation) inherits the abstract base `AquaApp` from `1inch/aqua/src/AquaApp.sol`. This is the surface an app uses inside its own swap method to enforce that the taker actually pushed the promised tokens. It is not a standalone `interface` in source; the load-bearing members are reproduced verbatim below. ```solidity abstract contract AquaApp { // Reverts if strategy params don't match this app's address error InvalidAquaStrategy(address maker, bytes32 strategyHash, bytes32 salt, address app, address actualThis); // Reverts if taker hasn't pushed enough tokens to the maker error MissingTakerAquaPush(address token, uint256 newBalance, uint256 expectedBalance); // Reverts if _safeCheckAquaPush runs without reentrancy protection error MissingNonReentrantModifier(); IAqua public immutable AQUA; // Wrap every swap method in this (or the equivalent explicit lock/unlock) modifier nonReentrantStrategy(address maker, bytes32 strategyHash); constructor(IAqua aqua); // Verify taker settlement; MUST be called inside nonReentrantStrategy function _safeCheckAquaPush( address maker, bytes32 strategyHash, address token, uint256 expectedBalance ) internal view; } ``` ## Swap router interface (ISwapVM) The router's external quote/execute surface, verbatim from the **deployed** source `src/interfaces/ISwapVM.sol` -- identical in the `v1.0.1` and `v1.0.2` git tags of `1inch/swap-vm` (the v1.0.2 tag, cut 2026-07-26, changed only the fee instruction). This is the interface the live router at `0x111111338c...c0de` actually exposes -- selector `0x44aa5f14` for `quote`, `0xf4d2d412` for `swap`. The taker names `tokenIn` and `tokenOut` explicitly, and that pair _is_ the swap direction. `quote()` is a preview (safe to `staticcall`); `swap()` executes. Both return `(amountIn, amountOut, orderHash)`. ```solidity interface ISwapVM { struct Order { address maker; MakerTraits traits; // packed flags + receiver bytes data; // encoded hooks data + program bytecode (no token prefix) } function hash(Order calldata order) external view returns (bytes32); function quote( Order calldata order, address tokenIn, address tokenOut, uint256 amount, bytes calldata takerTraitsAndData ) external view returns (uint256 amountIn, uint256 amountOut, bytes32 orderHash); function swap( Order calldata order, address tokenIn, address tokenOut, uint256 amount, bytes calldata takerTraitsAndData ) external returns (uint256 amountIn, uint256 amountOut, bytes32 orderHash); } ``` :::info The `1inch/swap-vm` `main` branch has refactored this surface (dropping the token args and moving the pair into `order.data`), but that revision is **not deployed** -- the newest release tag is `v1.0.2` (cut 2026-07-26), which keeps the 5-argument surface unchanged. Build against the 5-argument signature above; it matches the on-chain router and the published `@1inch/swap-vm-sdk` (0.4.0). ::: :::info For Aqua orders, `hash(order)` returns `keccak256(abi.encode(order))` (no EIP-712 domain); for signature orders it returns the EIP-712 typed hash. This `orderHash` is the value emitted in `Swapped`. ::: ### Taker swap-callback (ITakerCallbacks) A taker contract may implement these to be invoked during settlement, verbatim from `1inch/swap-vm/src/interfaces/ITakerCallbacks.sol`. ```solidity interface ITakerCallbacks { // Called before tokenIn is transferred from taker to maker function preTransferInCallback( address maker, address taker, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut, bytes32 orderHash, bytes calldata takerData ) external; // Called before tokenOut is transferred from maker to taker function preTransferOutCallback( address maker, address taker, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut, bytes32 orderHash, bytes calldata takerData ) external; } ``` ## Custom errors Verbatim from source, with the exact revert condition. Registry errors are declared in `IAqua` and reverted in `Aqua.sol`; app errors are declared and reverted in `AquaApp.sol`; router errors in `SwapVM.sol`. | Error | Declared in | Reverts when | | --------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `MaxNumberOfTokensExceeded(uint256 tokensCount, uint256 maxTokensCount)` | IAqua | A strategy is shipped with more tokens than allowed (`tokensCount` collides with the `_DOCKED` sentinel). `maxTokensCount` = `_DOCKED - 1`. | | `StrategiesMustBeImmutable(address app, bytes32 strategyHash)` | IAqua | `ship()` targets a `(app, strategyHash)` that already exists (`tokensCount != 0`). Strategies cannot be modified once shipped. | | `DockingShouldCloseAllTokens(address app, bytes32 strategyHash)` | IAqua | `dock()` is called with a `tokens` array whose length the strategy's token count, i.e. it would not close every token. | | `PushToNonActiveStrategyPrevented(address maker, address app, bytes32 strategyHash, address token)` | IAqua | `push()` targets a strategy that is not active -- never shipped (`tokensCount == 0`) or already docked (`tokensCount == _DOCKED`). | | `SafeBalancesForTokenNotInActiveStrategy(address maker, address app, bytes32 strategyHash, address token)` | IAqua | `safeBalances()` is queried for a token whose `tokensCount` is `0` or `_DOCKED` (token not part of an active strategy). | | `InvalidAquaStrategy(address maker, bytes32 strategyHash, bytes32 salt, address app, address actualThis)` | AquaApp | Strategy parameters resolve to an `app` address that is not the executing contract (`address(this)`). | | `MissingTakerAquaPush(address token, uint256 newBalance, uint256 expectedBalance)` | AquaApp | `_safeCheckAquaPush` finds the maker's post-swap balance below the minimum expected -- the taker underpaid. | | `MissingNonReentrantModifier()` | AquaApp | `_safeCheckAquaPush` is called outside a reentrancy-locked context (the `nonReentrantStrategy` guard is not held). | | `BadSignature(address maker, bytes32 orderHash, bytes signature)` | SwapVM | A signature-based order fails ECDSA recovery to `maker`. | | `AquaBalanceInsufficientAfterTakerPush(uint256 balance, uint256 preBalance, uint256 amount, uint256 amountNetPulled)` | SwapVM | Post-push Aqua balance accounting for an Aqua order does not reconcile. | | `MakerTraitsUnwrapIsIncompatibleWithAqua()` | SwapVM | An Aqua order sets `shouldUnwrapWeth` -- unsupported for Aqua orders. | | `MakerTraitsCustomReceiverIsIncompatibleWithAqua()` | SwapVM | An Aqua order sets a custom receiver -- unsupported for Aqua orders. | ## Decoding the Shipped strategy bytes The `strategy` field in `Shipped` is the full ABI-encoded strategy passed to `ship()` -- deliberately logged in full (not pre-hashed) so that anyone can reconstruct a maker's program from logs alone (data availability). It encodes an `ISwapVM.Order`-shaped program whose `data` tail is the strategy _bytecode_: a sequence of VM instructions the swap router executes. - To interpret the bytecode, map each opcode to its handler in the router's opcode set. For the Aqua router this is `AquaOpcodes` (`1inch/swap-vm/src/opcodes/AquaOpcodes.sol`), which dispatches opcodes such as `10-16` (Controls: jump/deadline/balance guards), `17` (XYC swap), `18` (XYC concentrate), `19` (Decay), `21/27-30` (Fee variants), `31` (PeggedSwap), `32` (Extruction). Instruction bodies live under `src/instructions/`. - **The opcode table is versioned.** Opcode numbers are assigned by the deployed router build and new instructions are appended to preserve backward compatibility -- so a given opcode byte only has meaning relative to the exact router version that will execute the strategy. Decode against the opcode table of the specific `AquaSwapVMRouter` deployment the strategy targets, not a generic table. In particular, never derive the live table from the undeployed `main` branch: main's opcode enum has been renumbered (no reserved gaps; unknown opcodes revert `UnknownOpcode(uint256)`) and is not deployed anywhere -- the deployed v1.0.x builds are the only valid reference. The `v1.0.2` tag (the current router's source) left `AquaOpcodes.sol` untouched relative to v1.0.1, so the opcode table is identical across all deployed v1.0.x routers. :::note Several router builds are live-looking on-chain. The canonical router is `AquaSwapVMRouter` v1.0.2 at `0x111111338c5091e8440b67b168bae16a668ac0de` (the 2026-07-26 vanity deployment, built from the `v1.0.2` git tag). Superseded but still live, decode-only: `0x1111113db0e0ef9d0e3a50d5f094a3a57a26c0de` (first vanity router, 2026-07-19 - 2026-07-26), `0x016b417bc933370f5eacc40b1d58b015ac72b070` (2026-07-16 universal deploy, v1.0.1 source recompiled), and `0x3c4758979ec30ca45857cabc2462a70699ed790e` (v1.0.1, superseded 2026-07-16). The older `0x8fdd04dbf6111437b44bbca99c28882434e0958f` is a stale build with a different opcode layout -- do not decode current strategies against it. ::: ## Read paths / monitoring ### Enumerate a maker's strategies There is no on-chain registry enumeration. Reconstruct a maker's active strategies from the log stream: 1. `eth_getLogs` on the Aqua registry address filtered by `topics[0] = Shipped` topic0. Because `maker` is not indexed, you must decode each log's `data` and keep those where `maker` matches. 2. Subtract strategies that later emitted `Docked` (match on `maker, app, strategyHash`) to get the currently-active set. 3. For live balances of an active strategy, call `rawBalances(maker, app, strategyHash, token)` (or `safeBalances` for a token pair). ### Poll vs subscribe - **Subscribe** (`eth_subscribe` / `logs`) for near-real-time updates on `Pulled`/`Pushed`/`Swapped`. Remember all fields are in `data`; your subscription filter can only pin `address` + `topics[0]`, so decode-and-match client-side. - **Poll** (`eth_getLogs` over block ranges) for backfill and for chains/providers without reliable subscriptions. Chunk block ranges to stay under provider log limits and dedupe on `(blockNumber, logIndex)`. ### Backfill needs a per-chain deployment block :::note To backfill the full history you must start `eth_getLogs` at the block where the Aqua registry (and, separately, the swap router) was deployed on that chain. This **deployment block is per-deployment and differs on every chain** -- it is not encoded in this doc and must be looked up from the deployment artifacts / block explorer for each of the 13 Aqua chains. Do not assume a shared or zero start block; scanning from genesis is unnecessary and scanning from the wrong block will silently drop early strategies. ::: :::info **orderHash == strategyHash.** The `Swapped.orderHash` emitted by the router is the same 32-byte value as the strategy identifier returned by `ship()` and logged in `Shipped` as `strategyHash`. Join fills to strategies on this equality exactly: `Swapped.orderHash === Shipped.strategyHash` (this is why analytics can attribute volume by that key). ::: ## Per-chain deployment blocks The `AquaSwapVMRouter` v1.0.1 (superseded 2026-07-16 by `0x016b417bc933370f5eacc40b1d58b015ac72b070`, in turn superseded by the canonical vanity router `0x111111338c5091e8440b67b168bae16a668ac0de` on 2026-07-26) was deployed at the same deterministic address on every supported chain: ``` 0x3c4758979ec30ca45857cabc2462a70699ed790e ``` The table below lists the block in which the router creation transaction was mined on each of the 12 Aqua chains. Values are taken from the router deployment receipts in the `1inch/swap-vm` broadcast artifacts (`broadcast/__DeployPadCreate.s.sol//run-latest.json`) and converted from hex to decimal. | Chain | Chain ID | Router deploy block (decimal) | | ---------- | -------- | ----------------------------- | | Ethereum | 1 | 25330939 | | Optimism | 10 | 153012311 | | BNB Chain | 56 | 104589422 | | Gnosis | 100 | 46728143 | | Unichain | 130 | 50875110 | | Polygon | 137 | 88612289 | | Sonic | 146 | 74069677 | | zkSync Era | 324 | 70817618 | | Base | 8453 | 47416981 | | Arbitrum | 42161 | 474133160 | | Avalanche | 43114 | 88164944 | | Linea | 59144 | 31054397 | :::info When indexing router logs, start your `eth_getLogs` range at the deploy block shown above for each chain rather than from genesis. No router events exist before this block, so scanning earlier ranges wastes provider calls and returns nothing. ::: :::note The table above covers the **v1.0.1 router only**. Each later router generation has its own, later deploy blocks -- per chain, read them from the contract's creation transaction on the block explorer. On Ethereum: the current vanity router `0x111111338c...c0de` was created in block **25618917** (2026-07-26) and the current registry `0x1111113ccf...a90a` in block **25567141** (2026-07-19), both per Etherscan/Blockscout creation transactions. Do not reuse the v1.0.1 blocks for the current deployment: they are weeks earlier and will waste scans (though they return nothing rather than wrong data). ::: ### Registry deploy block The Aqua registry deployment block is **not** present in the public `1inch/swap-vm` broadcast artifacts, which cover only the router. To index registry events, read the registry contract's own creation transaction on each chain (for example, look up the contract address and inspect the block of its creation transaction) and begin your `eth_getLogs` range from that block. Do not assume it matches the router deploy block -- determine it per chain from the registry's creation transaction rather than guessing. ## Function-selector reference Every function on the deployed contracts is dispatched by its 4-byte selector -- `bytes4(keccak256(canonicalSignature))`. The tables below list the selectors for the `AquaSwapVMRouter` v1.0.2 swap surface and the Aqua registry (`IAqua`) surface, so any transaction or trace can be decided against the chain by matching `calldata[0:4]`. The _canonical signature_ column is the exact string hashed: struct arguments are expanded to their ABI tuple form and the `MakerTraits` user-defined value type resolves to its underlying `uint256`. :::info `Order.traits`**is the**`MakerTraits`**type -- a**`uint256`**user-defined value type** (`type MakerTraits is uint256;`, `1inch/swap-vm/src/libs/MakerTraits.sol`). For ABI-encoding and selector computation it is indistinguishable from `uint256`, so the `Order` struct `(address maker, MakerTraits traits, bytes data)` hashes as the tuple `(address,uint256,bytes)`. This same statement holds identically wherever `Order` appears in this reference. ::: ### Router selectors -- `AquaSwapVMRouter` v1.0.2 Both entry points take the same five arguments -- `(Order order, address tokenIn, address tokenOut, uint256 amount, bytes takerTraitsAndData)` -- verbatim from `1inch/swap-vm/src/interfaces/ISwapVM.sol`, identical at the `v1.0.1` and `v1.0.2` tags. `quote()` is a view preview; `swap()` executes. Router at `0x111111338c5091e8440b67b168bae16a668ac0de`. | Function | Selector | Canonical signature (hashed) | | -------- | ------------ | -------------------------------------------------------------- | | `quote` | `0x44aa5f14` | `quote((address,uint256,bytes),address,address,uint256,bytes)` | | `swap` | `0xf4d2d412` | `swap((address,uint256,bytes),address,address,uint256,bytes)` | :::note These are the **five-argument** v1.0.2 signatures -- `Order`, `tokenIn`, `tokenOut`, `amount`, `takerTraitsAndData`. A three-argument `quote/swap(Order,uint256,bytes)` hashes to different selectors and does not match the deployed router; decode calldata against the selectors above. ::: ### Registry selectors -- Aqua (`IAqua`) Selectors for the registry surface, computed from the signatures in `1inch/aqua/src/interfaces/IAqua.sol` at `v1.0.0` (the current Aqua release tag). Registry at `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a`. | Function | Selector | Canonical signature (hashed) | Mutability | | -------------- | ------------ | ------------------------------------------------------- | ---------- | | `ship` | `0xf50b870f` | `ship(address,bytes,address[],uint256[])` | nonpayable | | `dock` | `0x28defc17` | `dock(address,bytes32,address[])` | nonpayable | | `pull` | `0xb00bbd10` | `pull(address,bytes32,address,uint256,address)` | nonpayable | | `push` | `0x47d72768` | `push(address,address,bytes32,address,uint256)` | nonpayable | | `rawBalances` | `0x6d58b4cc` | `rawBalances(address,address,bytes32,address)` | view | | `safeBalances` | `0x65f2fe14` | `safeBalances(address,address,bytes32,address,address)` | view | :::info To recompute any selector, hash the exact string in the _canonical signature_ column and take the first 4 bytes -- e.g. `cast sig "ship(address,bytes,address[],uint256[])"` returns `0xf50b870f`. The same routine reproduces the well-known `Transfer(address,address,uint256)` topic hash, so the values are independently verifiable. ::: https://business.1inch.com/portal/assets/docs-v2/aqua/reference/encoding-reference.md This page gives the exact packed bit and byte layouts used by the on-chain SwapVM and Aqua contracts, so an off-chain decoder can reconstruct every field without guessing. Every offset below is taken directly from the source symbols cited under each section. Bit positions are counted from the least-significant bit (bit 0) of the underlying `uint256`; byte offsets are counted from the start of the byte string and are big-endian, matching Solidity's `abi.encodePacked` layout. :::info All numbers here are verified against the deployed contract source. Where a field's position depends on run-time data (variable-length hook slices), the layout tells you which packed index to read to locate it; the concrete offset is only known once the order's own `data` is present. ::: ## MakerTraits -- packed `uint256` header Source: `swap-vm/src/libs/MakerTraits.sol`, type `MakerTraits is uint256` and library `MakerTraitsLib`. The value is assembled in `MakerTraitsLib.build`; the constants below are the literal bit-flag definitions. | Bit(s) | Width | Field | Source constant / expression | | ------- | ----- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------ | ------------- | | 255 | 1 | `shouldUnwrapWeth` | `SHOULD_UNWRAP_BIT_FLAG = 1 << 255` | | 254 | 1 | `useAquaInsteadOfSignature` | `USE_AQUA_INSTEAD_OF_SIGNATURE_BIT_FLAG = 1 << 254` | | 253 | 1 | `allowZeroAmountIn` | `ALLOW_ZERO_AMOUNT_IN = 1 << 253` | | 252 | 1 | `hasPreTransferInHook` | `HAS_PRE_TRANSFER_IN_HOOK_BIT_FLAG = 1 << 252` | | 251 | 1 | `hasPostTransferInHook` | `HAS_POST_TRANSFER_IN_HOOK_BIT_FLAG = 1 << 251` | | 250 | 1 | `hasPreTransferOutHook` | `HAS_PRE_TRANSFER_OUT_HOOK_BIT_FLAG = 1 << 250` | | 249 | 1 | `hasPostTransferOutHook` | `HAS_POST_TRANSFER_OUT_HOOK_BIT_FLAG = 1 << 249` | | 248 | 1 | `preTransferInHookHasTarget` | `PRE_TRANSFER_IN_HOOK_HAS_TARGET = 1 << 248` | | 247 | 1 | `postTransferInHookHasTarget` | `POST_TRANSFER_IN_HOOK_HAS_TARGET = 1 << 247` | | 246 | 1 | `preTransferOutHookHasTarget` | `PRE_TRANSFER_OUT_HOOK_HAS_TARGET = 1 << 246` | | 245 | 1 | `postTransferOutHookHasTarget` | `POST_TRANSFER_OUT_HOOK_HAS_TARGET = 1 << 245` | | 244-224 | 21 | reserved (written as zero) | gap between flag bits and index block | | 223-208 | 16 | `index3` -- end of PostTransferOutHook slice = **program start offset** | `orderDataIndexes << ORDER_DATA_SLICES_INDEXES_BIT_OFFSET` where `ORDER_DATA_SLICES_INDEXES_BIT_OFFSET = 160`; four `uint16` packed as `(index0<<0) | (index1<<16) | (index2<<32) | (index3<<48)` | | 207-192 | 16 | `index2` -- end of PreTransferOutHook slice | | | 191-176 | 16 | `index1` -- end of PostTransferInHook slice | | | 175-160 | 16 | `index0` -- end of PreTransferInHook slice | | | 159-0 | 160 | `receiver` address (if zero, defaults to `maker`) | `uint160(args.receiver)` | The four packed indexes are read back by `_getOffset(traits, sliceNumber)`: ```solidity bitShift = sliceNumber << ORDER_DATA_SLICES_INDEX_BIT_SIZE_SHL; // sliceNumber * 16 offset = (traits >> 160 >> bitShift) & type(uint16).max; // ORDER_DATA_SLICES_INDEX_BIT_MASK ``` ### Order.data byte layout (the bytes referenced by MakerTraits) The `data` field of `ISwapVM.Order` is built with `bytes.concat` / `abi.encodePacked` in `MakerTraitsLib.build`. It is a packed (non-ABI) byte string. The `enum OrderDataSlices { PreTransferInHook, PostTransferInHook, PreTransferOutHook, PostTransferOutHook, Program }` names the slices. In the **deployed** **`v1.0.1`** layout there is **no token prefix** -- the token pair is passed to `quote`/`swap` as explicit `tokenIn`/`tokenOut` arguments, not stored in `data` -- and the four packed `index0..index3` values in `MakerTraits` mark the slice boundaries: | Byte range | Slice | Notes | | ------------------------ | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `[0 : index0]` | PreTransferInHook | Starts at 0. If `PRE_TRANSFER_IN_HOOK_HAS_TARGET` is set, first 20 bytes are the hook target address, remainder is hook calldata; otherwise the whole slice is calldata and the target defaults to `maker`. | | `[index0 : index1]` | PostTransferInHook | Same target-prefix rule with `POST_TRANSFER_IN_HOOK_HAS_TARGET`. | | `[index1 : index2]` | PreTransferOutHook | Same rule with `PRE_TRANSFER_OUT_HOOK_HAS_TARGET`. | | `[index2 : index3]` | PostTransferOutHook | Same rule with `POST_TRANSFER_OUT_HOOK_HAS_TARGET`. | | `[index3 : data.length]` | **Program** (VM bytecode tail) | The SwapVM program runs from `index3` to the end of `data`. With no hooks, `index0..index3` are all `0`, so the program is the entire `data`. See `program()` and `_getDataSlice(...)` for `OrderDataSlices.Program`. | Confirmed by `MakerTraitsLib._getDataSlice`: the PreTransferInHook slice starts at `0`, and the Program slice stops at `data.length`. All intermediate boundaries are the packed indexes above. (The unreleased `main` branch prepends a `tokenA || tokenB` pair and shifts every slice by 40 bytes, but that revision is not tagged or deployed -- build against the layout above.) ## TakerTraits -- packed header + slice table Source: `swap-vm/src/libs/TakerTraits.sol`, type `TakerTraits is uint256` and library `TakerTraitsLib`. Unlike MakerTraits, the taker value is a **22-byte (176-bit) header** that is prepended to a packed tail. `parse` reads it as `TakerTraits.wrap(uint176(bytes22(data.slice(0, 22, ...))))` and the tail is `data.slice(22)`. ### Header composition (as emitted by `build`) `build` does `abi.encodePacked(slicesIndexes /*uint160*/, flags /*uint16*/, ...tail...)`. So on the wire the first 20 bytes are the ten packed slice indexes and the next 2 bytes are the flag word: | Header byte range | Content | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `[0 : 20]` | `slicesIndexes` -- ten `uint16` values (`index0..index9`), big-endian; `index9` occupies bytes `[0:2]`, `index0` occupies bytes `[18:20]` | | `[20 : 22]` | `flags` -- the `uint16` flag word | ### Header as a `uint176` (how the getters read it) `TAKER_DATA_SLICES_INDEXES_BIT_OFFSET = 16`, so `_getOffset(traits, n) = (traits >> 16 >> (n*16)) & type(uint16).max`. The flag word sits in the low 16 bits. | Bit(s) of uint176 | Field | | ----------------- | ------------------------------ | | 15-0 | `flags` (see flag table below) | | 31-16 | `index0` | | 47-32 | `index1` | | 63-48 | `index2` | | 79-64 | `index3` | | 95-80 | `index4` | | 111-96 | `index5` | | 127-112 | `index6` | | 143-128 | `index7` | | 159-144 | `index8` | | 175-160 | `index9` | ### Flag word (low 16 bits) | Bit | Mask | Flag | | ---- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 0 | `0x0001` | `IS_EXACT_IN_BIT_FLAG` | | 1 | `0x0002` | `SHOULD_UNWRAP_BIT_FLAG` | | 2 | `0x0004` | `HAS_PRE_TRANSFER_IN_CALLBACK_BIT_FLAG` | | 3 | `0x0008` | `HAS_PRE_TRANSFER_OUT_CALLBACK_BIT_FLAG` | | 4 | `0x0010` | `IS_STRICT_THRESHOLD_BIT_FLAG` | | 5 | `0x0020` | `IS_FIRST_TRANSFER_FROM_TAKER_BIT_FLAG` | | 6 | `0x0040` | `USE_TRANSFER_FROM_AND_AQUA_PUSH_FLAG` | | 7 | `0x0080` | `IS_A_TO_B_BIT_FLAG` -- **unreleased** **`main`** **only**; not present on the deployed v1.0.1 router (there, swap direction is the explicit `tokenIn`/`tokenOut` arguments) | | 15-8 | -- | unused | ### Tail slice table Offsets are relative to the **tail** (the bytes after the 22-byte header). The `enum TakerDataSlices` ordering drives `_getStartOffset`/`_getStopOffset`: slice _s_ starts at `index[s-1]` (or 0 for the first) and stops at `index[s]` (or `dataLength` for the last). | Slice (enum) | idx | Start | Stop | Fixed width, if any | | ------------------------ | --- | -------- | ------------ | ----------------------- | | `Threshold` | 0 | `0` | `index0` | 0 or 32 bytes | | `To` | 1 | `index0` | `index1` | 0 or 20 bytes | | `Deadline` | 2 | `index1` | `index2` | 0 or 5 bytes (`uint40`) | | `PreTransferInHook` | 3 | `index2` | `index3` | variable | | `PostTransferInHook` | 4 | `index3` | `index4` | variable | | `PreTransferOutHook` | 5 | `index4` | `index5` | variable | | `PostTransferOutHook` | 6 | `index5` | `index6` | variable | | `PreTransferInCallback` | 7 | `index6` | `index7` | variable | | `PreTransferOutCallback` | 8 | `index7` | `index8` | variable | | `InstructionsArgs` | 9 | `index8` | `index9` | variable | | `Signature` | 10 | `index9` | `dataLength` | tail remainder | :::info There are 11 slices but only 10 packed indexes: the first slice always starts at 0 and the last always ends at `dataLength`, so only the 10 interior boundaries need storing. `Threshold` must be exactly 0 or 32 bytes (enforced in `build`). ::: ## The strategy shape emitted in `Shipped.strategy` Source: `aqua/src/interfaces/IAqua.sol` (event `Shipped(address maker, address app, bytes32 strategyHash, bytes strategy)`) and `aqua/src/Aqua.sol` (`ship` computes `strategyHash = keccak256(strategy)` and emits `Shipped`). For a SwapVM strategy, `strategy` is `abi.encode(order)` where `order` is `ISwapVM.Order` -- confirmed by `swap-vm/test/base/AquaStrategyBuilders.sol` (`bytes memory strategy = abi.encode(order);`) and by `SwapVM.hash`, which for Aqua orders returns `keccak256(abi.encode(order))` (so `strategyHash == orderHash`). `ISwapVM.Order` (from `swap-vm/src/interfaces/ISwapVM.sol`): ```solidity struct Order { address maker; // liquidity provider MakerTraits traits; // uint256, packed header above bytes data; // hooks... || program (no token prefix at v1.0.1) } ``` Because it is standard `abi.encode` of a `(address, uint256, bytes)` tuple, the outer byte layout of `Shipped.strategy` is: | Offset | Word | Content | | ------ | --------- | ------------------------------------------------------------------ | | `0x00` | head\[0\] | `maker` -- address, left-padded to 32 bytes (value in low 20 bytes) | | `0x20` | head\[1\] | `traits` -- the full `uint256` MakerTraits value | | `0x40` | head\[2\] | offset to `data` tail = `0x60` (96) | | `0x60` | tail | `data.length` (`uint256`) | | `0x80` | tail | `data` bytes, right-padded to a 32-byte boundary | A decoder therefore: 1. Reads `maker` from the low 20 bytes of `[0x00:0x20]`. 2. Reads `traits` from `[0x20:0x40]` and decodes the MakerTraits bit map (above) -- in particular `index3` at bits 223-208, which is the program start offset within `data`. 3. Reads `len = data.length` from `[0x60:0x80]` and takes `data = strategy[0x80 : 0x80 + len]`. 4. Within `data` (no token prefix at v1.0.1): the **SwapVM program bytecode tail =** **`data[index3 : len]`**; with no hooks `index3 = 0`, so the program is all of `data`. The token pair is not in `data` -- it is supplied as the `tokenIn`/`tokenOut` arguments to `quote`/`swap`. :::info The strategy is emitted in full (not pre-hashed) specifically for data availability, so an indexer can recover the complete Order -- including the executable program tail -- from the `Shipped` event alone. `strategyHash = keccak256(strategy)` is the same value the router computes as the order hash for Aqua strategies. ::: ## rawBalances packing (Balance struct) Source: `aqua/src/libs/Balance.sol` (`struct Balance { uint248 amount; uint8 tokensCount; }` and `BalanceLib.load`/`store`) and `aqua/src/Aqua.sol` (`_balances` mapping and `_DOCKED`). Each balance occupies exactly one 32-byte storage slot. The pack/unpack is done in assembly: ``` // store packed = or(amount, shl(248, tokensCount)); // amount in low 248 bits, count in top byte // load amount = packed & 0x00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; tokensCount = shr(248, packed); ``` | Slot bit(s) | Slot byte(s), big-endian | Field | | ----------- | ------------------------- | ------------------------------------- | | 255-248 | byte 0 (most significant) | `tokensCount` (`uint8`) | | 247-0 | bytes 1-31 | `amount` (`uint248`, max `2^248 1`) | The external view `rawBalances(maker, app, strategyHash, token)` returns `(uint248 balance, uint8 tokensCount)` as two separate ABI words; the single-slot packing above is the storage form, not the return form. ### `tokensCount` sentinel values and `_DOCKED` `uint8 private constant _DOCKED = 0xff;` (source: `aqua/src/Aqua.sol`). The `tokensCount` byte doubles as a lifecycle marker: | Value | Meaning | | --------------------- | ----------------------------------------------------- | | `0x00` | inactive -- never shipped, or slot empty | | `0x01 ... 0xFE` (1-254) | active strategy; the number of tokens in the strategy | | `0xFF` (`_DOCKED`) | docked (revoked); balance forced to 0 | The maximum token count is `_DOCKED - 1 = 254`: `ship` reverts with `MaxNumberOfTokensExceeded(tokensCount, _DOCKED - 1)` if `tokensCount == _DOCKED`. `safeBalances` and `push` both treat a token as usable only when `tokensCount > 0 && tokensCount != _DOCKED`; `dock` stores `(0, _DOCKED)` for every token in the strategy. ## Source files - `swap-vm/src/libs/MakerTraits.sol` -- `MakerTraits`, `MakerTraitsLib` (flag constants, `build`, `_getOffset`, `OrderDataSlices`) - `swap-vm/src/libs/TakerTraits.sol` -- `TakerTraits`, `TakerTraitsLib` (flag constants, `build`, `parse`, `TakerDataSlices`, `_getStartOffset`/`_getStopOffset`) - `swap-vm/src/interfaces/ISwapVM.sol` -- `ISwapVM.Order` - `swap-vm/src/SwapVM.sol` -- `hash()` (`keccak256(abi.encode(order))` for Aqua orders), `ORDER_TYPEHASH` - `swap-vm/test/base/AquaStrategyBuilders.sol` -- `abi.encode(order)` shipped as the strategy - `aqua/src/interfaces/IAqua.sol` -- `Shipped` event, `rawBalances` - `aqua/src/Aqua.sol` -- `ship`/`dock`/`push`/`safeBalances`, `_DOCKED` - `aqua/src/libs/Balance.sol` -- `Balance` struct, `BalanceLib.load`/`store` ## TakerTraits bridge -- SDK wrapper -> Solidity flag -> bit The two canonical TakerTraits implementations are the on-chain library `TakerTraitsLib` (`swap-vm/src/libs/TakerTraits.sol`) and the off-chain wrapper class `TakerTraits` in `@1inch/swap-vm-sdk` (`typescript/swap-vm/src/swap-vm/taker-traits.ts`). They encode the same 22-byte header -- `uint176` = 20-byte `slicesIndexes` (ten `uint16`) followed by a `uint16` flag word -- but name the flags differently and, critically, store the flag constants in two different forms. This section is the exact field-by-field bridge so a value round-trips losslessly between them. :::note **The two sides define the flag constants differently.** In Solidity each constant is a _mask_ already shifted into place (`IS_EXACT_IN_BIT_FLAG = 0x0001`, `SHOULD_UNWRAP_BIT_FLAG = 0x0002`, ...) and is applied with `traits & MASK`. In the SDK each constant is a _bit index_ (`IS_EXACT_IN_BIT_FLAG = 0n`, `SHOULD_UNWRAP_BIT_FLAG = 1n`, ...) applied with `BN.getBit(index)` / `BN.setBit(index)`. Mask `1 << index` on the Solidity side equals bit `index` on the SDK side. Do not read the SDK's `0n...6n` as masks. ::: ### Flag word bridge (low 16 bits of the header) All seven flags live in the low `uint16` of the `uint176` header. The SDK exposes each as a `public readonly` boolean property on the `TakerTraits` instance (there are no getter methods on the SDK side); Solidity exposes each via a library getter that masks the packed value. | Bit | Solidity mask | Solidity constant (`TakerTraitsLib`) | Solidity `Args` field / getter | SDK property (`TakerTraits`) | SDK constant (bit index) | | --- | ------------- | ---------------------------------------- | ------------------------------------------------------------- | ------------------------------- | --------------------------------------------- | | 0 | `0x0001` | `IS_EXACT_IN_BIT_FLAG` | `isExactIn` / `isExactIn()` | `exactIn` | `IS_EXACT_IN_BIT_FLAG = 0n` | | 1 | `0x0002` | `SHOULD_UNWRAP_BIT_FLAG` | `shouldUnwrapWeth` / `shouldUnwrapWeth()` | `shouldUnwrap` | `SHOULD_UNWRAP_BIT_FLAG = 1n` | | 2 | `0x0004` | `HAS_PRE_TRANSFER_IN_CALLBACK_BIT_FLAG` | `hasPreTransferInCallback` / `hasPreTransferInCallback()` | `preTransferInCallbackEnabled` | `HAS_PRE_TRANSFER_IN_CALLBACK_BIT_FLAG = 2n` | | 3 | `0x0008` | `HAS_PRE_TRANSFER_OUT_CALLBACK_BIT_FLAG` | `hasPreTransferOutCallback` / `hasPreTransferOutCallback()` | `preTransferOutCallbackEnabled` | `HAS_PRE_TRANSFER_OUT_CALLBACK_BIT_FLAG = 3n` | | 4 | `0x0010` | `IS_STRICT_THRESHOLD_BIT_FLAG` | `isStrictThresholdAmount` / `isStrictThresholdAmount()` | `strictThreshold` | `IS_STRICT_THRESHOLD_BIT_FLAG = 4n` | | 5 | `0x0020` | `IS_FIRST_TRANSFER_FROM_TAKER_BIT_FLAG` | `isFirstTransferFromTaker` / `isFirstTransferFromTaker()` | `firstTransferFromTaker` | `IS_FIRST_TRANSFER_FROM_TAKER_BIT_FLAG = 5n` | | 6 | `0x0040` | `USE_TRANSFER_FROM_AND_AQUA_PUSH_FLAG` | `useTransferFromAndAquaPush` / `useTransferFromAndAquaPush()` | `useTransferFromAndAquaPush` | `USE_TRANSFER_FROM_AND_AQUA_PUSH_FLAG = 6n` | :::note **Bits 7-15 are unused at v1.0.1, and there is no** **`IS_A_TO_B`** **flag.** `TakerTraitsLib` defines exactly the seven constants above (masks `0x0001`-`0x0040`) and the SDK defines exactly the seven matching bit indices (`0n`-`6n`). Neither side defines a bit-7 `IS_A_TO_B_BIT_FLAG` in the deployed protocol. A decoder must treat bits 7 through 15 of the flag word as reserved zero and must not attribute swap direction to any TakerTraits bit -- direction is determined by the order's token layout, not by a taker flag. ::: ### Header slice fields (SDK constructor field -> Solidity slice) The non-flag header fields set the ten packed `slicesIndexes` that delimit the tail. Each SDK constructor field maps one-to-one to a `TakerTraitsLib.Args` field and to one `TakerDataSlices` entry. The SDK omits a slice (contributes zero length, leaving the boundary equal to the previous index) under the same emptiness rule the Solidity `build` uses. | SDK field (`TakerTraits`) | Solidity `Args` field | `TakerDataSlices` | Slice idx | Emitted when | Fixed width | | ---------------------------- | ---------------------------- | ------------------------ | --------- | ------------------------------------------------- | -------------------------------- | | `threshold` (`bigint`) | `threshold` | `Threshold` | 0 | `threshold > 0n` | 0 or 32 bytes | | `customReceiver` (`Address`) | `to` | `To` | 1 | non-zero (Solidity also drops when `to == taker`) | 0 or 20 bytes | | `deadline` (`bigint`) | `deadline` | `Deadline` | 2 | `deadline > 0n` | 0 or 5 bytes (`uint40`) | | `preTransferInHookData` | `preTransferInHookData` | `PreTransferInHook` | 3 | non-empty | variable | | `postTransferInHookData` | `postTransferInHookData` | `PostTransferInHook` | 4 | non-empty | variable | | `preTransferOutHookData` | `preTransferOutHookData` | `PreTransferOutHook` | 5 | non-empty | variable | | `postTransferOutHookData` | `postTransferOutHookData` | `PostTransferOutHook` | 6 | non-empty | variable | | `preTransferInCallbackData` | `preTransferInCallbackData` | `PreTransferInCallback` | 7 | non-empty | variable | | `preTransferOutCallbackData` | `preTransferOutCallbackData` | `PreTransferOutCallback` | 8 | non-empty | variable | | `instructionsArgs` | `instructionsArgs` | `InstructionsArgs` | 9 | non-empty | variable | | `signature` | `signature` | `Signature` | -- | non-empty | tail remainder (no packed index) | :::info Two callback flags are coupled to their data on both sides. In the SDK, `encode()` sets bit 2 when `preTransferInCallbackEnabled || preTransferInCallbackData` is non-empty, and bit 3 likewise for the out-callback -- so supplying callback data auto-raises the flag. In Solidity, `build` does not auto-raise it: it reverts (`TakerTraitsMissingHasPreTransferInFlag` / `...OutFlag`) if callback data is present while `hasPreTransferInCallback` / `hasPreTransferOutCallback` is false. Set the flag explicitly when hand-building taker data for the contract. ::: :::info **SDK defaults** (`TakerTraits.new()` / `TakerTraits.default()`): `exactIn = true` and `useTransferFromAndAquaPush = true`; every other flag defaults to `false` and every data field defaults to empty. Because `customReceiver` is dropped only when it is the zero address (whereas Solidity also drops it when it equals the taker), a header built on-chain with `to == taker` decodes in the SDK to `customReceiver = Address.ZERO_ADDRESS` -- both mean "taker receives", so the round-trip is semantically stable. ::: https://business.1inch.com/portal/assets/docs-v2/aqua/reference/contract-addresses.md > **Status note (2026-07-29).** The announced **vanity-address redeployment is live**: the canonical registry is now `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` and the canonical router `0x111111338c5091e8440b67b168bae16a668ac0de`. The previous 2026-07-16 addresses (`0xe8026b...139b` / `0x016b41...b070`) are superseded and moved to the historical list below. The 1inch Aqua learn pages, the Aqua API, and the current SDK releases (`@1inch/aqua-sdk` 0.3.0, `@1inch/swap-vm-sdk` 0.4.0) all carry the vanity addresses. 1inch Aqua is deployed on **13 EVM chains**. Each core contract holds the same address on every chain. The vanity addresses are created through a deployer factory (Ethereum: factory `0x71481C3B9C6FBa3066AE84961EA22378A80cabe7`, `deploy` method, from EOA `0x0BD61d605C64A857C3D94779aEf7cA295702b3A2` -- per the creation transactions on Blockscout); a shared address is an operational convention and does not by itself prove identical bytecode or constructor arguments. Verify each address on-chain per chain before integrating. The addresses below are the current production deployments. **2026-07 vanity redeployment.** The registry was redeployed to `0x1111113ccf...a90a` on 2026-07-19 and the router to `0x111111338c...c0de` on 2026-07-26 (Ethereum dates; per-chain creation blocks differ). The current router is built from the `1inch/swap-vm` **`v1.0.2` git tag** (cut 2026-07-26), which adds best-effort protocol-fee collection and the `ProtocolFeeSkipped` event; its verified constructor binds it to the vanity registry, and both contracts report owner `0x4134e66d52EfC4C77DD8Ccc952D87b9E92E0C352` (verified on-chain on Ethereum). A first vanity router (`0x1111113db0...c0de`, 2026-07-19) was superseded a week later by the current one. **2026-07-16 universal redeployment (superseded).** Aqua was first redeployed on all chains so the registry held one universal address everywhere (previously Robinhood Chain carried a different, CREATE3-deployed registry); that generation used plain `CREATE` from the nonce-synchronized deployer EOA `0x72B4736F6e482DB07C4F3d7d3b90A24b2FedBe7a`. Because the SwapVM router and the 1inch aggregation executors depend on the registry address, they were redeployed together with it; the router version moved from v1.0.1 to **v1.0.2**. The `KycNFT` credential was **not** redeployed and is unchanged through both redeployments. Superseded registries and routers remain live on-chain: strategies shipped against them stay active there until the maker docks and re-ships on the current deployment. These contracts are audited but new. Audits reduce, but do not eliminate, smart-contract risk. Aqua is self-custodial: tokens stay in the maker's wallet under a revocable, per-chain, per-token allowance, and the protocol holds no tokens. ## Core contracts | Contract | Role | Address | | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | | Aqua (registry) | Balance accounting and strategy registry (deployed on-chain under the name `AquaRouter`; Aqua core plus Simulator, Multicall, and Rescuable) | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` | | AquaSwapVMRouter (SwapVM router, v1.0.2) | The deployed SwapVM swap engine router (KYC-capable), deployed 2026-07-26 from the `v1.0.2` tag | `0x111111338c5091e8440b67b168bae16a668ac0de` | | KycNFT | Taker access credential (symbol `RES`; reuses the Fusion `KycNFT.sol` design). Unchanged by the 2026-07 redeployments | `0x26FFc7D378E8e49Be2c483295A3e3E511F96a468` | Each contract uses one shared address across all 13 chains, including the KycNFT credential. --- ## Aqua (registry) The registry handles balance accounting and holds the immutable strategy records created by `ship()`. It is deployed on-chain under the name `AquaRouter`; in these docs it is referred to as Aqua, or the Aqua registry, and it is not a swap router. It exposes **no EIP-712/ERC-5267 signing domain** (`eip712Domain()` reverts) -- typed-data signing happens only against the router. Address `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a`, identical on every chain (deployed 2026-07-19 on Ethereum, block 25567141; same bytecode as the superseded `0xe8026b...139b` registry). | Chain | Contract address | Explorer | | ---------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------ | | Ethereum Mainnet | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` | [View](https://etherscan.io/address/0x1111113ccf1426a8e30e2bff5e005d929bf6a90a) | | Optimism | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` | [View](https://optimistic.etherscan.io/address/0x1111113ccf1426a8e30e2bff5e005d929bf6a90a) | | BNB Chain | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` | [View](https://bscscan.com/address/0x1111113ccf1426a8e30e2bff5e005d929bf6a90a) | | Gnosis | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` | [View](https://gnosisscan.io/address/0x1111113ccf1426a8e30e2bff5e005d929bf6a90a) | | Unichain | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` | [View](https://uniscan.xyz/address/0x1111113ccf1426a8e30e2bff5e005d929bf6a90a) | | Polygon | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` | [View](https://polygonscan.com/address/0x1111113ccf1426a8e30e2bff5e005d929bf6a90a) | | Sonic | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` | [View](https://sonicscan.org/address/0x1111113ccf1426a8e30e2bff5e005d929bf6a90a) | | zkSync Era | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` | [View](https://explorer.zksync.io/address/0x1111113ccf1426a8e30e2bff5e005d929bf6a90a) | | Robinhood Chain | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` | [View](https://robinhoodchain.blockscout.com/address/0x1111113ccf1426a8e30e2bff5e005d929bf6a90a) | | Base | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` | [View](https://basescan.org/address/0x1111113ccf1426a8e30e2bff5e005d929bf6a90a) | | Arbitrum | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` | [View](https://arbiscan.io/address/0x1111113ccf1426a8e30e2bff5e005d929bf6a90a) | | Avalanche | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` | [View](https://snowtrace.io/address/0x1111113ccf1426a8e30e2bff5e005d929bf6a90a) | | Linea | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` | [View](https://lineascan.build/address/0x1111113ccf1426a8e30e2bff5e005d929bf6a90a) | --- ## AquaSwapVMRouter (SwapVM router, v1.0.2) AquaSwapVMRouter is the deployed SwapVM router (the AquaApp) that executes swaps against strategies registered on Aqua. A single router serves all three strategy types (xyc, concentrated, and pegged); the strategy type is set by the instruction program inside the strategy bytes, not by the app address. Address `0x111111338c5091e8440b67b168bae16a668ac0de`, identical on every chain (deployed 2026-07-26 on Ethereum, block 25618917). The router's constructor binds it to the current registry -- `AQUA() = 0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` -- and its EIP-712 domain is name `1inch SwapVM v1.0`, version `1.0.2` (both verified on-chain on Ethereum via `AQUA()` and `eip712Domain()`). This build matches the `1inch/swap-vm` **`v1.0.2` git tag** (cut 2026-07-26): relative to v1.0.1 it changes only `src/instructions/Fee.sol`, adding best-effort protocol-fee collection and the `ProtocolFeeSkipped` event (present in the Blockscout-verified deployed source). Do not derive its behaviour or opcode table from the undeployed `main` branch. | Chain | Contract address | Explorer | | ---------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------ | | Ethereum Mainnet | `0x111111338c5091e8440b67b168bae16a668ac0de` | [View](https://etherscan.io/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Optimism | `0x111111338c5091e8440b67b168bae16a668ac0de` | [View](https://optimistic.etherscan.io/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | BNB Chain | `0x111111338c5091e8440b67b168bae16a668ac0de` | [View](https://bscscan.com/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Gnosis | `0x111111338c5091e8440b67b168bae16a668ac0de` | [View](https://gnosisscan.io/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Unichain | `0x111111338c5091e8440b67b168bae16a668ac0de` | [View](https://uniscan.xyz/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Polygon | `0x111111338c5091e8440b67b168bae16a668ac0de` | [View](https://polygonscan.com/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Sonic | `0x111111338c5091e8440b67b168bae16a668ac0de` | [View](https://sonicscan.org/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | zkSync Era | `0x111111338c5091e8440b67b168bae16a668ac0de` | [View](https://era.zksync.network/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Robinhood Chain | `0x111111338c5091e8440b67b168bae16a668ac0de` | [View](https://robinhoodchain.blockscout.com/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Base | `0x111111338c5091e8440b67b168bae16a668ac0de` | [View](https://basescan.org/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Arbitrum | `0x111111338c5091e8440b67b168bae16a668ac0de` | [View](https://arbiscan.io/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Avalanche | `0x111111338c5091e8440b67b168bae16a668ac0de` | [View](https://snowtrace.io/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Linea | `0x111111338c5091e8440b67b168bae16a668ac0de` | [View](https://lineascan.build/address/0x111111338c5091e8440b67b168bae16a668ac0de) | --- ## Historical and superseded addresses (decoding only) The addresses below are retained only for decoding historical transactions. Do not use them as current deployments. Strategies shipped against a superseded registry or router remain active on that deployment until the maker docks; decode their history against these addresses, but never route new orders to them. - `0xe8026bf31e58b738647319362581ab11be92139b`: the universal Aqua registry from the 2026-07-16 redeployment; superseded 2026-07-19 by the canonical vanity registry `0x1111113ccf...a90a`. - `0x016b417bc933370f5eacc40b1d58b015ac72b070`: AquaSwapVMRouter v1.0.2 from the 2026-07-16 redeployment (v1.0.1 tag source recompiled with new constructor args), bound to `0xe8026b...139b`; superseded by the canonical vanity router `0x111111338c...c0de`. - `0x1111113db0e0ef9d0e3a50d5f094a3a57a26c0de`: the first vanity AquaSwapVMRouter (deployed 2026-07-19); superseded 2026-07-26 by the current router `0x111111338c...c0de`. - `0x4a055aa172c98ec32de118b9b5b6ac8b4099a580`: the Aqua registry on the original 12 chains until 2026-07-16; superseded by the universal registry `0xe8026b...139b`, in turn superseded by the vanity registry. - `0x3c4758979ec30ca45857cabc2462a70699ed790e`: AquaSwapVMRouter v1.0.1 (deployed 2026-06-16, KYC-capable), bound to the superseded registry `0x4a055a...a580`; replaced on 2026-07-16. - `0x7c2d4aa5c900c08004fadb1c0d953c5b099fec86`: the Robinhood-only Aqua registry (CREATE3, deployed 2026-06-10); superseded by the universal registry. - `0xa5368dd669c5706256cf417b66e71a1efe1d331a`: the Robinhood-only SwapVM router (CREATE3, deployed 2026-06-10); superseded by the universal router. - `0xdfd05fe230bfe7b212878414270c72c8345506fa`: earlier SwapVM router (deployed 2026-03-27) with no KYC opcode. - `0x8fdd04dbf6111437b44bbca99c28882434e0958f`: a stale 28-opcode router that appeared in the `1inch/swap-vm` README (a calling program that reaches instruction index 32 reverts out-of-bounds). - `0x499943e74fb0ce105688beee8ef2abec5d936d31`: a pre-deployment placeholder that appeared in earlier drafts of this page; it was never a production deployment. --- ## Supported networks Aqua's deployed-contract footprint is exactly 13 EVM chains, with the same address per contract on each. | Network | Chain ID | | ---------------- | -------- | | Ethereum Mainnet | 1 | | Optimism | 10 | | BNB Chain | 56 | | Gnosis | 100 | | Unichain | 130 | | Polygon | 137 | | Sonic | 146 | | zkSync Era | 324 | | Robinhood Chain | 4663 | | Base | 8453 | | Arbitrum | 42161 | | Avalanche | 43114 | | Linea | 59144 | Robinhood Chain (4663) joined the Aqua chain set with the 2026-07-16 redeployment; it previously carried a different registry address and was excluded. See [Supported chains](/portal/documentation/aqua/liquidity-layer/supported-chains). **Testnet:** the vanity **registry** is also live at the same address on **Sepolia (chain ID** `11155111`); as of 2026-07-29 the vanity **router is not yet deployed there** (the 2026-07-16-generation contracts remain available on Sepolia for testing). Sepolia is not part of the production Aqua chain set -- re-verify testnet addresses on-chain before use. --- ## Verified addresses and stale references Both public repo READMEs are **still stale** (re-checked 2026-07-29: the `1inch/swap-vm` README shows the old `0x8fdd04...958f` build and the `1inch/aqua` README a placeholder plus the old Robinhood registry) -- do not cite them. The **current SDK releases are now correct**: `@1inch/aqua-sdk` 0.3.0 and `@1inch/swap-vm-sdk` 0.4.0 carry the canonical vanity addresses in `AQUA_CONTRACT_ADDRESSES` / `AQUA_SWAP_VM_CONTRACT_ADDRESSES` for all chains. Earlier releases (0.1.1 / 0.2.2) still return superseded addresses -- upgrade before relying on SDK constants. Canonical: registry **Aqua** `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a`, router **AquaSwapVMRouter v1.0.2** `0x111111338c5091e8440b67b168bae16a668ac0de`, **KycNFT** `0x26FFc7D378E8e49Be2c483295A3e3E511F96a468` -- identical across the 13 Aqua chains. Both vanity contracts are source-verified on Ethereum (Blockscout, contract names `AquaRouter` / `AquaSwapVMRouter`); verify per chain before integrating. See [Verified contracts](/portal/documentation/aqua/reference/verified-contract-addresses) and [Supported chains](/portal/documentation/aqua/liquidity-layer/supported-chains). https://business.1inch.com/portal/assets/docs-v2/aqua/reference/verified-contract-addresses.md > **Status note (2026-07-29).** The **vanity-address redeployment is live**: canonical registry `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` (deployed 2026-07-19 on Ethereum), canonical router `0x111111338c5091e8440b67b168bae16a668ac0de` (2026-07-26, built from the `1inch/swap-vm` `v1.0.2` git tag). The 2026-07-16 pair (`0xe8026b...139b` / `0x016b41...b070`) moved to the stale/historical list below. **Verification status of the vanity deployment (2026-07-29, this docs review):** both contracts are **source-verified on Ethereum** (Blockscout; on-chain names `AquaRouter` / `AquaSwapVMRouter`) and their bytecode was spot-checked as present and identical-length on Ethereum and Base -- full 13-chain verification has not been re-run for this generation, so **verify per chain before integrating**. On-chain checks on Ethereum: the router's `AQUA()` returns the vanity registry (binding proven), `eip712Domain()` reports name `1inch SwapVM v1.0` / version `1.0.2`, and `owner()` returns `0x4134e66d52EfC4C77DD8Ccc952D87b9E92E0C352` for both contracts. **Deployment mechanism:** the vanity addresses were created through a deployer factory (Ethereum creation txs: factory `0x71481C3B9C6FBa3066AE84961EA22378A80cabe7`, method `deploy`, sender EOA `0x0BD61d605C64A857C3D94779aEf7cA295702b3A2`) -- unlike the 2026-07-16 generation, which used plain `CREATE` from the nonce-synchronized deployer EOA `0x72B4736F6e482DB07C4F3d7d3b90A24b2FedBe7a`. Either way, **address equality across chains does not prove identical bytecode or constructor args -- verify per chain.** Both repo READMEs remain stale (re-checked 2026-07-29) -- do not cite them. The **current SDK releases carry the canonical vanity addresses** (`@1inch/aqua-sdk` 0.3.0, `@1inch/swap-vm-sdk` 0.4.0); releases 0.1.1/0.2.2 and older still return superseded addresses. | Contract | Address (all 13 chains) | Notes | | -------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Aqua** (registry; on-chain name `AquaRouter`) | `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a` | Same `AquaRouter` source/bytecode as the superseded `0xe8026b...139b` registry; redeployed 2026-07-19 (Ethereum block 25567141) to the vanity address on every chain. **No EIP-712/ERC-5267 signing domain** (`eip712Domain()` reverts). Owner `0x4134e66d52EfC4C77DD8Ccc952D87b9E92E0C352` (verified on-chain on Ethereum) | | **AquaSwapVMRouter** v1.0.2 (the deployed SwapVM router) | `0x111111338c5091e8440b67b168bae16a668ac0de` | Deployed 2026-07-26 (Ethereum block 25618917); constructor: `aqua = 0x1111113c...a90a` (proven via `AQUA()`), EIP-712 name `1inch SwapVM v1.0`, version `1.0.2` (string), owner `0x4134e66d52EfC4C77DD8Ccc952D87b9E92E0C352`. Built from the **`v1.0.2` git tag** (cut 2026-07-26) -- adds `ProtocolFeeSkipped` | | **KycNFT** (taker credential, symbol `RES`) | `0x26FFc7D378E8e49Be2c483295A3e3E511F96a468` | **Not redeployed** -- unchanged through the 2026-07-16 and 2026-07 vanity redeployments; configured for all 13 chains including Robinhood Chain (4663) | **EIP-712 corrections.** Earlier revisions of these docs stated the router's EIP-712 domain name as `SwapVMRouter` and gave the registry a domain of `AquaRouter` v`1` (integer). Both were wrong: every production router generation (v1.0.1, the 2026-07-16 v1.0.2, and the current vanity router) uses the name `1inch SwapVM v1.0` with a string version (`1.0.1` / `1.0.2`), and the registry has **no EIP-712/ERC-5267 domain at all** -- there is no `EIP712` base contract in `1inch/aqua` and `eip712Domain()` reverts on-chain. Rebuild domain separators with the router's constructor values (note: `verifyingContract` changes with each router generation); never attempt typed-data signing against the registry. **Source provenance.** The current router (`0x111111338c...c0de`) is built from the **`v1.0.2` git tag** of `1inch/swap-vm` (cut 2026-07-26, target commit `32c687c2`): relative to v1.0.1 the tag changes only `src/instructions/Fee.sol` -- best-effort protocol-fee collection plus the `ProtocolFeeSkipped` event, confirmed present in the Blockscout-verified deployed source -- and leaves `AquaOpcodes.sol` and `ISwapVM` untouched. The earlier `0x016b41...b070` build was the v1.0.1 tag source recompiled with new constructor args (it predates the tag). The `main` branch (post-PR-#154) carries a renumbered opcode enum with different miss behaviour (`UnknownOpcode(uint256)` revert, no reserved gaps) that is **not deployed anywhere** -- never derive the live opcode table or revert behaviour from `main`. ## Aggregation executors (routing infrastructure, not part of the Aqua contract surface) The addresses below belong to the **2026-07-16 generation**, redeployed alongside Aqua/SwapVM because they depend on the registry address; same address on every chain: simple `0xd7d7FE53A5FC886e1B69c7213Fb0B24ccE1511a3`, protected `0xE3B94BF84310fDcec27A219cA0923F46D7137DC7`. The protected executor executes only commands signed by `AGGREGATION_EXECUTOR_SIGNER = 0x4447696e7878c0fa2c581768f04c3871a9a57b76`. Whether the executors were redeployed again for the vanity-registry generation has **not been confirmed in these docs** (2026-07-29) -- executors are internal 1inch routing infrastructure, deliberately **never source-verified** (team policy), and their constructor args **differ per chain** while the address stays identical -- a concrete illustration that shared-address equality does not imply identical init code. ## Stale / historical (do not cite as current) - `0xe8026bf31e58b738647319362581ab11be92139b` -- universal Aqua registry from 2026-07-16; superseded 2026-07-19 by the vanity registry `0x1111113c...a90a`. Historical/decoding only. - `0x016b417bc933370f5eacc40b1d58b015ac72b070` -- AquaSwapVMRouter v1.0.2-recompile from 2026-07-16, bound to `0xe8026b...139b`; superseded by the vanity router `0x111111338c...c0de`. Historical/decoding only. - `0x1111113db0e0ef9d0e3a50d5f094a3a57a26c0de` -- first vanity AquaSwapVMRouter (2026-07-19, Ethereum block 25567144); superseded 2026-07-26 by the current router (per the `@1inch/swap-vm-sdk` 0.4.0 constants note). Historical/decoding only. - `0x4a055aa172c98ec32de118b9b5b6ac8b4099a580` -- Aqua registry on the original 12 chains until 2026-07-16; superseded by `0xe8026b...139b`. Historical/decoding only. - `0x3c4758979ec30ca45857cabc2462a70699ed790e` -- AquaSwapVMRouter v1.0.1 (2026-06-16); superseded 2026-07-16. Historical/decoding only. - `0x7c2d4aa5c900c08004fadb1c0d953c5b099fec86` -- Robinhood-only registry (CREATE3, 2026-06-10); superseded by the universal registry. - `0xa5368dd669c5706256cf417b66e71a1efe1d331a` -- Robinhood-only SwapVM router (CREATE3, 2026-06-10); superseded by the universal router. - `0x499943e74fb0ce105688beee8ef2abec5d936d31` -- pre-deployment placeholder in the `1inch/aqua` README; never existed on-chain. - `0x8fdd04dbf6111437b44bbca99c28882434e0958f` -- stale router in the `1inch/swap-vm` README. - `0xdfd05fe230bfe7b212878414270c72c8345506fa` -- superseded router (2026-03-27, no KYC opcode); historical/decoding only. Strategies shipped against a superseded registry/router remain active on that deployment until the maker docks and re-ships on the current one -- decode their history there, but never route new orders to them. Ownership: per the PT1-690 deployment report (pipeline `aqua-redeploy` / `prod-v1.0.2`, executed 2026-07-16 16:28-16:58 UTC), the 2026-07-16 registry and router were deployed with owner `0x4134e66d52EfC4C77DD8Ccc952D87b9E92E0C352` on **every chain**; the **current vanity registry and router report the same owner** (verified on-chain via `owner()` on Ethereum, 2026-07-29). That owner is an **EOA, held as an interim arrangement** -- the final ownership transfer will occur once everything is set up and working (blockchain team, 2026-07-17). The KycNFT owner/minter remains the single EOA `0x0bd61D605c64A857c3d94779aEF7cA295702b3A2` (not a multisig; multisig migration proposed, not executed) -- the same EOA that sent the vanity-deploy factory transactions. See [Supported chains](/portal/documentation/aqua/liquidity-layer/supported-chains) for the chain table. --- ## Per-chain deployment All three contracts share one address on every chain. Verify on-chain before integrating; a shared address does not by itself guarantee identical bytecode or constructor args across chains -- check per chain. | Chain | Chain ID | Explorer (router) | | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------- | | Ethereum | `1` | [etherscan.io](https://etherscan.io/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Optimism | `10` | [optimistic.etherscan.io](https://optimistic.etherscan.io/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | BNB Chain | `56` | [bscscan.com](https://bscscan.com/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Gnosis | `100` | [gnosisscan.io](https://gnosisscan.io/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Unichain | `130` | [uniscan.xyz](https://uniscan.xyz/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Polygon | `137` | [polygonscan.com](https://polygonscan.com/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Sonic | `146` | [sonicscan.org](https://sonicscan.org/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | zkSync Era | `324` | [era.zksync.network](https://era.zksync.network/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Robinhood Chain | `4663` | [robinhoodchain.blockscout.com](https://robinhoodchain.blockscout.com/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Base | `8453` | [basescan.org](https://basescan.org/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Arbitrum | `42161` | [arbiscan.io](https://arbiscan.io/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Avalanche | `43114` | [snowtrace.io](https://snowtrace.io/address/0x111111338c5091e8440b67b168bae16a668ac0de) | | Linea | `59144` | [lineascan.build](https://lineascan.build/address/0x111111338c5091e8440b67b168bae16a668ac0de) | **Testnet:** from the 2026-07-16 pipeline run, that generation's four contracts (registry, router, both executors) went live on **Sepolia (chain ID** `11155111`). For the vanity generation, as of 2026-07-29 the **registry** is live on Sepolia at the canonical address but the **router is not yet** -- re-verify on-chain before testnet use. Sepolia is not part of the production Aqua chain set. ## Machine-readable The SDK constants `AQUA_CONTRACT_ADDRESSES[chainId]` (registry) and `AQUA_SWAP_VM_CONTRACT_ADDRESSES[chainId]` (router) are the intended integration path, and the **current releases carry the canonical vanity addresses** (`@1inch/aqua-sdk` 0.3.0, `@1inch/swap-vm-sdk` 0.4.0 -- verified from the published packages, 2026-07-29). Older releases are wrong: 0.1.1 -> `0x4a055a...` / Robinhood `0x7c2d4a...`; 0.2.2 -> `0x3c4758...` (its Robinhood entry was wrong even pre-redeploy -- it listed `0x3c4758...` while Robinhood's actual router at the time was `0xa5368d...`). Pin exact SDK versions 0.3.0 / 0.4.0, or pin from this page: registry `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a`, router `0x111111338c5091e8440b67b168bae16a668ac0de`, KycNFT `0x26FFc7D378E8e49Be2c483295A3e3E511F96a468`. For backfill, read each contract creation block from the block explorer (per-chain; Ethereum: registry 25567141, router 25618917); for older history, decode against the superseded set above. https://business.1inch.com/portal/assets/docs-v2/aqua/reference/data-and-analytics.md How to attribute volume and monitor strategies from on-chain data. Every field below is decoded from event data; see [Events & interfaces](./events-and-interfaces) for the exact signatures. ## Strategy identity A strategy is keyed by the tuple `(chainId, maker, app, strategyHash)`, where `strategyHash = keccak256(strategy)` (the `strategy` bytes are already `abi.encode(order)`, so no second encode) and is immutable after `ship()`. The router's `Swapped.orderHash` equals the strategy's `strategyHash`, which is the join key between fills and strategies. ## Volume attribution Attribute a fill to a strategy by joining `Swapped.orderHash == Shipped.strategyHash`. When aggregating across the aggregation router and Aqua, watch for **double counting**: a single user swap routed through Aqua produces one Aqua `Swapped` and may also appear in aggregation-layer accounting -- attribute to one layer per methodology and state which. ## Derived-metric methodology :::info Every derived number (volume, TVL-equivalent, APY, SLR) is an **estimate from public data, not an official 1inch figure**. Publish a method block with it: the formula, the price source, the price-as-of timestamp, and the methodology version. ::: ## Reference indexer A hosted subgraph is not currently available. Build a reference indexer over the five registry/router events keyed on `(maker, app, strategyHash)`. Backfill from each chain's deployment block (deployment-specific; read it from the contract's creation transaction) and reconcile poll vs subscribe as described on the Events page. --- ## Event ABI (copy-paste) The five events, source-verified (no `indexed` params). See [Events & interfaces](./events-and-interfaces) for topic0 hashes. ```solidity [ {"type":"event","name":"Shipped","anonymous":false,"inputs":[ {"name":"maker","type":"address"},{"name":"app","type":"address"}, {"name":"strategyHash","type":"bytes32"},{"name":"strategy","type":"bytes"}]}, {"type":"event","name":"Docked","anonymous":false,"inputs":[ {"name":"maker","type":"address"},{"name":"app","type":"address"}, {"name":"strategyHash","type":"bytes32"}]}, {"type":"event","name":"Pulled","anonymous":false,"inputs":[ {"name":"maker","type":"address"},{"name":"app","type":"address"}, {"name":"strategyHash","type":"bytes32"},{"name":"token","type":"address"},{"name":"amount","type":"uint256"}]}, {"type":"event","name":"Pushed","anonymous":false,"inputs":[ {"name":"maker","type":"address"},{"name":"app","type":"address"}, {"name":"strategyHash","type":"bytes32"},{"name":"token","type":"address"},{"name":"amount","type":"uint256"}]}, {"type":"event","name":"Swapped","anonymous":false,"inputs":[ {"name":"orderHash","type":"bytes32"},{"name":"maker","type":"address"},{"name":"taker","type":"address"}, {"name":"tokenIn","type":"address"},{"name":"tokenOut","type":"address"}, {"name":"amountIn","type":"uint256"},{"name":"amountOut","type":"uint256"}]} ] ``` ## Decode example (Dune / ethers) ```typescript -- Dune: Aqua volume by strategy (join fills to strategies on orderHash == strategyHash) select s.maker, sw.token_in, sw.token_out, sum(sw.amount_out) as out_volume, count(*) as fills from aqua_router_evt_Swapped sw join aqua_registry_evt_Shipped s on sw.orderHash = s.strategyHash group by 1,2,3; // ethers: decode a Swapped log const iface = new ethers.Interface(AQUA_EVENT_ABI); const { args } = iface.parseLog(log); // orderHash === strategyHash ``` ## Indexer completeness Three details decide whether a backfill is complete and correctly attributed: the per-chain start block, log classification, and how the join key is derived. The [Events & interfaces](./events-and-interfaces) page has the signatures and topic0 hashes; [Encoding](./encoding-reference) has the exact `strategy` byte layout referenced below. ### Per-chain deploy block Registry events (`Shipped`, `Docked`, `Pulled`, `Pushed`) are emitted by the `AquaRouter` registry, a different contract from the SwapVM router that emits `Swapped`. The two contracts are deployed independently and land at different blocks on every chain. Your `eth_getLogs` `fromBlock` for registry events must be the registry's own creation block. :::note **Do not reuse the swap-vm broadcast artifacts for the registry start block.** The `broadcast/**/run-latest.json` files in the swap-vm repo record the _SwapVM router_ deployment, not the `AquaRouter` registry. Read the registry's deploy block per chain from its own contract-creation transaction (the block of the tx that created the registry address), then use that as `fromBlock`. Using the router's block will silently skip or over-scan registry logs. ::: Pin one `(chainId -> registry address -> creation block)` row per chain in config and treat it as the backfill floor. Note that `ship()` emits a `Shipped` plus one `Pushed` per seeded token in the same transaction, so the earliest registry activity for a maker is always at or after that floor. ### Log classification by contract and topic0 None of the five events declare `indexed` parameters, so every log carries exactly one topic -- `topic0`, the event-signature hash -- and all payload fields live in `data`. Classify a raw log on two dimensions: | Dimension | Registry logs | Router logs | | --------------------------------- | ------------------------------------------------- | ---------------------- | | Emitting contract (`log.address`) | AquaRouter registry address | SwapVM router address | | `topic0` | Shipped / Docked / Pulled / Pushed signature hash | Swapped signature hash | | Topic count | 1 (no indexed params) | 1 (no indexed params) | Match on `(log.address, topic0)` together. The address alone separates registry from router; `topic0` alone separates the four registry event types from each other. Once matched, ABI-decode the non-indexed tuple from `data` to recover the fields. :::info **Join key.** `strategyHash = keccak256(strategy)` -- the keccak256 of the exact `strategy` bytes passed to `ship()` (verified at `aqua` `src/Aqua.sol`, not `keccak256(abi.encode(strategy))`). It is immutable after ship and is the `strategyHash` in every registry event. The router's `Swapped.orderHash` equals this value for Aqua orders -- see [Encoding](./encoding-reference) for why the shipped bytes and the order encoding coincide -- which is the fills-to-strategies join. ::: ### Warehouse schema sketch A minimal two-table schema keyed on `(chainId, maker, app, strategyHash)`. Store addresses and hashes as fixed-width binary; keep `strategy` raw so `strategyHash` is reproducible offline. ```solidity -- Strategies: one row per shipped strategy (keyed by identity tuple) CREATE TABLE strategies ( chain_id BIGINT NOT NULL, maker BYTEA NOT NULL, -- 20 bytes app BYTEA NOT NULL, -- 20 bytes strategy_hash BYTEA NOT NULL, -- 32 bytes = keccak256(strategy) strategy BYTEA NOT NULL, -- raw shipped bytes (Shipped.strategy) status TEXT NOT NULL DEFAULT 'active', -- 'active' | 'docked' shipped_block BIGINT NOT NULL, shipped_tx BYTEA NOT NULL, shipped_at TIMESTAMPTZ NOT NULL, PRIMARY KEY (chain_id, maker, app, strategy_hash) ); -- Fills: one row per Swapped log (orderHash = strategy_hash) CREATE TABLE fills ( chain_id BIGINT NOT NULL, order_hash BYTEA NOT NULL, -- 32 bytes; joins strategies.strategy_hash maker BYTEA NOT NULL, taker BYTEA NOT NULL, token_in BYTEA NOT NULL, token_out BYTEA NOT NULL, amount_in NUMERIC(78,0) NOT NULL, -- uint256 amount_out NUMERIC(78,0) NOT NULL, -- uint256 block_number BIGINT NOT NULL, tx_hash BYTEA NOT NULL, log_index INTEGER NOT NULL, PRIMARY KEY (chain_id, tx_hash, log_index) ); CREATE INDEX fills_by_strategy ON fills (chain_id, order_hash); -- Attribute fills to strategies SELECT s.maker, s.app, f.token_in, f.token_out, SUM(f.amount_out) AS out_volume, COUNT(*) AS n_fills FROM fills f JOIN strategies s ON s.chain_id = f.chain_id AND s.strategy_hash = f.order_hash GROUP BY 1,2,3,4; ``` Apply `Docked` to flip `status`, and fold `Pulled`/`Pushed` into a per-`(chainId, maker, app, strategyHash, token)` running balance if you track inventory. The identity tuple is stable across all of them. ### Dune caveat :::note Because no parameter is `indexed`, Dune's decoded `evt_*` tables and any manual decode must pull **every field from** `data`; there are no `topic1..topic3` columns to filter on. Select rows by `topic0` (the event-signature hash) against `logs`, and scope by the emitting contract address to keep registry and router logs apart -- `topic0` plus `contract_address`, never a topic-indexed field. When decoding by hand, remember dynamic `bytes` (`Shipped.strategy`) is offset-encoded within `data`, not a fixed slot. ::: ## Cross-layer volume de-dup (exact rule) The Volume attribution note above tells you double counting _can_ happen; this section replaces that directional guidance with an exact rule. A single user swap that the 1inch aggregation router lands on Aqua produces **one Aqua** `Swapped` **log and one aggregation-layer entry, both in the same transaction**. Counting both inflates any combined total by exactly the overlapping set. Pick one of the two equivalent rules below and state which you used in the method block. :::info **The overlap is identifiable two ways, and they agree.** The aggregation-routed fills are exactly the `Swapped` rows whose `taker` is a permitted resolver _and_ whose `(chain_id, tx_hash)` matches an aggregation-router transaction. `Swapped.taker` is `msg.sender` -- the address that called the SwapVM router (verified at `swap-vm` `src/SwapVM.sol`, `emit Swapped(orderHash, order.maker, msg.sender, ...)`). For an aggregation-routed swap that caller is the resolver contract; for a direct Aqua swap it is an ordinary taker. ::: ### Rule A -- union and de-dup on `(chain_id, tx_hash)` Build the combined figure as the union of your aggregation source and Aqua fills, then collapse to one row per `(chain_id, tx_hash)` so a swap that traversed both layers is counted once. Choose the surviving layer deterministically and record the choice. ``` -- Combined 1inch volume, each swap counted exactly once WITH agg AS ( -- your aggregation-router source, priced SELECT chain_id, tx_hash, amount_usd, 'aggregation' AS layer FROM aggregation_fills ), aqua AS ( -- Aqua Swapped fills, priced to the same numeraire SELECT f.chain_id, f.tx_hash, (f.amount_in / p.scale) * p.price AS amount_usd, 'aqua' AS layer FROM fills f JOIN prices p ON p.token = f.token_in AND p.as_of = :price_as_of ), unified AS ( SELECT * FROM agg UNION ALL SELECT * FROM aqua ) SELECT SUM(amount_usd) AS combined_volume_usd FROM ( SELECT DISTINCT ON (chain_id, tx_hash) chain_id, tx_hash, amount_usd FROM unified ORDER BY chain_id, tx_hash, layer -- deterministic: 'aggregation' wins ties ) deduped; ``` ### Rule B -- scope by permitted resolver Equivalently, reconcile on the Aqua side alone: the resolver-executed fills are the aggregation-routed overlap, and they match the aggregation router's own transaction 1:1 on `(chain_id, tx_hash)`. Attribute that resolver-taker set to exactly one layer; the remaining non-resolver takers are direct Aqua flow and are never in the aggregation source, so they cannot be double counted. ```solidity -- Aggregation-routed Aqua volume (the reconcilable, resolver-executed set) SELECT SUM(f.amount_in) AS routed_volume_raw -- raw uint256 units; price outside SQL FROM fills f JOIN permitted_resolvers r ON r.chain_id = f.chain_id AND r.resolver = f.taker; -- Swapped.taker = msg.sender -- Direct (non-resolver) Aqua volume, attributed separately SELECT SUM(f.amount_in) AS direct_volume_raw FROM fills f LEFT JOIN permitted_resolvers r ON r.chain_id = f.chain_id AND r.resolver = f.taker WHERE r.resolver IS NULL; ``` :::note `Swapped.taker` is `msg.sender`, the immediate caller of the SwapVM router. If a resolver executes through an intermediary contract, `msg.sender` is that intermediary, not the resolver EOA -- add a `tx.origin` check against your resolver allowlist for those cases. Maintain the `permitted_resolvers` set per `chain_id`; a stale allowlist silently misclassifies routed fills as direct and reopens the double count. ::: ## Shared Liquidity Ratio (SLR): worked formula SLR measures **capital reuse** -- how much swap flow a strategy's committed, on-chain-available inventory supported over a window. It is a turnover/availability figure, **not leverage and not yield**: the denominator is the maker's own committed balance (no borrowed funds), and the ratio says nothing about return. Like every number on this page it is an estimate from public data; publish the method block with it. ### Inputs | Symbol | Meaning | Source | | ------ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `V` | Swap flow routed by the strategy over the window, in a common numeraire | `Swapped.amountIn` (fills), priced at `price_as_of` | | `C` | Committed, available inventory of the strategy, same numeraire | `rawBalances(maker, app, strategyHash, token)` -> `uint248 balance`, priced at `price_as_of` | | `SLR` | `V / C` -- units of flow served per unit of committed capital | derived | `rawBalances` is the on-chain view `rawBalances(address maker, address app, bytes32 strategyHash, address token) returns (uint248 balance, uint8 tokensCount)` (verified at `aqua` `src/Aqua.sol`). Snapshot `balance` at the price-as-of block, or reconstruct the identical value offline by folding `Pushed` minus `Pulled` deltas per `(chainId, maker, app, strategyHash, token)`. Both `balance` and `amountIn` are raw integer token units -- divide by `10^decimals` before pricing. ### Arithmetic ``` SLR = V / C V = sum over fills in [from_block, to_block] of (amountIn / 10^dec_in) * price(tokenIn) C = sum over strategy tokens of (rawBalances.balance / 10^dec) * price(token) -- SLR per strategy, mirroring the volume-attribution join (order_hash = strategy_hash) WITH vol AS ( -- V: window flow, priced to numeraire SELECT f.chain_id, s.maker, s.app, f.order_hash AS strategy_hash, SUM((f.amount_in / p.scale) * p.price) AS v_usd FROM fills f JOIN strategies s ON s.chain_id = f.chain_id AND s.strategy_hash = f.order_hash JOIN prices p ON p.token = f.token_in AND p.as_of = :price_as_of WHERE f.block_number BETWEEN :from_block AND :to_block GROUP BY 1,2,3,4 ), cap AS ( -- C: committed inventory from rawBalances(), priced SELECT b.chain_id, b.maker, b.app, b.strategy_hash, SUM((b.balance / p.scale) * p.price) AS c_usd FROM raw_balances b -- one row per (chain,maker,app,strategy_hash,token) JOIN prices p ON p.token = b.token AND p.as_of = :price_as_of GROUP BY 1,2,3,4 ) SELECT v.chain_id, v.maker, v.app, v.strategy_hash, v.v_usd, c.c_usd, v.v_usd / NULLIF(c.c_usd, 0) AS slr FROM vol v JOIN cap c USING (chain_id, maker, app, strategy_hash); ``` ### Worked sample ``` Strategy committed inventory (rawBalances at price_as_of): USDC 1,000,000 (6 dec) x $1.00 = $1,000,000 WETH 300 (18 dec) x $3,000.00 = $ 900,000 C = $1,900,000 7-day window flow (sum of amountIn, priced): V = $9,500,000 SLR = V / C = 9,500,000 / 1,900,000 = 5.0 ``` Read: over the window, each unit of committed, available inventory supported **5 units of swap flow** -- a capital-reuse measure. It does not imply borrowed capital (no leverage) or any return (no yield). :::info **Method block (publish with the number).** Formula `SLR = V / C`, methodology version; window `[from_block, to_block]` and its UTC timestamps; `V` from `Swapped.amountIn`, `C` from `rawBalances().balance` snapshotted at `price_as_of`; price source and the `price_as_of` timestamp; the numeraire. State whether cross-layer de-dup (Rule A or Rule B above) was applied to `V`. As with all figures here, this is an estimate from public data, not an official 1inch number. ::: https://business.1inch.com/portal/assets/docs-v2/aqua/reference/sdk-overview.md Two published TypeScript packages in the `1inch/sdks` monorepo drive Aqua end-to-end. (Python/Rust are placeholders today.) | Package | Role | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `@1inch/aqua-sdk` | Registry writes and events: `AquaProtocolContract.ship()`/`dock()` -> `CallInfo`, `calculateStrategyHash()`, `encodeShip/DockCallData`, `AQUA_CONTRACT_ADDRESSES`, and `ShippedEvent`/`DockedEvent`/`PulledEvent`/`PushedEvent.fromLog()` | | `@1inch/swap-vm-sdk` | Program building and router: `AquaProgramBuilder`/`ProgramBuilder`, strategy builders `AquaXYCAmmStrategy.new()`/`.newConcentrate()` and `AquaPeggedAmmStrategy` (fluent `withTxOriginAccessToken`, `withFeeTokenIn`, `withProtocolFee`, `withDecayPeriod`, `withSalt`), `Order`/`MakerTraits`/`TakerTraits`, `SwapVMContract.quote()/swap()/hashOrder()`, `AQUA_SWAP_VM_CONTRACT_ADDRESSES`, `SwappedEvent.fromLog()` | ## Install ```bash pnpm add @1inch/aqua-sdk @1inch/swap-vm-sdk viem ``` ## Provide liquidity (maker) ```typescript // build the program with the recommended high-level builder const strategy = AquaXYCAmmStrategy.newConcentrate({ rawPriceMin, rawPriceMax }) .withFeeTokenIn(30) // conventional bps: 30 = 0.30% (scaled x100000 internally) .withTxOriginAccessToken(aquaKycToken); // attach the taker access gate const order = Order.new({ maker, program: strategy.build(), traits: MakerTraits.default() }); const { to, data } = aqua.ship({ app: AQUA_SWAP_VM_CONTRACT_ADDRESSES[chainId], strategy: order.encode(), amountsAndTokens }); // sign in the maker wallet and broadcast via your own RPC provider ``` ## Take a swap (taker) ```typescript // The deployed v1.0.2 router takes the token pair explicitly (tokenIn/tokenOut = direction); // the SDK mirrors this. amount is the taker's exact-in input in base units. const args = { order, tokenIn: USDC, tokenOut: WETH, amount, takerTraits }; const out = await swapVm.quote(args); // static simulation -> (amountIn, amountOut, orderHash) const tx = swapVm.swap(args); // pull() + push() atomically // decode fills: SwappedEvent.fromLog(log) // orderHash === strategyHash ``` **Runtime caveat.** The SDK can encode every SwapVM opcode, but deployed `AquaSwapVMRouter` contracts only execute the `aquaInstructions` subset. Use `AquaProgramBuilder` (pre-wired to `aquaInstructions`), which rejects an unregistered opcode at build time (`OpcodeNotFound`). If you hand-encode one anyway, at runtime a reserved gap index is a no-op (`_notInstruction`) and an index past the table reverts with a Solidity array-out-of-bounds `Panic(0x32)` -- there is no named runtime error. Pin versions as build-time; strategy bytecode is opcode-table-versioned and not portable across router versions. ## SDK additions: Swap VM taker & program building The Aqua developer surface ships as two TypeScript packages published from the `1inch/sdks` monorepo. The aqua-sdk section above covers the maker-side registry calls (`ship`/`dock`); this section adds the **Swap VM** package that a taker uses to quote and execute swaps, plus the `AquaProgramBuilder` flow for hand-assembling a strategy program. ### Packages & exact versions | Package | Version | viem | Other runtime deps | | -------------------- | ------- | ----------------------------- | ------------------------------------------------------------ | | `@1inch/aqua-sdk` | `0.3.0` | `^2.48.4` (direct dependency) | `@1inch/sdk-core 0.1.2`, `tslib` | | `@1inch/swap-vm-sdk` | `0.4.0` | `^2.21.0` (_peer_ dependency) | `@1inch/byte-utils ^3.1.7`, `@1inch/sdk-core 0.1.2`, `tslib` | Note: `@1inch/aqua-sdk` 0.3.0 and `@1inch/swap-vm-sdk` 0.4.0 carry the **canonical vanity contract addresses** (registry `0x1111113ccf...a90a`, router `0x111111338c...c0de`) in their address constants -- verified from the published packages on 2026-07-29. Earlier releases (0.1.1 / 0.2.2 and older) still return superseded addresses: upgrade, or take addresses from the [Verified Contract Addresses](/portal/documentation/aqua/reference/verified-contract-addresses) page. `@1inch/sdk-core` is a real dependency of _both_ packages (declared as `workspace:*` in the monorepo, resolved to a pinned version on publish). You do not install it separately: the primitives you need -- `Address`, `HexString`, `NetworkEnum`, and the `CallInfo` type -- are re-exported from each SDK's root, so import them from `@1inch/swap-vm-sdk` / `@1inch/aqua-sdk` directly. Because `@1inch/swap-vm-sdk` declares **viem as a peer dependency**, you must install viem yourself. `@1inch/aqua-sdk` pins viem `^2.48.4` as a direct dependency, so install a viem that satisfies both -- `^2.48.4` is the safe floor. ```bash # viem must be installed explicitly (peer dep of swap-vm-sdk) pnpm add @1inch/swap-vm-sdk @1inch/aqua-sdk viem@^2.48.4 ``` **Version drift & pinning.** The published npm package and the `1inch/swap-vm` GitHub repo are _not_ the same build, and they have diverged in ways that will silently emit broken calldata if you cross them: - **Constructor arity.** `AquaSwapVMRouter`'s constructor takes **5 arguments** -- `constructor(address aqua, address weth, address owner, string name, string version)` (verified in `src/routers/AquaSwapVMRouter.sol`). Builds floating around npm / templates have been observed with a 3-argument constructor, plus different import paths and different `TakerTraits.Args` field layouts. - **Canonical source of truth.** Treat the _deployed_ `AquaSwapVMRouter v1.0.2` at `0x111111338c5091e8440b67b168bae16a668ac0de` (13 Aqua chains) together with the `1inch/swap-vm` **`v1.0.2` git tag** as authoritative -- not the `main` branch, whose refactored surface is undeployed. The superseded builds (`0x016b41...b070`, `0x8fdd04...958f`) are decode-only -- do not target them. - **Pin exact versions.** Install `@1inch/swap-vm-sdk` and `@1inch/aqua-sdk` with exact versions (no `^`) and lock your lockfile, so an off-cycle patch cannot shift the opcode table under you. - **Stale opcode enums.** A hand-maintained TypeScript opcode enum (e.g. a `SwapVMHelpers.ts` that lists opcode numbers) can drift out of sync with the on-chain `AquaOpcodes` table -- the numbers stop matching the deployed Solidity (duplicate/reordered entries), and encoding still _succeeds_ while producing a program that executes the wrong instruction. Do not trust a standalone enum: build programs through `AquaProgramBuilder`, which is pre-wired with `aquaInstructions` and validates every opcode against that set. (For the same reason, note the README snippet that calls `Order.parse(...)` is stale -- the shipped method is `Order.decode(...)`.) ### Taker flow: quote() then swap() A taker never builds a program. It fetches the maker's encoded order (from the `Shipped` event or an API), rehydrates it with `Order.decode`, simulates `quote()` to read the output amount, applies its own slippage/receiver/deadline via `TakerTraits`, then sends `swap()`. The Swap VM has no dedicated `minReturn` or `slippage` parameter. Slippage protection is the single `TakerTraits.threshold` field: in `exactIn` mode it is the **minimum acceptable output** (revert if `amountOut < threshold`); in `exactOut` mode it is the maximum acceptable input. The receiver override is `customReceiver` (defaults to the taker), and `deadline` is a `uint40` Unix-seconds guard (`0n` = no deadline). ```solidity import { Order, HexString, TakerTraits, Address, AQUA_SWAP_VM_CONTRACT_ADDRESSES, NetworkEnum, SwapVMContract, SwappedEvent, ABI, } from '@1inch/swap-vm-sdk' import { decodeFunctionResult } from 'viem' const chainId = NetworkEnum.ETHEREUM const swapVM = new SwapVMContract(AQUA_SWAP_VM_CONTRACT_ADDRESSES[chainId]) const USDC = new Address('0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48') const WETH = new Address('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') // Maker order, fetched from the Shipped event or the API, then rehydrated. const encodedOrder = '0x...' const order = Order.decode(new HexString(encodedOrder)) const srcAmount = 100n * 10n ** 6n // 100 USDC (exactIn) // 1) Simulate quote() with a permissive TakerTraits to read amountOut. // quote() returns (amountIn, amountOut, orderHash) per the SwapVM ABI. const quoteParams = { order, tokenIn: USDC, tokenOut: WETH, amount: srcAmount, takerTraits: TakerTraits.default(), } const sim = await taker.call(swapVM.quote(quoteParams)) // taker = viem client w/ publicActions const [amountIn, amountOut, orderHash] = decodeFunctionResult({ abi: ABI.SWAP_VM_ABI, functionName: 'quote', data: sim.data!, }) // 2) Turn the quote into slippage-protected TakerTraits. const slippageBps = 50n // 0.5% const minOut = (amountOut * (10_000n - slippageBps)) / 10_000n const takerTraits = TakerTraits.new({ exactIn: true, // amount is the input; threshold is the min output threshold: minOut, // revert if amountOut < minOut customReceiver: new Address('0xReceiver...'), // omit to receive as the taker deadline: BigInt(Math.floor(Date.now() / 1000) + 300), // uint40 unix seconds }) // 3) Send the swap with the protected traits (same order/tokens/amount). const swapTx = swapVM.swap({ ...quoteParams, takerTraits }) const hash = await taker.send(swapTx) const receipt = await taker.waitForTransactionReceipt({ hash }) // 4) Decode the Swapped event from the router logs. for (const log of receipt.logs) { try { const swapped = SwappedEvent.fromLog(log) console.log(swapped.orderHash, swapped.maker, swapped.taker) console.log(swapped.tokenIn, swapped.tokenOut) console.log(swapped.amountIn, swapped.amountOut) break } catch { // not a Swapped log; skip } } ``` `swapVM.quote(...)` and `swapVM.swap(...)` both return a `CallInfo` (`{ to, data, value }`) -- you pass it straight to your viem client's `call` / `sendTransaction`. `SwappedEvent.fromLog` throws on a non-matching log, so the try/catch scan above is the idiomatic way to pull it out of a receipt. ### Program building: AquaProgramBuilder (Path B) There are two ways to produce the `SwapVmProgram` a maker ships. **Path A** is the high-level strategy -- `AquaXYCAmmStrategy.newConcentrate({ rawPriceMin, rawPriceMax }).build()` -- which is enough for standard AMM pools. **Path B**, below, drops to `AquaProgramBuilder` and appends instructions by hand when you need control the strategy classes don't expose. **Only** `aquaInstructions` **execute on the deployed router.** The SDK can encode/decode the _full_ Swap VM opcode set, but today's on-chain `AquaSwapVMRouter` implements only the Aqua subset. Any program using an opcode outside `aquaInstructions` will encode fine and then revert at runtime. `AquaProgramBuilder` is pre-wired with `aquaInstructions` and its `.add()` throws (listing the supported opcode IDs) if you hand it anything outside that set -- use it, not the bare `ProgramBuilder`, for anything meant to run on Aqua now. **Instruction order matters.** Instructions execute in the order you append them, and the builder does not reorder. The valid pipeline mirrors `AquaXYCAmmStrategy.build()`: dynamic-balance / liquidity setup first (e.g. `concentrateGrowLiquidity2D`), then any fee-on-input, then the swap formula (`xycSwapXD`), then trailing controls such as `salt`. ```typescript import { AquaProgramBuilder, instructions, Order, MakerTraits, Address } from "@1inch/swap-vm-sdk"; const { concentrate, fee } = instructions; const { ONE_E18 } = concentrate; const maker = new Address("0xMaker..."); // Build the program by appending instructions in execution order. const builder = new AquaProgramBuilder(); // (1) Dynamic balances: concentrate/grow liquidity within a price band. // Price P = tokenGt / tokenLt in 1e18 fixed point. // e.g. 1500-3000 USDC per WETH -> P = WETH-per-USDC = 1/3000 .. 1/1500. builder.add( concentrate.concentrateGrowLiquidity2D.createIx( concentrate.ConcentrateGrowLiquidity2DArgs.fromRawPrices(ONE_E18 / 3000n, ONE_E18 / 1500n) ) ); // (2) Fee: 5 bps taker fee charged on amountIn. builder.add(fee.flatFeeAmountInXD.createIx(fee.FlatFeeArgs.fromBps(5))); // (3) Swap formula: constant-product XYC swap (always the core step). builder.xycSwapXD(); // (4) Control: salt makes the order hash unique (replay protection). builder.salt({ salt: 1n }); const program = builder.build(); // -> SwapVmProgram // Embed the program in an Order the maker will ship. const order = Order.new({ maker, program, traits: MakerTraits.default() }); const encodedOrder = order.encode(); // HexString, pass as `strategy` to aqua.ship(...) ``` Several of the fee/control opcodes above also have fluent shorthands on the builder -- `builder.flatFeeAmountInXD({ fee })`, `builder.concentrateGrowLiquidity2D({ sqrtPriceMin, sqrtPriceMax })`, `builder.decayXD({ decayPeriod })`, `builder.salt({ salt })`, and taker-gating guards like `builder.onlyTakerTokenBalanceGte({ token, minAmount })`. Use `.createIx(...)` + `.add(...)` when you want the args class directly (e.g. `fromRawPrices` / `fromBps` helpers), and the fluent methods for terseness. To round-trip an existing program, `AquaProgramBuilder.decode(program)` maps the on-chain opcode indices back to typed instructions -- which only works when your SDK's `aquaInstructions` table matches the deployed router, the exact reason to pin versions. ## Browser wallet & raw ABI This section shows two lower-level integration paths that sit underneath the TypeScript SDKs: driving Aqua from a **browser wallet with viem** over an injected EIP-1193 provider, and **encoding calldata by hand** (Python/Go/Rust) directly against the on-chain function ABIs. Both paths sign in the user's wallet and broadcast through _your own_ RPC -- no hosted intermediary is involved. Contracts referenced here (identical address across every listed EVM network): the **Aqua registry** (`IAqua`) at `0x1111113ccf1426a8e30e2bff5e005d929bf6a90a`, and the **AquaSwapVMRouter** (`ISwapVM`, the swap engine and the _app_ for AMM strategies) at `0x111111338c5091e8440b67b168bae16a668ac0de`. Makers keep custody in their own wallet; Aqua moves funds only when a strategy pulls. ### 1. Browser wallet with viem (injected EIP-1193 provider) Use two viem clients: a `walletClient` bound to `window.ethereum` for signing, and a `publicClient` bound to **your own RPC endpoint** for reads and quote simulation. Nothing is routed through a hosted gateway. ```solidity import { createWalletClient, createPublicClient, custom, http, parseUnits } from 'viem' import { mainnet } from 'viem/chains' import { AquaProtocolContract, AQUA_CONTRACT_ADDRESSES, } from '@1inch/aqua-sdk' import { Order, MakerTraits, TakerTraits, AquaXYCAmmStrategy, SwapVMContract, AQUA_SWAP_VM_CONTRACT_ADDRESSES, Address, HexString, NetworkEnum, instructions, ABI, } from '@1inch/swap-vm-sdk' import { decodeFunctionResult } from 'viem' const chainId = NetworkEnum.ETHEREUM // Signs via the browser wallet; broadcasts through the wallet's provider. const walletClient = createWalletClient({ chain: mainnet, transport: custom(window.ethereum) }) // Reads & quote simulation go through YOUR OWN RPC -- not a hosted endpoint. const publicClient = createPublicClient({ chain: mainnet, transport: http('https://YOUR_OWN_RPC') }) // Step A -- connect an EOA const [account] = await walletClient.requestAddresses() const USDC = new Address('0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48') const WETH = new Address('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') const registry = AQUA_CONTRACT_ADDRESSES[chainId] // Aqua registry const swapVM = AQUA_SWAP_VM_CONTRACT_ADDRESSES[chainId] // AquaSwapVMRouter (= app) const erc20Approve = [{ type: 'function', name: 'approve', stateMutability: 'nonpayable', inputs: [{ name: 'spender', type: 'address' }, { name: 'value', type: 'uint256' }], outputs: [{ name: '', type: 'bool' }], }] // Step B -- maker grants the Aqua REGISTRY an ERC-20 allowance for the liquidity it will provide await walletClient.writeContract({ address: USDC.toString(), abi: erc20Approve, functionName: 'approve', args: [registry.toString(), parseUnits('10000', 6)], account, chain: mainnet, }) // Step C -- build a strategy program with the swap-vm-sdk builder, wrap it in an Order const { ONE_E18 } = instructions.concentrate const program = AquaXYCAmmStrategy.newConcentrate({ rawPriceMin: ONE_E18 / 3000n, // price P = tokenGt/tokenLt in 1e18 fixed-point rawPriceMax: ONE_E18 / 1500n, }).build() const order = Order.new({ maker: new Address(account), program, traits: MakerTraits.default(), // Aqua-authenticated (no signature) }) // Step D -- ship() via the aqua-sdk; the app is the AquaSwapVMRouter const aqua = new AquaProtocolContract(registry) const shipTx = aqua.ship({ app: new Address(swapVM.toString()), strategy: order.encode(), // abi.encode of the Order tuple amountsAndTokens: [ { token: USDC, amount: parseUnits('10000', 6) }, { token: WETH, amount: parseUnits('5', 18) }, ], }) // Signed in the wallet, broadcast through the wallet's provider (your infra -- not a hosted gateway) await walletClient.sendTransaction({ to: shipTx.to, data: shipTx.data, value: shipTx.value, account, chain: mainnet, }) ``` Any taker (possibly a different EOA) then swaps against that shipped strategy. The maker's encoded `Order` is available from the Aqua `Shipped` event (its `strategy` field). The taker approves the **router** for the input token, simulates `quote()` over your own RPC, then sends `swap()`. ```typescript const swapVMContract = new SwapVMContract(swapVM); // Reconstruct the maker Order from the Shipped event's `strategy` bytes const makerOrder = Order.decode(new HexString(encodedStrategyFromShippedEvent)); const srcAmount = parseUnits("100", 6); // 100 USDC in const params = { order: makerOrder, tokenIn: USDC, tokenOut: WETH, amount: srcAmount, takerTraits: TakerTraits.default() // exact-in, no hooks }; // Taker approves the ROUTER to pull the input token await walletClient.writeContract({ address: USDC.toString(), abi: erc20Approve, functionName: "approve", args: [swapVM.toString(), srcAmount], account, chain: mainnet }); // quote() is a read -- simulate it via eth_call on YOUR OWN RPC const sim = await publicClient.call(swapVMContract.quote(params)); const [amountIn, amountOut] = decodeFunctionResult({ abi: ABI.SWAP_VM_ABI, functionName: "quote", data: sim.data }); // swap() -- signed in the wallet, broadcast through your provider const swapTx = swapVMContract.swap(params); await walletClient.sendTransaction({ to: swapTx.to, data: swapTx.data, value: swapTx.value, account, chain: mainnet }); ``` Every write above is signed in the user's wallet and broadcast through the transport _you_ configure. If instead you want a hosted API, private/protected transaction broadcast, or an MCP integration, that is a separate 1inch Business offering -- see `business.1inch.com/portal/documentation`. Do not route these calls through any hosted gateway to use Aqua. ### 2. Encode ship / dock / pull / push / quote / swap without the TS SDK The SDKs are thin ABI encoders. A Python (`eth-abi`/`web3.py`), Go (`abigen`), or Rust (`ethers-rs`/`alloy`) team can encode the exact same calldata from the function ABIs below and broadcast it through their own RPC (`eth_sendRawTransaction`); read `quote()` via `eth_call`. These signatures are taken from the deployed contracts -- `IAqua` on the registry and the deployed `AquaSwapVMRouter`'s `ISwapVM`. | Contract | Function | Selector | Mutability | | ---------------- | -------------------------------------------------------------- | ------------ | ---------------------- | | Aqua registry | `ship(address,bytes,address[],uint256[])` | `0xf50b870f` | nonpayable -> `bytes32` | | Aqua registry | `dock(address,bytes32,address[])` | `0x28defc17` | nonpayable | | Aqua registry | `pull(address,bytes32,address,uint256,address)` | `0xb00bbd10` | nonpayable | | Aqua registry | `push(address,address,bytes32,address,uint256)` | `0x47d72768` | nonpayable | | AquaSwapVMRouter | `hash((address,uint256,bytes))` | `0xf5d08521` | view -> `bytes32` | | AquaSwapVMRouter | `quote((address,uint256,bytes),address,address,uint256,bytes)` | `0x44aa5f14` | view (eth_call) | | AquaSwapVMRouter | `swap((address,uint256,bytes),address,address,uint256,bytes)` | `0xf4d2d412` | nonpayable | The `Order` tuple is `(address maker, uint256 traits, bytes data)`, where `traits` is the packed `MakerTraits` bitfield and `data` is `hooksData ++ program`. Full JSON ABI for the seven entry points: ```solidity [ { "type": "function", "name": "ship", "stateMutability": "nonpayable", "inputs": [ { "name": "app", "type": "address" }, { "name": "strategy", "type": "bytes" }, { "name": "tokens", "type": "address[]" }, { "name": "amounts", "type": "uint256[]" } ], "outputs": [{ "name": "strategyHash", "type": "bytes32" }] }, { "type": "function", "name": "dock", "stateMutability": "nonpayable", "inputs": [ { "name": "app", "type": "address" }, { "name": "strategyHash", "type": "bytes32" }, { "name": "tokens", "type": "address[]" } ], "outputs": [] }, { "type": "function", "name": "pull", "stateMutability": "nonpayable", "inputs": [ { "name": "maker", "type": "address" }, { "name": "strategyHash", "type": "bytes32" }, { "name": "token", "type": "address" }, { "name": "amount", "type": "uint256" }, { "name": "to", "type": "address" } ], "outputs": [] }, { "type": "function", "name": "push", "stateMutability": "nonpayable", "inputs": [ { "name": "maker", "type": "address" }, { "name": "app", "type": "address" }, { "name": "strategyHash", "type": "bytes32" }, { "name": "token", "type": "address" }, { "name": "amount", "type": "uint256" } ], "outputs": [] }, { "type": "function", "name": "hash", "stateMutability": "view", "inputs": [ { "name": "order", "type": "tuple", "components": [ { "name": "maker", "type": "address" }, { "name": "traits", "type": "uint256" }, { "name": "data", "type": "bytes" } ] } ], "outputs": [{ "name": "", "type": "bytes32" }] }, { "type": "function", "name": "quote", "stateMutability": "view", "inputs": [ { "name": "order", "type": "tuple", "components": [ { "name": "maker", "type": "address" }, { "name": "traits", "type": "uint256" }, { "name": "data", "type": "bytes" } ] }, { "name": "tokenIn", "type": "address" }, { "name": "tokenOut", "type": "address" }, { "name": "amount", "type": "uint256" }, { "name": "takerTraitsAndData", "type": "bytes" } ], "outputs": [ { "name": "amountIn", "type": "uint256" }, { "name": "amountOut", "type": "uint256" }, { "name": "orderHash", "type": "bytes32" } ] }, { "type": "function", "name": "swap", "stateMutability": "nonpayable", "inputs": [ { "name": "order", "type": "tuple", "components": [ { "name": "maker", "type": "address" }, { "name": "traits", "type": "uint256" }, { "name": "data", "type": "bytes" } ] }, { "name": "tokenIn", "type": "address" }, { "name": "tokenOut", "type": "address" }, { "name": "amount", "type": "uint256" }, { "name": "takerTraitsAndData", "type": "bytes" } ], "outputs": [ { "name": "amountIn", "type": "uint256" }, { "name": "amountOut", "type": "uint256" }, { "name": "orderHash", "type": "bytes32" } ] } ] ``` **Getting the operands.** A raw taker needs the maker's `Order` tuple and a `takerTraitsAndData` blob: - **Order tuple** -- the Aqua registry emits `Shipped(address maker, address app, bytes32 strategyHash, bytes strategy)`. The `strategy` field is exactly `abi.encode((address,uint256,bytes))`; ABI-decode it to `(maker, traits, data)` and pass it straight into `quote`/`swap`. Note `strategyHash == keccak256(strategy)`, which is also the order hash used for Aqua balance lookups. - **takerTraitsAndData** -- a packed blob of ten `uint16` section offsets, a `uint16` flags word, then the referenced sections (threshold, receiver, deadline, hook/callback data, instruction args) and an optional signature. For a plain **exact-in swap with no hooks, no threshold and no custom receiver**, all offsets are zero and only two flag bits are set -- `exactIn` (bit 0) and `useTransferFromAndAquaPush` (bit 6) -- giving flags `0x0041` and the constant value: ```solidity takerTraitsAndData = 0x00000000000000000000000000000000000000000041 // 10 zero uint16 offsets flags // 0x0041 = exactIn | useTransferFromAndAquaPush ``` For anything beyond the default (thresholds/slippage, deadlines, hooks, callbacks, a custom receiver, or signature-based rather than Aqua-authenticated orders) the offset table is non-trivial -- mirror the packing exactly, or generate the blob once with the SDK's `TakerTraits` and reuse the bytes from your own language. **Broadcasting.** ABI-encode the calldata, sign locally, and send via `eth_sendRawTransaction` to **your own RPC provider**. Read `quote()` with `eth_call` against the router. If you need a hosted API, protected/private broadcast, or an MCP surface instead of running your own RPC, that is offered separately through 1inch Business at `business.1inch.com/portal/documentation`. https://business.1inch.com/portal/assets/docs-v2/aqua/reference/debug.md The debug instructions are Foundry-only helpers that print swap state to the console during local testing. They are not part of the production SwapVM opcode set and are not intended for production programs. Each one depends on `forge-std/console.sol` and produces no output outside a Foundry test environment. :::info Debug instructions are Foundry-only. Do not include them in production strategy programs or on deployed routers. ::: **Source:** `src/instructions/Debug.sol` --- ## Injecting debug opcodes ```solidity function _injectDebugOpcodes( function(Context memory, bytes calldata) internal[] memory opcodes ) internal pure returns (function(Context memory, bytes calldata) internal[] memory) ``` Registers all five debug instructions into opcode slots 0 to 4 of the provided `opcodes` array. Call it during router initialization to enable the debug instructions in tests. --- ## Instructions All debug instructions take no args (0 bytes) and are pure or view only. ### `_printSwapRegisters` ```solidity function _printSwapRegisters(Context memory ctx, bytes calldata /* args */) internal pure ``` Logs `ctx.swap`: `balanceIn`, `balanceOut`, `amountIn`, `amountOut`. --- ### `_printSwapQuery` ```solidity function _printSwapQuery(Context memory ctx, bytes calldata /* args */) internal pure ``` Logs `ctx.query`: `orderHash`, `taker`, `maker`, `tokenIn`, `tokenOut`, `isExactIn`. --- ### `_printContext` ```solidity function _printContext(Context memory ctx, bytes calldata /* args */) internal pure ``` Logs `ctx.vm`: `nextPC` and `takerArgs` (as hex string). --- ### `_printGasLeft` ```solidity function _printGasLeft(Context memory /* ctx */, bytes calldata /* args */) internal view ``` Logs `gasleft()`. --- ### `_printFreeMemoryPointer` ```solidity function _printFreeMemoryPointer(Context memory /* ctx */, bytes calldata /* args */) internal pure ``` Logs the free memory pointer (`mload(0x40)`). --- ## Related - [SwapVM](../swapvm/swapvm-engine): overview of the instruction execution model. https://business.1inch.com/portal/assets/docs-v2/aqua/api/introduction.md The 1inch Aqua API provides HTTP access to the [Aqua Protocol](https://github.com/1inch/aqua) -- a shared liquidity layer that lets liquidity providers allocate virtual token balances across multiple trading strategies without fragmenting capital across isolated pools. Use this API to query protocol state, strategies, and related on-chain data from your applications. For encoding transactions and parsing contract events, see the [Aqua TypeScript SDK](https://github.com/1inch/sdks/tree/master/typescript/aqua) and the [Aqua app template](https://github.com/1inch/aqua-app-template). ## Key endpoints - `GET /v1.0/strategies/opened` -- the **discovery endpoint**: every currently open strategy, newest first, with the strategy bytes and per-token maker balance/allowance data. Filter with `chainIds` (and app filters); results are cursor-paginated (`nextCursor`), up to 500 items per page. Balance/allowance values are indexed with a delay -- always re-quote on-chain before execution. - `GET /v1.0/general/status` -- service availability check. The full endpoint list lives in the [OpenAPI section](./methods). ## Supported chains The API covers all 13 Aqua chains. The canonical list with deployment details lives in [Supported chains](/portal/documentation/aqua/liquidity-layer/supported-chains). | Supported chains | Chain ID | | :--------------- | :------- | | Ethereum | 1 | | Optimism | 10 | | BNB Chain | 56 | | Gnosis | 100 | | Unichain | 130 | | Polygon | 137 | | Sonic | 146 | | zkSync Era | 324 | | Robinhood Chain | 4663 | | Base | 8453 | | Arbitrum | 42161 | | Avalanche | 43114 | | Linea | 59144 | ## Core concepts | Term | Meaning | | --------------- | --------------------------------------------------------------------------------------------------------------------- | | **Maker** | Liquidity provider who ships virtual balances into Aqua strategies. | | **App** | Smart contract that implements a trading application on top of Aqua. | | **Strategy** | Immutable configuration (identified by `keccak256(abi.encode(strategy))`) that defines how virtual balances are used. | | **Ship / Dock** | Lifecycle operations that allocate or remove virtual balances for a strategy. | | **Pull / Push** | Swap interface operations that move tokens during trade execution. | ## Authentication All requests require a valid 1inch API key. See [Authentication](../../apis/authentication) for header and OAuth details. Aqua is enabled per plan. If your key's plan does not include Aqua, requests to `/aqua` return `404`. ## API reference For detailed information about each endpoint, refer to the Aqua API [OpenAPI section](./methods). https://business.1inch.com/portal/assets/docs-v2/aqua/api/quick-start.md Base URL: `https://api.1inch.com/aqua` The gateway rewrites `/aqua/...` to the Aqua service. OpenAPI spec: `/aqua/swagger-json`. :::info All requests require a 1inch API key (`Authorization: Bearer ...`) -- the same key used for the other 1inch APIs; see [Authentication](../../apis/authentication). Aqua is enabled per plan -- if your key's plan does not include Aqua, requests to `/aqua` return `404`. ::: ## Check service availability ```bash curl 'https://api.1inch.com/aqua/v1.0/general/status' \ -H 'accept: application/json' \ -H 'Authorization: Bearer YOUR_API_KEY' ``` ## Discover open strategies `GET /v1.0/strategies/opened` returns every currently open strategy (newest first) with its strategy bytes and per-token maker balance/allowance data -- cursor-paginated via `nextCursor`, up to 500 items per page. Balance data is indexed with a delay: re-quote on-chain before execution. ```bash curl 'https://api.1inch.com/aqua/v1.0/strategies/opened?chainIds=1&limit=100' \ -H 'accept: application/json' \ -H 'Authorization: Bearer YOUR_API_KEY' ``` ## Example request Replace the path below with the endpoint you need from the [OpenAPI section](./methods): ```bash curl 'https://api.1inch.com/aqua/v1.0/...' \ -H 'accept: application/json' \ -H 'Authorization: Bearer YOUR_API_KEY' ``` ## TypeScript SDK For on-chain interactions (building `ship`, `dock`, `pull`, and `push` calldata), use the Aqua SDK: ```bash pnpm add @1inch/aqua-sdk ``` ```typescript import { AquaProtocolContract, AQUA_CONTRACT_ADDRESSES, Address, HexString, NetworkEnum } from "@1inch/aqua-sdk"; const aqua = new AquaProtocolContract(AQUA_CONTRACT_ADDRESSES[NetworkEnum.ETHEREUM]); const shipTx = aqua.ship({ app: new Address("0x..."), strategy: new HexString("0x..."), amountsAndTokens: [{ token: new Address("0x..."), amount: 1000000000000000000n }] }); ``` See the [SDK repository](https://github.com/1inch/sdks/tree/master/typescript/aqua) for full examples. https://business.1inch.com/portal/assets/docs-v2/aqua/help/aqua-faq.md Answers for makers providing liquidity on **1inch Aqua**, the shared liquidity layer. Questions are phrased as real search queries; this is the canonical Aqua FAQ (also on the 1inch Help Center) and carries `FAQPage` JSON-LD for AI-answer citation. For engine / build questions, see the [SwapVM FAQ](/portal/documentation/aqua/help/swapvm-faq). ## Basics & category ### What is 1inch Aqua? 1inch Aqua is the shared liquidity layer for self-custodial liquidity provision. One wallet balance can back many positions at the same time, and your tokens stay in your wallet until a swap fills. It is fully self-custodial: you never deposit, and you can revoke access at any time. Aqua runs on the [SwapVM](/portal/documentation/aqua/swapvm/swapvm-engine) engine across [13 EVM chains](/portal/documentation/aqua/liquidity-layer/supported-chains). ### Is a position the same as a strategy? Yes, they are the same thing. In the 1inch dApp you open a **position**. On-chain, in the Aqua smart contracts, the SDK and the API, that exact same object is called a **strategy**, because those code identifiers are fixed and cannot be renamed (for example `strategyHash` and the `/strategies` endpoint). So a position _is_ a strategy: if you read the developer docs, a block explorer or an API response and see "strategy," it means your position. ### What is shared liquidity? Shared liquidity means the same tokens in your wallet can back many positions at the same time, instead of being split across separate pools. You grant one allowance so positions can quote against your wallet, and tokens move only when a swap fills. The result is more positions across more pairs from the same tokens, with full self-custody. ### How is shared liquidity different from a normal liquidity pool? In a normal liquidity pool you deposit tokens into one pool, and those tokens only work in that pool. With 1inch Aqua, your tokens stay in your wallet and the same balance can back many positions at the same time. You provide liquidity across more pairs without splitting your tokens or depositing them. ### Is 1inch Aqua an AMM? 1inch Aqua is a shared liquidity layer, not a single-pool model. You provide liquidity by opening positions that quote against the tokens in your wallet. Many positions can share one wallet balance, and tokens move only when a swap fills, so you keep full self-custody throughout. ### Is shared liquidity the same as leverage? No. Shared liquidity is not leverage. Your tokens are not borrowed and you take on no debt. The same tokens are simply made available to many positions, and a swap can only pull what is actually in your wallet. The Shared Liquidity Ratio measures availability, not exposure. ### What is the Shared Liquidity Ratio? The Shared Liquidity Ratio shows how much liquidity the same tokens have available across many positions. It measures availability, not exposure, and it is not leverage: your tokens are not borrowed, and a swap can only pull what is in your wallet. ### What is SwapVM? SwapVM is the engine that powers 1inch Aqua. Each position is an immutable, hash-addressed config that SwapVM executes, so you can verify exactly what you commit to before you open a position, and close it in one transaction. Developers can read the engine details in the [SwapVM FAQ](/portal/documentation/aqua/help/swapvm-faq). ## Custody & mechanics ### Do my tokens ever leave my wallet? No. Aqua is self-custodial and never holds your tokens; they stay in your wallet until a swap fills. Nothing is deposited. You grant a revocable allowance so positions can quote against your wallet, and you can revoke it at any time. ### Do I need to deposit or lock tokens to provide liquidity? No. You never deposit. You open a position and grant an allowance so positions can quote against your wallet; your tokens stay in your wallet and move only when a swap fills. ### What is the allowance, and how do I revoke it? The allowance is a permission you grant, with an upper limit. It is not the amount that moves, and not a guarantee that tokens are there. A swap can only move tokens that are actually in your wallet: when it executes, tokens transfer straight from your wallet, and if your balance is too low at that moment the swap simply reverts. Your real wallet balance is the binding limit; the allowance only sets the cap for what can be moved. You can revoke the allowance at any time to stop new swaps from filling. ### If one of my positions fills, what happens to the others? Your positions share one wallet balance, so a swap fills against your available tokens. After a fill, your other positions quote against the tokens that remain in your wallet. Tokens move only when a swap fills. ### What happens if a position is underfunded? Your tokens stay in your wallet, so if your wallet runs low the position simply stops filling. There is no separate "paused" status, and nothing is liquidated. Add tokens back to your wallet and it can fill again. To stop a position entirely, you close it, which clears it without moving any tokens. ## Earning & risk ### How do I earn on 1inch Aqua? You collect swap fees when swaps fill against your positions. Swap fees go to your wallet, ready to use as liquidity. ### Are swap fees guaranteed? No. Swaps are not guaranteed, so swap fees are not guaranteed. Providing liquidity takes knowledge and experience. ### What are the risks of providing liquidity on Aqua? Providing liquidity takes knowledge and experience. Swap fees are not guaranteed, prices can move against your position (impermanent loss), and you bear market risk and smart-contract risk. You can lose tokens you commit. You stay in self-custody and can cancel a position or revoke the token allowance at any time. See the developer [Risks & disclosures](/portal/documentation/aqua/liquidity-layer/risks-and-disclosures) for the smart-contract detail. ### Does it cost gas to open or close a position? Yes. Opening and closing positions cost gas, like any onchain action. Because your tokens stay in your wallet and move only when a swap fills, you do not pay to redeposit between positions. ## Access & usage ### Which chains does 1inch Aqua support? 1inch Aqua is available across [13 chains](/portal/documentation/aqua/liquidity-layer/supported-chains): Ethereum, Arbitrum, Robinhood Chain, Base, Optimism, Polygon, BNB Chain, Avalanche, Gnosis, zkSync Era, Linea, Unichain, and Sonic. ### Can I provide liquidity from a smart-contract or MPC wallet? Yes. You can provide liquidity on 1inch Aqua from a smart-contract wallet and from an MPC wallet. ### Can I run 1inch Aqua from a script or SDK? Yes. Use the Aqua SDK to open and manage positions programmatically. Aqua SDK and SwapVM SDK are live in the [1inch/sdks](https://github.com/1inch/sdks) repository on GitHub. See the developer [SDK overview](/portal/documentation/aqua/reference/sdk-overview). ### Is there a limit on how much I can provide? There is no fixed cap you sign up for. What a swap can pull is bounded by the tokens in your wallet and the allowance you grant, so you stay in control of your exposure at all times. ### Who can swap against my liquidity, and can I control it? In the dApp, swaps that fill your liquidity are routed by 1inch Resolvers, the KYC-verified network that routes swaps through 1inch. Calling the contracts directly gives you more control. To restrict it, you can add a Conditional Access rule to your position that requires takers to hold a specific NFT. The check runs on-chain first, so unauthorized takers are rejected before anything else runs. This covers cases like institutional gating, NFT-gated liquidity, loyalty-token holders, or DAO-member access. Developers: see [Access, resolvers & Pathfinder](/portal/documentation/aqua/liquidity-layer/access-resolvers-and-pathfinder). ### How do I open and close a position? Connect your wallet to open a position, which registers an immutable, hash-addressed config. You close it in one transaction, which clears the position without moving any tokens, and you can revoke the allowance at any time. ### Is 1inch Aqua audited? Yes. 1inch Aqua and the SwapVM engine have been audited by OpenZeppelin, Bailsec, Hexens, Nethermind, Theori, Decurity, Hashlock and MixBytes. Audits reduce risk, they do not remove it: Aqua is audited, but new. ### What is my maximum risk when I provide liquidity on 1inch Aqua? Your maximum risk is not the approval you grant, it is the most a swap can pull from your wallet. In the 1inch dApp the token approval defaults to an unlimited amount (max, type(uint256).max), but that is only a ceiling. Aqua never holds your tokens, and a swap can move only what is in your wallet when it fills. Your real exposure is the Usable amount in the app: the smaller of your wallet balance and what you have committed across positions. You can revoke the allowance at any time, per token and per chain. ## FAQPage JSON-LD Machine-readable twin of the questions above; ship it with the page for AI-answer citation. ```json { "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [ { "@type": "Question", "name": "What is 1inch Aqua?", "acceptedAnswer": { "@type": "Answer", "text": "1inch Aqua is the shared liquidity layer where you provide liquidity on Aqua. One wallet balance can back many positions at the same time, and your tokens stay in your wallet until a swap fills. It is fully self-custodial: you never deposit, and you can revoke access at any time. Aqua runs on the SwapVM engine across 13 chains." } }, { "@type": "Question", "name": "Is a position the same as a strategy?", "acceptedAnswer": { "@type": "Answer", "text": "Yes, they are the same thing. In the 1inch dApp you open a position. On-chain, in the Aqua smart contracts, the SDK and the API, that exact same object is called a strategy, because those code identifiers are fixed and cannot be renamed (for example strategyHash and the /strategies endpoint). So a position is a strategy: if you read the developer docs, a block explorer or an API response and see strategy, it means your position." } }, { "@type": "Question", "name": "What is shared liquidity?", "acceptedAnswer": { "@type": "Answer", "text": "Shared liquidity means the same tokens in your wallet can back many positions at the same time, instead of being split across separate pools. You grant one allowance so positions can quote against your wallet, and tokens move only when a swap fills. The result is more positions across more pairs from the same tokens, with full self-custody." } }, { "@type": "Question", "name": "How is shared liquidity different from a normal liquidity pool?", "acceptedAnswer": { "@type": "Answer", "text": "In a normal liquidity pool you deposit tokens into one pool, and those tokens only work in that pool. With 1inch Aqua, your tokens stay in your wallet and the same balance can back many positions at the same time. You provide liquidity across more pairs without splitting your tokens or depositing them." } }, { "@type": "Question", "name": "Is 1inch Aqua an AMM?", "acceptedAnswer": { "@type": "Answer", "text": "1inch Aqua is a shared liquidity layer, not a single-pool model. You provide liquidity by opening positions that quote against the tokens in your wallet. Many positions can share one wallet balance, and tokens move only when a swap fills, so you keep full self-custody throughout." } }, { "@type": "Question", "name": "Is shared liquidity the same as leverage?", "acceptedAnswer": { "@type": "Answer", "text": "No. Shared liquidity is not leverage. Your tokens are not borrowed and you take on no debt. The same tokens are simply made available to many positions, and a swap can only pull what is actually in your wallet. The Shared Liquidity Ratio measures availability, not exposure." } }, { "@type": "Question", "name": "What is the Shared Liquidity Ratio?", "acceptedAnswer": { "@type": "Answer", "text": "The Shared Liquidity Ratio shows how much liquidity the same tokens have available across many positions. It measures availability, not exposure, and it is not leverage: your tokens are not borrowed, and a swap can only pull what is in your wallet." } }, { "@type": "Question", "name": "What is SwapVM?", "acceptedAnswer": { "@type": "Answer", "text": "SwapVM is the engine that powers 1inch Aqua. Each position is an immutable, hash-addressed config that SwapVM executes, so you can verify exactly what you commit to before you open a position, and close it in one transaction. Developers can read the engine details in the SwapVM FAQ." } }, { "@type": "Question", "name": "Do my tokens ever leave my wallet?", "acceptedAnswer": { "@type": "Answer", "text": "No. Aqua is self-custodial and never holds your tokens; they stay in your wallet until a swap fills. Nothing is deposited. You grant a revocable allowance so positions can quote against your wallet, and you can revoke it at any time." } }, { "@type": "Question", "name": "Do I need to deposit or lock tokens to provide liquidity?", "acceptedAnswer": { "@type": "Answer", "text": "No. You never deposit. You open a position and grant an allowance so positions can quote against your wallet; your tokens stay in your wallet and move only when a swap fills." } }, { "@type": "Question", "name": "What is the allowance, and how do I revoke it?", "acceptedAnswer": { "@type": "Answer", "text": "The allowance is a permission you grant, with an upper limit. It is not the amount that moves, and not a guarantee that funds are there. A swap can only move tokens that are actually in your wallet: when it executes, tokens transfer straight from your wallet, and if your balance is too low at that moment the swap simply reverts. Your real wallet balance is the binding limit; the allowance only sets the ceiling of what can be moved. You can revoke the allowance at any time to stop new swaps from filling." } }, { "@type": "Question", "name": "If one of my positions fills, what happens to the others?", "acceptedAnswer": { "@type": "Answer", "text": "Your positions share one wallet balance, so a swap fills against your available tokens. After a fill, your other positions quote against the tokens that remain in your wallet. Tokens move only when a swap fills." } }, { "@type": "Question", "name": "What happens if a position is underfunded?", "acceptedAnswer": { "@type": "Answer", "text": "Your tokens stay in your wallet, so if your wallet runs low the position simply stops filling. There is no separate \"paused\" status, and nothing is liquidated. Add tokens back to your wallet and it can fill again. To stop a position entirely, you close it, which clears it without moving any tokens." } }, { "@type": "Question", "name": "How do I earn on 1inch Aqua?", "acceptedAnswer": { "@type": "Answer", "text": "You collect swap fees when swaps fill against your positions. Swap fees go to your wallet, ready to use as liquidity." } }, { "@type": "Question", "name": "Are swap fees guaranteed?", "acceptedAnswer": { "@type": "Answer", "text": "No. Swaps are not guaranteed, so swap fees are not guaranteed. Providing liquidity takes knowledge and experience." } }, { "@type": "Question", "name": "What are the risks of providing liquidity on Aqua?", "acceptedAnswer": { "@type": "Answer", "text": "Providing liquidity takes knowledge and experience. Swap fees are not guaranteed, prices can move against your position (impermanent loss), and you bear market risk and smart-contract risk. You can lose tokens you commit. You stay in self-custody and can cancel a position or revoke the token allowance at any time. See the developer Risks & disclosures for the smart-contract detail." } }, { "@type": "Question", "name": "What is my maximum risk when I provide liquidity on 1inch Aqua?", "acceptedAnswer": { "@type": "Answer", "text": "Your maximum risk is not the approval you grant, it is the most a swap can pull from your wallet. In the 1inch dApp the token approval defaults to an unlimited amount (max, type(uint256).max), but that is only a ceiling. Aqua never holds your tokens, and a swap can move only what is in your wallet when it fills. Your real exposure is the Usable amount in the app: the smaller of your wallet balance and what you have committed across positions. You can revoke the allowance at any time, per token and per chain." } }, { "@type": "Question", "name": "Does it cost gas to open or close a position?", "acceptedAnswer": { "@type": "Answer", "text": "Yes. Opening and closing positions cost gas, like any onchain action. Because your tokens stay in your wallet and move only when a swap fills, you do not pay to redeposit between positions." } }, { "@type": "Question", "name": "Which chains does 1inch Aqua support?", "acceptedAnswer": { "@type": "Answer", "text": "1inch Aqua is available across 13 chains: Ethereum, Arbitrum, Robinhood Chain, Base, Optimism, Polygon, BNB Chain, Avalanche, Gnosis, zkSync Era, Linea, Unichain, and Sonic." } }, { "@type": "Question", "name": "Can I provide liquidity from a smart-contract or MPC wallet?", "acceptedAnswer": { "@type": "Answer", "text": "Yes. You can provide liquidity on 1inch Aqua from a smart-contract wallet and from an MPC wallet." } }, { "@type": "Question", "name": "Can I run 1inch Aqua from a script or SDK?", "acceptedAnswer": { "@type": "Answer", "text": "Yes. Use the Aqua SDK to open and manage positions programmatically. Aqua SDK and SwapVM SDK are live in the 1inch/sdks repository on GitHub. See the developer SDK overview." } }, { "@type": "Question", "name": "Is there a limit on how much I can provide?", "acceptedAnswer": { "@type": "Answer", "text": "There is no fixed cap you sign up for. What a swap can pull is bounded by the tokens in your wallet and the allowance you grant, so you stay in control of your exposure at all times." } }, { "@type": "Question", "name": "Who can swap against my liquidity, and can I control it?", "acceptedAnswer": { "@type": "Answer", "text": "In the dApp, swaps that fill your liquidity are routed by 1inch Resolvers, the KYC-verified network that routes swaps through 1inch. Calling the contracts directly gives you more control. To restrict it, you can add a Conditional Access rule to your position that requires takers to hold a specific NFT. The check runs on-chain first, so unauthorized takers are rejected before anything else runs. This covers cases like institutional gating, NFT-gated liquidity, loyalty-token holders, or DAO-member access. Developers: see Access, resolvers & Pathfinder." } }, { "@type": "Question", "name": "How do I open and close a position?", "acceptedAnswer": { "@type": "Answer", "text": "Connect your wallet to open a position, which registers an immutable, hash-addressed config. You close it in one transaction, which clears the position without moving any tokens, and you can revoke the allowance at any time." } }, { "@type": "Question", "name": "Is 1inch Aqua audited?", "acceptedAnswer": { "@type": "Answer", "text": "Yes. 1inch Aqua and the SwapVM engine have been audited by OpenZeppelin, Bailsec, Hexens, Nethermind, Theori, Decurity, Hashlock and MixBytes. Audits reduce risk, they do not remove it: Aqua is audited, but new." } } ] } ``` https://business.1inch.com/portal/assets/docs-v2/aqua/help/swapvm-faq.md Developer answers about **SwapVM**, the programmable execution engine behind 1inch Aqua. Questions are phrased as real search queries and answered for engineers who build, quote, test and index strategies. This set feeds `llms.txt` and carries `FAQPage` JSON-LD for AI-answer citation. For maker / provide-liquidity questions, see the [Aqua FAQ](/portal/documentation/aqua/help/aqua-faq). ### What is SwapVM? **SwapVM** is the programmable execution engine behind 1inch Aqua. It runs a strategy's instruction program (a stream of opcodes) inside the on-chain `AquaSwapVMRouter` during `quote()` and `swap()`. The same engine also powers the 1inch Limit Order and Fusion routers. A strategy is a byte-addressable program identified by its `strategyHash`. ### What is the difference between 1inch Aqua and SwapVM? They are two separate products that work together. **1inch Aqua** is the _shared liquidity layer_: the registry, custody and accounting where a maker keeps inventory in their own wallet, ships and docks strategies, and controls who can take (virtual balances, the Shared Liquidity Ratio, access & resolvers). **SwapVM** is the _execution engine_: the VM that prices and executes a strategy's opcode program. A strategy is a SwapVM program registered in the Aqua registry -- Aqua supplies the liquidity and access, SwapVM supplies the pricing logic. See [Access & resolvers](/portal/documentation/aqua/liquidity-layer/access-resolvers-and-pathfinder) (Aqua) and the [SwapVM engine](/portal/documentation/aqua/swapvm/swapvm-engine). ### What is a SwapVM strategy (program)? A strategy is a compact byte program -- a sequence of `[opcode][argsLength][args]` instructions the VM executes. It is wrapped in an `Order` and registered on-chain via Aqua `ship()`. The `strategyHash` is `keccak256` of the ABI-encoded Order, and it equals the router's `orderHash` emitted in `Swapped` -- so fills join to strategies on that key. See [Program, order and strategy](/portal/documentation/aqua/swapvm/program-order-and-strategy). ### What opcodes does SwapVM support? On the deployed Aqua router the executable subset is Controls, XYCSwap, XYCConcentrate, Decay, Fee, PeggedSwap and Extruction, verified against `AquaOpcodes`. The SDK can encode the full instruction set, but the `AquaProgramBuilder` rejects an unregistered opcode at **build time** (`OpcodeNotFound`). At runtime there is no named error: a reserved gap index is a no-op (`_notInstruction`) and an index past the registered table reverts with a Solidity array-out-of-bounds `Panic(0x32)`. The full directory (index, function, byte schema) is the [Opcode gallery](/portal/documentation/aqua/swapvm/opcode-gallery); to add your own, see [Write your own opcode](/portal/documentation/aqua/swapvm/write-your-own-opcode). ### How do I build a SwapVM strategy? Use `@1inch/swap-vm-sdk` -- e.g. `AquaXYCAmmStrategy.new()...build()` or `AquaProgramBuilder` -- or write the opcode bytes directly. Then wrap it in an `Order` and `ship()` it. The fastest path is the copy-paste [Strategy template](/portal/documentation/aqua/getting-started/strategy-template) (Foundry + TypeScript); for a custom pricing contract see [Build an AquaApp](/portal/documentation/aqua/getting-started/build-an-aquaapp). ### How do I quote and swap against a SwapVM strategy? Call the deployed `AquaSwapVMRouter` v1.0.2 (`0x111111338c5091e8440b67b168bae16a668ac0de`). Both `quote` and `swap` take `(Order order, address tokenIn, address tokenOut, uint256 amount, bytes takerTraitsAndData)` and return `(amountIn, amountOut, orderHash)`. The explicit `tokenIn`/`tokenOut` pair sets the swap direction. `quote()` is a free `staticcall` preview; `swap()` executes. Full surface: [Events & interfaces](/portal/documentation/aqua/reference/events-and-interfaces). ### What version of SwapVM is deployed? `AquaSwapVMRouter` **v1.0.2**. Its `quote`/`swap` take explicit `tokenIn`/`tokenOut` (five arguments; selectors `0x44aa5f14` and `0xf4d2d412`), and an order's `data` holds only hooks and the program (no token prefix). The `1inch/swap-vm` `main` branch has an unreleased refactor with a different signature -- build against v1.0.2, which matches the on-chain router and the published SDK. ### How do I test a SwapVM strategy? Fork-test with Foundry. The contracts compile with **solc 0.8.30**, the **cancun** EVM target, the optimizer and **viaIR**. The [Strategy template](/portal/documentation/aqua/getting-started/strategy-template) ships a runnable `forge test --fork-url` that ships, quotes, swaps and docks, asserting every protocol event by its `topic0` hash. The [Debug](/portal/documentation/aqua/reference/debug) opcode helps trace execution during development. ### How do I control who can swap against my strategy? Prepend a Controls guard to the program. For example `onlyTxOriginTokenBalanceNonZero` pointed at a credential token (the Fusion `KycNFT`) gates the strategy to holders only; the check runs on-chain _before_ pricing, so unauthorized takers revert first. This is the Conditional Access pattern. See [Access, resolvers & Pathfinder](/portal/documentation/aqua/liquidity-layer/access-resolvers-and-pathfinder) and the [Conditional access](/portal/documentation/aqua/swapvm/patterns/conditional-access) pattern. ### Is it called a strategy or a position? On-chain and in the API it is a **strategy** (`strategyHash`, `/strategies`, the Strategy DTO, `ship`/`dock`). "Position" is the user-facing label in the consumer dApp only. Developer docs and code use **strategy** throughout. ### Is SwapVM open source and where is the code? Yes. The contracts are in [1inch/swap-vm](https://github.com/1inch/swap-vm) and the SDKs (`@1inch/aqua-sdk`, `@1inch/swap-vm-sdk`) in [1inch/sdks](https://github.com/1inch/sdks). SwapVM and Aqua have been audited by OpenZeppelin, Bailsec, Hexens, Nethermind, Theori, Decurity, Hashlock and MixBytes. Audits reduce risk, they do not remove it: the code is audited, but new. ### Can I run SwapVM from a script or SDK? Yes. Use `@1inch/swap-vm-sdk` for building and encoding strategies and `@1inch/aqua-sdk` for the registry (ship/dock), and broadcast through your own RPC endpoint. See the [SDK overview](/portal/documentation/aqua/reference/sdk-overview). ### Which chains is SwapVM deployed on? 13 EVM chains: Ethereum, Arbitrum, Base, Optimism, Polygon, BNB Chain, Avalanche, Gnosis, zkSync Era, Linea, Unichain, Sonic and Robinhood. Addresses are the same across chains for the registry and router; verify per chain and do not hardcode. See [Supported chains](/portal/documentation/aqua/liquidity-layer/supported-chains). Robinhood 4663 joined the Aqua chain set with the 2026-07-16 redeploy. ### Does SwapVM cost gas? `quote()` is a free static call (`eth_call`). `swap()`, `ship()` and `dock()` are transactions and cost gas like any on-chain action. Because a maker's tokens stay in their wallet and move only when a swap fills, there is no redeposit cost between strategies. ### Does providing liquidity through SwapVM pay yield? There is no guaranteed yield, APY or return. A maker earns **swap fees** when swaps fill against their strategy; those fees are not guaranteed and do not offset impermanent loss in every market. See [Risks & disclosures](/portal/documentation/aqua/liquidity-layer/risks-and-disclosures) and the worked [fee and net-P&L examples](/portal/documentation/aqua/getting-started/worked-examples). ### Are there hosted SwapVM APIs, MCP or agent endpoints? These developer docs cover the self-service, on-chain protocol and SDKs. Hosted API endpoints, MCP and x402 are KYB/KYC-gated, payable products under **1inch Business** -- see the [Aqua API](/portal/documentation/aqua/api/introduction) for where to go. ## FAQPage JSON-LD Machine-readable twin of the questions above; ship it with the page for AI-answer citation. ```json { "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [ { "@type": "Question", "name": "What is SwapVM?", "acceptedAnswer": { "@type": "Answer", "text": "SwapVM is the programmable execution engine behind 1inch Aqua. It runs a strategy's instruction program (a stream of opcodes) inside the on-chain AquaSwapVMRouter during quote() and swap(). The same engine also powers the 1inch Limit Order and Fusion routers. A strategy is a byte-addressable program identified by its strategyHash." } }, { "@type": "Question", "name": "What is the difference between 1inch Aqua and SwapVM?", "acceptedAnswer": { "@type": "Answer", "text": "They are two separate products that work together. 1inch Aqua is the shared liquidity layer: the registry, custody and accounting where a maker keeps inventory in their own wallet, ships and docks strategies, and controls who can take (virtual balances, the Shared Liquidity Ratio, access & resolvers). SwapVM is the execution engine: the VM that prices and executes a strategy's opcode program. A strategy is a SwapVM program registered in the Aqua registry -- Aqua supplies the liquidity and access, SwapVM supplies the pricing logic. See Access & resolvers (Aqua) and the SwapVM engine." } }, { "@type": "Question", "name": "What is a SwapVM strategy (program)?", "acceptedAnswer": { "@type": "Answer", "text": "A strategy is a compact byte program -- a sequence of [opcode][argsLength][args] instructions the VM executes. It is wrapped in an Order and registered on-chain via Aqua ship(). The strategyHash is keccak256 of the ABI-encoded Order, and it equals the router's orderHash emitted in Swapped -- so fills join to strategies on that key. See Program, order and strategy." } }, { "@type": "Question", "name": "What opcodes does SwapVM support?", "acceptedAnswer": { "@type": "Answer", "text": "On the deployed Aqua router the executable subset is Controls, XYCSwap, XYCConcentrate, Decay, Fee, PeggedSwap and Extruction, verified against AquaOpcodes. The SDK can encode the full instruction set, but the AquaProgramBuilder rejects an unregistered opcode at build time (OpcodeNotFound). At runtime there is no named error: a reserved gap index is a no-op (_notInstruction) and an index past the registered table reverts with a Solidity array-out-of-bounds Panic(0x32). The full directory (index, function, byte schema) is the Opcode gallery; to add your own, see Write your own opcode." } }, { "@type": "Question", "name": "How do I build a SwapVM strategy?", "acceptedAnswer": { "@type": "Answer", "text": "Use @1inch/swap-vm-sdk -- e.g. AquaXYCAmmStrategy.new()...build() or AquaProgramBuilder -- or write the opcode bytes directly. Then wrap it in an Order and ship() it. The fastest path is the copy-paste Strategy template (Foundry + TypeScript); for a custom pricing contract see Build an AquaApp." } }, { "@type": "Question", "name": "How do I quote and swap against a SwapVM strategy?", "acceptedAnswer": { "@type": "Answer", "text": "Call the deployed AquaSwapVMRouter v1.0.2 (0x111111338c5091e8440b67b168bae16a668ac0de). Both quote and swap take (Order order, address tokenIn, address tokenOut, uint256 amount, bytes takerTraitsAndData) and return (amountIn, amountOut, orderHash). The explicit tokenIn/tokenOut pair sets the swap direction. quote() is a free staticcall preview; swap() executes. Full surface: Events & interfaces." } }, { "@type": "Question", "name": "What version of SwapVM is deployed?", "acceptedAnswer": { "@type": "Answer", "text": "AquaSwapVMRouter v1.0.2. Its quote/swap take explicit tokenIn/tokenOut (five arguments; selectors 0x44aa5f14 and 0xf4d2d412), and an order's data holds only hooks and the program (no token prefix). The 1inch/swap-vm main branch has an unreleased refactor with a different signature -- build against v1.0.2, which matches the on-chain router and the published SDK." } }, { "@type": "Question", "name": "How do I test a SwapVM strategy?", "acceptedAnswer": { "@type": "Answer", "text": "Fork-test with Foundry. The contracts compile with solc 0.8.30, the cancun EVM target, the optimizer and viaIR. The Strategy template ships a runnable forge test --fork-url that ships, quotes, swaps and docks, asserting every protocol event by its topic0 hash. The Debug opcode helps trace execution during development." } }, { "@type": "Question", "name": "How do I control who can swap against my strategy?", "acceptedAnswer": { "@type": "Answer", "text": "Prepend a Controls guard to the program. For example onlyTxOriginTokenBalanceNonZero pointed at a credential token (the Fusion KycNFT) gates the strategy to holders only; the check runs on-chain before pricing, so unauthorized takers revert first. This is the Conditional Access pattern. See Access, resolvers & Pathfinder and the Conditional access pattern." } }, { "@type": "Question", "name": "Is it called a strategy or a position?", "acceptedAnswer": { "@type": "Answer", "text": "On-chain and in the API it is a strategy (strategyHash, /strategies, the Strategy DTO, ship/dock). \"Position\" is the user-facing label in the consumer dApp only. Developer docs and code use strategy throughout." } }, { "@type": "Question", "name": "Is SwapVM open source and where is the code?", "acceptedAnswer": { "@type": "Answer", "text": "Yes. The contracts are in 1inch/swap-vm and the SDKs (@1inch/aqua-sdk, @1inch/swap-vm-sdk) in 1inch/sdks. SwapVM and Aqua have been audited by OpenZeppelin, Bailsec, Hexens, Nethermind, Theori, Decurity, Hashlock and MixBytes. Audits reduce risk, they do not remove it: the code is audited, but new." } }, { "@type": "Question", "name": "Can I run SwapVM from a script or SDK?", "acceptedAnswer": { "@type": "Answer", "text": "Yes. Use @1inch/swap-vm-sdk for building and encoding strategies and @1inch/aqua-sdk for the registry (ship/dock), and broadcast through your own RPC endpoint. See the SDK overview." } }, { "@type": "Question", "name": "Which chains is SwapVM deployed on?", "acceptedAnswer": { "@type": "Answer", "text": "13 EVM chains: Ethereum, Arbitrum, Base, Optimism, Polygon, BNB Chain, Avalanche, Gnosis, zkSync Era, Linea, Unichain, Sonic and Robinhood. Addresses are the same across chains for the registry and router; verify per chain and do not hardcode. See Supported chains. Robinhood 4663 joined the Aqua chain set with the 2026-07-16 redeploy." } }, { "@type": "Question", "name": "Does SwapVM cost gas?", "acceptedAnswer": { "@type": "Answer", "text": "quote() is a free static call (eth_call). swap(), ship() and dock() are transactions and cost gas like any on-chain action. Because a maker's tokens stay in their wallet and move only when a swap fills, there is no redeposit cost between strategies." } }, { "@type": "Question", "name": "Does providing liquidity through SwapVM pay yield?", "acceptedAnswer": { "@type": "Answer", "text": "There is no guaranteed yield, APY or return. A maker earns swap fees when swaps fill against their strategy; those fees are not guaranteed and do not offset impermanent loss in every market. See Risks & disclosures and the worked fee and net-P&L examples." } }, { "@type": "Question", "name": "Are there hosted SwapVM APIs, MCP or agent endpoints?", "acceptedAnswer": { "@type": "Answer", "text": "These developer docs cover the self-service, on-chain protocol and SDKs. Hosted API endpoints, MCP and x402 are KYB/KYC-gated, payable products under 1inch Business -- see APIs, MCP & agents for where to go." } } ] } ``` ## Resolvers https://business.1inch.com/portal/assets/docs-v2/resolvers/introduction.md Welcome to the onboarding guide for becoming a Resolver in the 1inch ecosystem. This document outlines the self-service onboarding process and helpful resources for resolving **Intent-based Orders** (Fusion and Cross-chain/Fusion+ Orders) and **Limit Orders**. ## Existing users: Non-resolvers ### Step 1 (as an existing customer): Set business activity If you already have a 1inch Business Portal account but are not registered as a resolver yet, follow the steps below. Access your [Company profile](/portal/profile/company), select **Resolver** as an additional business segment, and then complete the resolver details in your [Resolver profile](/portal/profile/resolver) as described in **Step 3**. ## New users/registration: ### Step 1 (as a new customer): Set business activity Visit the [1inch Business Portal registration page](https://business.1inch.com/portal/registration) and select the preferred registration method. On the first onboarding screen, select your business segment(s) from the available options (multi-select): **Resolver** and other applicable segments. ### Step 2: Set your entity type and accept the terms Select your entity type and confirm that you've read, understood, and accepted the [Terms of Use of the Business Portal](portal/assets/legal-docs/terms_of_service_public_api_20260727.pdf) and the [1inch Network Terms of Use for Resolvers](/portal/assets/legal-docs/resolvers/terms-of_use_resolvers_20260814.pdf) ### Step 3: Provide Resolver Information 1. Provide the following resolver details: - **Resolver name** (required) - **Logo** (optional) -- URL to your logo in SVG format. The URL must point to a valid image file. - **Contract Address** for each protocol where resolving is intended | Protocol | Configuration | | :------------------- | :-------------------------- | | **Limit Order (LO)** | Chains + contract addresses | | **Intent** | Chains + contract addresses | | **Cross Chain** | Chains + contract addresses | :::info **1inch Aqua.** Taker access to [1inch Aqua](/portal/documentation/aqua/overview/overview) is not yet part of this self-service flow. Complete the resolver onboarding above first, then request the Aqua resolver NFT by emailing **csm@1inch.com** -- see [How to request the Aqua resolver NFT](/portal/documentation/aqua/liquidity-layer/access-resolvers-and-pathfinder#how-to-request-the-aqua-resolver-nft) for the request checklist. ::: Contract address validation: - Every contract address you add is automatically scanned across all chains before you proceed. - The UI indicates if a contract address is blacklisted (flagged as positive). ### Step 4: Perform KYC/KYB Complete the verification process based on your selection in Step 2. If you face any difficulties with KYB, please reach out to compliance@1inch.com. ### Step 5: Complete Compliance Survey Complete the compliance survey for resolvers. This step is mandatory and no whitelisting will be enabled until the verification and survey have been completed. ### Step 6: NFT Minting and Confirmation Once **both** of the following are complete: - KYC/KYB approval - Compliance survey completion Your Access NFT(s) will be minted, granting access to Order fulfillment. **Email notifications:** - A single confirmation email is sent after all NFTs are minted. - If any contract address becomes blacklisted during this step, you will receive a notification email (content to be provided). :::info **Adding contracts later.** Existing resolvers can add additional Limit Order, Fusion, and Fusion+ contract addresses and chains at any time in their [Resolver profile](/portal/profile/resolver) -- each new address goes through the same scan and minting flow automatically. Aqua is the exception: it is enabled only on request, after manual review (see the note above). ::: --- ## Protocol-Specific Onboarding From here, the next steps are determined by the protocol you choose to integrate with. Please follow the corresponding subsection. ## Intent-Based Orders ### Request Off-Chain Auction Access [optional for Fusion Orders] Resolvers may apply for off-chain auction access to Fusion Orders before the Dutch Auction phase begins. To request access: 1. Provide your API endpoint developed in accordance with the Exclusive Resolver API script example and related technical documentation. 2. Submit the request via the dedicated support communication channel created during onboarding. 3. Upon review and approval by the technical team, your Resolver will be integrated into the Exclusive Resolver API and granted off-chain auction access to Fusion Orders before the Dutch Auction phase begins. Participation in the Exclusive Resolver API requires ongoing compliance with the Service Level Agreement (SLA) as detailed in Annex A of the Resolver Terms of Use. [Exclusive Resolver API Documentation](./exclusive-resolver-terms) :::info Governance-Dependent Functionality The whitelisting, farming, and any Unicorn Power-based exclusivity or eligibility mechanisms are currently disabled for Fusion and Limit Orders as per 1IP-89 and shall apply only if and when enabled through a governance vote or a decision originating therefrom. Until such activation occurs, these features remain unavailable, and Intent Order Resolvers are not required to perform any related actions. ::: ### Follow Quickstart Guidelines #### Intent - Fusion SDK: https://github.com/1inch/cross-chain-swap/tree/ef6956a527528ff6c842dbdcbbd00a812436cd1f - Fusion Resolver Example: https://github.com/1inch/fusion-resolver-example - Fusion API Docs: https://github.com/1inch/fusion-resolver-example #### Cross-chain - Cross-chain SDK: https://github.com/1inch/fusion-sdk - Cross-chain Resolver Example: https://github.com/1inch/fusion-resolver-example - Cross-chain API Docs: https://github.com/1inch/cross-chain-swap/tree/ef6956a527528ff6c842dbdcbbd00a812436cd1f ## Limit Orders - Limit Order SDK: https://github.com/1inch/limit-order-sdk/tree/master - Limit Order Resolver Example: https://github.com/1inch/limit-order-sdk/blob/master/tests/extensions/fee-taker/fee-taker.spec.ts - Limit Order API Docs: https://business.1inch.com/portal/documentation/apis/orderbook/introduction **Fees** - 5 bps for stable/pegged pairs - 30 bps for normal/non-stable pairs https://business.1inch.com/portal/assets/docs-v2/resolvers/terms-of-use.md ## Terms of Use for Resolvers: The document defines eligibility, obligations, and compliance requirements for approved resolvers. Explore the [document](/portal/assets/legal-docs/resolvers/terms-of_use_resolvers_20260814.pdf) ## Exclusive Resolver API Terms: The document contains technical specifications and access details for eligble resolvers to the exlusive resolver endpoint. Explore the [document](/documentation/resolvers/exclusive-resolver-terms) https://business.1inch.com/portal/assets/docs-v2/resolvers/limit-order/quick-start-for-limit-order.md ## Kick off integration Once verified, it's time to integrate. The following resources will help you start fast: - [Limit Order Protocol Extension FeeTaker](https://github.com/1inch/limit-order-protocol/blob/master/contracts/extensions/FeeTaker.sol) - [Limit Order SDK](https://github.com/1inch/limit-order-sdk) - [Example for order submission (line 24)](https://github.com/1inch/limit-order-sdk/blob/master/src/sdk/sdk.ts#L24) - [`makingAmount` and `takingAmount` calculation (lines 78-108)](https://github.com/1inch/limit-order-sdk/blob/master/src/limit-order/limit-order-with-fee.ts#L78-L108) If you need further assistance, reach out to our support team: support@1inch.com. A dedicated support communication channel will be established. ## Active order discovery 1. Use the Orderbook API endpoint to fetch active orders: [GET /v4.1/1/all](/documentation/apis/orderbook/methods/v4.1/1/all/method/get). 2. Apply one of the statuses for filtering: - `status=1`: Order can be filled for the full `remainingMakerAmount` - `status=2`: Orders with insufficient approval or balance for the full remaining amount https://business.1inch.com/portal/assets/docs-v2/resolvers/limit-order/contracts-interaction.md ## Deployed contracts | Name | Contract | Address | | -------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------- | | Staking v2 | st1inch | [0x9A0C8Ff858d273f57072D714bca7411D717501D7](https://etherscan.io/address/0x9A0C8Ff858d273f57072D714bca7411D717501D7) | | Staking farm | StakingFarmingPod | [0x1A87c0F9CCA2f0926A155640e8958a8A6B0260bE](https://etherscan.io/address/0x1A87c0F9CCA2f0926A155640e8958a8A6B0260bE) | | Settlement | Settlement | [0xA88800CD213dA5Ae406ce248380802BD53b47647](https://etherscan.io/address/0xA88800CD213dA5Ae406ce248380802BD53b47647) | | Delegation pod | PowerPod | [0xAccfAc2339e16DC80c50d2fa81b5c2B049B4f947](https://etherscan.io/address/0xAccfAc2339e16DC80c50d2fa81b5c2B049B4f947) | | Resolver information | ResolverMetadata | [0xBF4543819ECede56220bcB1e8C1BBa9Ef290a58a](https://etherscan.io/address/0xBF4543819ECede56220bcB1e8C1BBa9Ef290a58a) | | Whitelist | WhitelistRegistry | [0xF55684BC536487394B423e70567413faB8e45E26](https://etherscan.io/address/0xF55684BC536487394B423e70567413faB8e45E26) | | Whitelist info | WhitelistHelper | [0xF779bdde38C39138Dcaf1514B8a6b8a6C165642D](https://etherscan.io/address/0xF779bdde38C39138Dcaf1514B8a6b8a6C165642D) | ## Stake 1INCH (relevant only for UP optional strategy) | Repository | [limit-order-settlement](https://github.com/1inch/limit-order-settlement/) | | ---------------- | -------------------------------------------------------------------------- | | Contract | st1inch.sol | | Contract address | 0x9A0C8Ff858d273f57072D714bca7411D717501D7 | | Methods | Staking | - deposit(uint256 amount, uint256 duration) - depositWithPermit(uint256 amount, uint256 duration, bytes calldata permit) - depositFor(address account, uint256 amount, uint256 duration) - depositForWithPermit(address account, uint256 amount, uint256 duration, bytes calldata permit) | | Description | Stakes 1inch to get staking power according to the lock time | ```javascript //Deposits 100 1inch with 1 day lock await st1inch.deposit(ether("100"), time.duration.days("1")); ``` ## Register delegation pod (relevant only for UP optional strategy) | Repository | [limit-order-settlement](https://github.com/1inch/limit-order-settlement/) | | ---------------- | -------------------------------------------------------------------------- | | Contract | PowerPod.sol | | Contract address | 0xAccfAc2339e16DC80c50d2fa81b5c2B049B4f947 | | Methods | addPod(address pod) | | Description | Register pod usage for the tx sender. Needed for: | - Resolvers to enable resolver's and delegated staking power usage for whitelisting - Stakers to enable unicorn power delegation | ```javascript // register `delegation` pod usage a `resolver` await st1inch.connect(resolver).addPod(delegation.address); ``` ## Register delegation share token (relevant only for UP optional strategy) | Repository | [limit-order-settlement](https://github.com/1inch/limit-order-settlement/) | | ---------------- | ----------------------------------------------------------------------------------------- | | Contract | PowerPod.sol | | Contract address | 0xAccfAc2339e16DC80c50d2fa81b5c2B049B4f947 | | Methods | register(string memory name, string memory symbol, uint256 maxUserFarms) | | Description | Creates a resolvers share token to count delegated staked power shares and accrue rewards | ```javascript // Register resolver's token with name 'resolver token share' and symbol 'RTS' await delegation.register("resolver token share", "RTS"); ``` ## Delegate resolver's staking power to self (relevant only for UP optional strategy) | Repository | [limit-order-settlement](https://github.com/1inch/limit-order-settlement/) | | ---------------- | -------------------------------------------------------------------------- | | Contract | PowerPod.sol | | Contract address | 0xAccfAc2339e16DC80c50d2fa81b5c2B049B4f947 | | Methods | delegate(address delegatee) | | Description | delegate(address delegatee) | ```javascript // Delegates all staking power to self await delegation.connect(resolver).delegate(resolver.address); ``` ## Whitelist resolver | Repository | [limit-order-settlement](https://github.com/1inch/limit-order-settlement/) | | ---------------- | --------------------------------------------------------------------------------------------------------------- | | Contract | WhitelistRegistry.sol | | Contract address | 0xF55684BC536487394B423e70567413faB8e45E26 | | Methods | register() | | Description | Checks if sender is eligible to be whitelisted and put it into the whitelist sorted by staking power descending | ```javascript // Try to put the sender to the whitelist await whitelist.connect(resolver).register(); ``` ## FeeBank | Repository | [limit-order-settlement](https://github.com/1inch/limit-order-settlement/) | | ------------------------- | -------------------------------------------------------------------------- | | Contract | FeeBank.sol | | Contract address | 0xa0844e046a5B7Db55Bb8DcdFfbF0bBF9c6dc6546 | | Methods | | - deposit(uint256 amount) | - depositFor(address account, uint256 amount) - depositWithPermit(uint256 amount, bytes calldata permit) - depositForWithPermit | | Description | Deposits 1INCH for fee deduction when filling orders | ```javascript // Deposit fees to fee bank await feeBank.connect(resolver).deposit(amount); ``` ## Resolving ```solidity struct Order { uint256 salt; address makerAsset; address takerAsset; address maker; address receiver; address allowedSender; // equals to Zero address on public orders uint256 makingAmount; uint256 takingAmount; uint256 offsets; // bytes makerAssetData; // bytes takerAssetData; // bytes getMakingAmount; // this.staticcall(abi.encodePacked(bytes, swapTakerAmount)) => (swapMakerAmount) // bytes getTakingAmount; // this.staticcall(abi.encodePacked(bytes, swapMakerAmount)) => (swapTakerAmount) // bytes predicate; // this.staticcall(bytes) => (bool) // bytes permit; // On first fill: permit.1.call(abi.encodePacked(permit.selector, permit.2)) // bytes preInteraction; // bytes postInteraction; bytes interactions; // concat(makerAssetData, takerAssetData, getMakingAmount, getTakingAmount, predicate, permit, preIntercation, postInteraction) } ``` | Repository | [limit-order-settlement](https://github.com/1inch/limit-order-settlement/) | | ---------------- | -------------------------------------------------------------------------- | | Contract | Settlement.sol | | Contract address | 0xA88800CD213dA5Ae406ce248380802BD53b47647 | | Methods | .settleOrders(bytes calldata data) | | Description | Settles an order | https://business.1inch.com/portal/assets/docs-v2/resolvers/limit-order/resolver-farming-guide.md ## Overview As a resolver, offering farm incentives to delegators can help increase your Unicorn Power balance and consequently, your probability of filling orders. Upon registering as a resolver, the farm contract is automatically deployed. You can fetch the farm by interacting with the `dst1inch` contract's ABI. Note that you can reward delegators with any ERC-20 token, not only tokens from the incentive program. --- ### How to Fetch a Farm with the `dst1inch` Contract 1. Go to the "Read Contract" section of the [`dst1inch` contract](https://etherscan.io/token/0xAccfAc2339e16DC80c50d2fa81b5c2B049B4f947#readContract#F6) on Etherscan. Under the `defaultFarms` method, enter your resolver address within the 'address' parameter and send the call.

resolver-farming

1. Click on the returned farm contract address and navigate to the 'Write Contract' section of the returned contract page. This is your newly generated farm distribution address.

resolver-farming

3. Call the `setDistributor` method using the address that will be managing and distributing rewards.

resolver-farming

4. On the same contract, call `addRewardsToken` with the desired token address that will be distributed to your delegators.

resolver-farming

5. Lastly, on the same contract, call `startFarming`, entering the `rewardsToken` address, amount, and period (both amount and period are uint256). Once called, your farm will have started, and rewards will begin to be distributed to your delegators.

resolver-farming

--- ### How to Replenish Farming Token Balance (Optional) To replenish the token balance of your farm rewards, call `addRewardsToken` and `startFarming` to the farm contract as done in the initial setup. --- ### Example ABI Interaction :::info The example script below reads ABI definitions for both the [dst1inch](https://etherscan.io/token/0xAccfAc2339e16DC80c50d2fa81b5c2B049B4f947#code) and [multiFarmingPod](https://etherscan.io/address/0x1583C1dBe20625d0B752d472E845FbA96D096829#code) contracts. You will need to create two new files and add the ABI definitions which can be found at these URLS: - [`dst1inchABI.json`](https://api.etherscan.io/api?module=contract&action=getabi&address=0xAccfAc2339e16DC80c50d2fa81b5c2B049B4f947&apikey=YourApiKeyToken) - [`abi.json`](https://api.etherscan.io/api?module=contract&action=getabi&address=0x1583C1dBe20625d0B752d472E845FbA96D096829&apikey=YourApiKeyToken) Don't forget to replace 'YourApiKeyToken' with your actual Etherscan API key at the end of each linked URL above! ::: ```javascript require("dotenv").config(); //for accessing sensitive information such as private keys, API keys, etc. const { Web3 } = require("web3"); const web3 = new Web3(`Your_ethereum_RPC`); const fs = require("fs"); const dst1inchABI = JSON.parse(fs.readFileSync("dst1inchABI.json", "utf8")); const farmABI = JSON.parse(fs.readFileSync("abi.json", "utf8")); const erc20Abi = [ // ERC20 ABI fragment (for contract approval) { constant: false, inputs: [ { name: "spender", type: "address" }, { name: "value", type: "uint256" } ], name: "approve", outputs: [ { name: "", type: "bool" } ], payable: false, stateMutability: "nonpayable", type: "function" } ]; const dst1inch = "0xAccfAc2339e16DC80c50d2fa81b5c2B049B4f947"; const dst1inchContract = new web3.eth.Contract(dst1inchABI, dst1inch); const account = "YOUR_REGISTERED_ADDRESS"; const privateKey = "YOUR_PRIVATE_KEY"; async function distributeFarmingRewards(resolverAddress, newDistributor, rewardsToken, amount, period) { try { // Call defaultFarms with your resolver address const farmAddress = await dst1inchContract.methods.defaultFarms(resolverAddress).call(); console.log(`Farm address: ${farmAddress}`); const farmContract = new web3.eth.Contract(farmABI, farmAddress); const tokenContract = new web3.eth.Contract(erc20Abi, rewardsToken); // Call setDistributor on the returned farm address const setDistributorTx = { from: resolverAddress, to: farmAddress, data: farmContract.methods.setDistributor(newDistributor).encodeABI() }; const setDistributorSignedTx = await web3.eth.accounts.signTransaction(setDistributorTx, privateKey); await web3.eth.sendSignedTransaction(setDistributorSignedTx.rawTransaction); console.log("Distributor set successfully"); // Approve the farm contract to spend the tokens const approveTx = { from: newDistributor, to: rewardsToken, data: tokenContract.methods.approve(farmAddress, amount).encodeABI() }; const approveSignedTx = await web3.eth.accounts.signTransaction(approveTx, privateKey); await web3.eth.sendSignedTransaction(approveSignedTx.rawTransaction); console.log("Approval transaction confirmed"); // Call addRewardsToken with the address of distribution token const addRewardsTokenTx = { from: newDistributor, to: farmAddress, data: farmContract.methods.addRewardsToken(rewardsToken).encodeABI() }; const addRewardsTokenSignedTx = await web3.eth.accounts.signTransaction(addRewardsTokenTx, privateKey); await web3.eth.sendSignedTransaction(addRewardsTokenSignedTx.rawTransaction); console.log("Rewards token added successfully"); // Call startFarming(address rewardsToken, uint256 amount, uint256 period) const startFarmingTx = { from: newDistributor, to: farmAddress, data: farmContract.methods.startFarming(rewardsToken, amount, period).encodeABI() }; const startFarmingSignedTx = await web3.eth.accounts.signTransaction(startFarmingTx, privateKey); await web3.eth.sendSignedTransaction(startFarmingSignedTx.rawTransaction); console.log("Farming started successfully"); } catch (error) { console.error("Error distributing farming rewards:", error); } } const resolverAddress = account; const newDistributor = account; // you can use any address as the distributor const rewardsToken = "0xRewardsTokenAddress"; const amount = web3.utils.toWei("Amount", "Token Name"); // Amount of rewards tokens const period = 3600; // Period in seconds distributeFarmingRewards(resolverAddress, newDistributor, rewardsToken, amount, period) .then(() => console.log("Farming rewards distributed successfully")) .catch((error) => console.error("Error distributing farming rewards:", error)); ``` Have questions? Reach out to us in the live support chat! https://business.1inch.com/portal/assets/docs-v2/resolvers/limit-order/unicorn-power-setup.md These steps initialize Unicorn Power farming: 1. Approve staking 1INCH for 1inch contract. 2. Stake 1INCH to gain Unicorn Power. 3. Add a delegation pod to enable power delegation. 4. Register yourself to be a resolver. 5. (Optional) Change the default farm to a new farm. 6. Delegate available Unicorn Power to yourself. 7. Register a resolver at the whitelist. 8. Set up a worker to settle orders. ## Setup script example ```javascript const { ether, time } = require("@1inch/solidity-utils"); const { ethers } = require("hardhat"); // Setup environment const inch = await ethers.getContractAt("IERC20", "0x111111111117dc0aa78b770fa6a738034120c302"); const st1inch = await ethers.getContractAt("IERC20", "0x9a0c8ff858d273f57072d714bca7411d717501d7"); const powerPod = await ethers.getContractAt("IERC20", "0xaccfac2339e16dc80c50d2fa81b5c2b049b4f947"); const whitelist = await ethers.getContractAt("WhitelistRegistry", "0xF55684BC536487394B423e70567413faB8e45E26"); const stakeAmount = ether("1000000"); const lockTime = time.duration.years("2"); const myShareToken = { name: "MyShareTokenName", symbol: "MST" }; const worker = "..."; // Address of wallet which send transaction const [resolver] = await ethers.getSigners(); // Ethers setup script // approve 1inch staking await await inch.connect(resolver).approve(st1inch.address, stakeAmount); // stake 1inch token await (await st1inch.connect(resolver).deposit(stakeAmount, lockTime)).wait(); // add delegation pod to // 1. make it possible for any user to delegate staking power to // the resolver's account // 2. make it possible for a resolver to allocate its staking power for itself await (await st1inch.connect(resolver).addPod(powerPod.address)).wait(); // register resolver's delegation token to count stakers' shares and rewards await ( await powerPod.connect(resolver).functions["register(string,string)"](myShareToken.name, myShareToken.symbol) ).wait(); // Delegate staked power to self await (await powerPod.connect(resolver).delegate(resolver.address)).wait(); // Whitelist resolver (there should be enough unicorn power to be in top 10) await (await whitelist.connect(resolver).register()).wait(); // Add worker address from which order settlement will be executed await (await whitelist.connect(resolver).promote(1, worker)).wait(); ``` https://business.1inch.com/portal/assets/docs-v2/resolvers/intent/evm.md ## Kick off integration Once verified, it's time to integrate. Start exploring [Swap APIs technical documentation](/documentation/apis/swap/intent-swap/introduction) to learn more about Intent Swaps. ## Testing Complete your resolver technical deployment and validate it in a test environment. For testing, create and fulfill your own Intent Swaps orders using [1inch Fusion Resolver Example on GitHub](https://github.com/1inch/fusion-resolver-example). https://business.1inch.com/portal/assets/docs-v2/resolvers/intent/solana.md ## Read the docs Once verified, it's time to integrate. The following resources will help you start fast: - [Solana Intent Swaps SDK](https://github.com/1inch/solana-fusion-sdk) - [Intent Swaps API documentation](/documentation/apis/swap/intent-swap/introduction). When using the OpenAPI specs, make sure you select Solana in the network selector to get the correct information. - [Example integration tests demonstrating order creation, fill, and cancellation flows on Solana using the Bankrun simulator](https://github.com/1inch/solana-fusion-sdk/blob/master/tests/fusion-order.spec.ts). - [Core method building the fill transaction instruction](https://github.com/1inch/solana-fusion-sdk/blob/master/src/contracts/fusion-swap-contract.ts#L135), including resolver whitelist check and required account layout. ## Retrieve active orders Use the Intent Swap API endpoint to fetch active orders: [GET /v2.0/1/order/active](/documentation/apis/swap/intent-swap/orders/v2.0/1/order/active/method/get). ## Calculate current price Calculate the current price, using the [Dutch auction parameters](https://github.com/1inch/solana-fusion-sdk/blob/master/src/amount-calculator/auction-calculator/README.md). ## Execute order on-chain Use the SDK method [`FusionSwapContract.fill()`](https://github.com/1inch/solana-fusion-sdk/blob/master/src/contracts/fusion-swap-contract.ts#L135) to build and send the on-chain fill instruction. This call constructs the full transaction, including the resolver's whitelist verification and all required token and escrow accounts. ## Execution tips - The `fill` instruction does not support post-actions. You must send the taker token before receiving the maker token. - Solana has strict limits on transaction size. If you need extra actions, use batching tools (for example, Jito) to send several transactions together. - In most cases, you can execute everything from a regular wallet (EOA) without writing your own program. https://business.1inch.com/portal/assets/docs-v2/resolvers/cross-chain/cross-chain-test-examples.md This section provides a practical, end-to-end overview of working with the **1inch Cross-Chain Swaps** infrastructure from a resolver perspective. The guide focuses on **three main test scenarios**, each demonstrating a critical part of the resolver workflow: 1. **Basic Cross-Chain Order Execution** Demonstrates setting up the environment, initializing chains and wallets, deploying escrows, and completing a single-fill swap. 2. **Partial Fills (Multi-Secret Orders)** Shows how to construct multi-fill orders using Merkle-based hash-locks and execute partial fills while maintaining correct resolver logic. 3. **Order Cancellation Flow** Covers creating a cancelable order, interacting with the corresponding hash-lock, and validating the resolver and user withdrawals. Together, these examples walk you through key implementation steps such as environment initialization, escrow deployment on source and destination chains, event handling, secret management, and balance validation. They are designed to help you confidently validate your resolver integration and ensure compatibility with the latest **Cross-Chain SDK** and **Resolver API** requirements. ## Test 1: Basic cross-chain order ### Initialization Set up the environment including initializing chains, wallets, and contracts. ```javascript import 'dotenv/config' import {expect, jest} from '@jest/globals' import {createServer, CreateServerReturnType} from 'prool' import {anvil} from 'prool/instances' import Sdk from '@1inch/cross-chain-sdk' import { computeAddress, ContractFactory, JsonRpcProvider, MaxUint256, parseEther, parseUnits, randomBytes, Wallet as SignerWallet } from 'ethers' import {uint8ArrayToHex, UINT_40_MAX} from '@1inch/byte-utils' import assert from 'node:assert' import {ChainConfig, config} from './config' import {Wallet} from './wallet' import {Resolver} from './resolver' import {EscrowFactory} from './escrow-factory' import factoryContract from '../dist/contracts/TestEscrowFactory.sol/TestEscrowFactory.json' import resolverContract from '../dist/contracts/Resolver.sol/Resolver.json' const {Address} = Sdk jest.setTimeout(1000 * 60) // Private keys for testing (NEVER EXPOSE YOUR REAL PRIVATE KEY) const userPk = '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d' const resolverPk = '0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a' describe('Resolving example', () => { const srcChainId = config.chain.source.chainId const dstChainId = config.chain.destination.chainId // Define chain-related data type Chain = { node?: CreateServerReturnType | undefined provider: JsonRpcProvider escrowFactory: string resolver: string } // Variables for chain and wallet data let src: Chain let dst: Chain let srcChainUser: Wallet let dstChainUser: Wallet let srcChainResolver: Wallet let dstChainResolver: Wallet let srcFactory: EscrowFactory let dstFactory: EscrowFactory let srcResolverContract: Wallet let dstResolverContract: Wallet let srcTimestamp: bigint // Store the current timestamp // Utility to increase time on both chains async function increaseTime(t: number): Promise { await Promise.all([src, dst].map((chain) => chain.provider.send('evm_increaseTime', [t]))) } // Setup before running tests beforeAll(async () => { // Initialize chains ;[src, dst] = await Promise.all([initChain(config.chain.source), initChain(config.chain.destination)]) // Initialize wallets srcChainUser = new Wallet(userPk, src.provider) dstChainUser = new Wallet(userPk, dst.provider) srcChainResolver = new Wallet(resolverPk, src.provider) dstChainResolver = new Wallet(resolverPk, dst.provider) // Initialize factory contracts srcFactory = new EscrowFactory(src.provider, src.escrowFactory) dstFactory = new EscrowFactory(dst.provider, dst.escrowFactory) // Top up user account and approve tokens await srcChainUser.topUpFromDonor( config.chain.source.tokens.USDC.address, config.chain.source.tokens.USDC.donor, parseUnits('1000', 6) ) await srcChainUser.approveToken( config.chain.source.tokens.USDC.address, config.chain.source.limitOrderProtocol, MaxUint256 ) // Initialize resolver contracts and top up balances srcResolverContract = await Wallet.fromAddress(src.resolver, src.provider) dstResolverContract = await Wallet.fromAddress(dst.resolver, dst.provider) await dstResolverContract.topUpFromDonor( config.chain.destination.tokens.USDC.address, config.chain.destination.tokens.USDC.donor, parseUnits('2000', 6) ) await dstChainResolver.transfer(dst.resolver, parseEther('1')) await dstResolverContract.unlimitedApprove(config.chain.destination.tokens.USDC.address, dst.escrowFactory) // Store the current timestamp from the source chain srcTimestamp = BigInt((await src.provider.getBlock('latest'))!.timestamp) }) // Retrieve balances for maker and resolver on both chains async function getBalances( srcToken: string, dstToken: string ): Promise<{src: {user: bigint; resolver: bigint}; dst: {user: bigint; resolver: bigint}}> { return { src: { user: await srcChainUser.tokenBalance(srcToken), resolver: await srcResolverContract.tokenBalance(srcToken) }, dst: { user: await dstChainUser.tokenBalance(dstToken), resolver: await dstResolverContract.tokenBalance(dstToken) } } } ``` --- ### Fetching orders Fetch existing orders via 1inch Cross-Chain Websocket API. ```javascript // Test suite for filling an order describe('Fill', () => { // Swapping USDC between Ethereum and BSC it('should swap Ethereum USDC -> Bsc USDC. Single fill only', async () => { const initialBalances = await getBalances( config.chain.source.tokens.USDC.address, config.chain.destination.tokens.USDC.address ) // Create a cross-chain order const secret = uint8ArrayToHex(randomBytes(32)) const order = Sdk.CrossChainOrder.new( new Address(src.escrowFactory), { salt: Sdk.randBigInt(1000n), maker: new Address(await srcChainUser.getAddress()), makingAmount: parseUnits('100', 6), takingAmount: parseUnits('99', 6), makerAsset: new Address(config.chain.source.tokens.USDC.address), takerAsset: new Address(config.chain.destination.tokens.USDC.address) }, { hashLock: Sdk.HashLock.forSingleFill(secret), timeLocks: Sdk.TimeLocks.new({ srcWithdrawal: 10n, srcPublicWithdrawal: 120n, srcCancellation: 121n, srcPublicCancellation: 122n, dstWithdrawal: 10n, dstPublicWithdrawal: 100n, dstCancellation: 101n }), srcChainId, dstChainId, srcSafetyDeposit: parseEther('0.001'), dstSafetyDeposit: parseEther('0.001') }, { auction: new Sdk.AuctionDetails({ initialRateBump: 0, points: [], duration: 120n, startTime: srcTimestamp }), whitelist: [ { address: new Address(src.resolver), allowFrom: 0n } ], resolvingStartTime: 0n }, { nonce: Sdk.randBigInt(UINT_40_MAX), allowPartialFills: false, allowMultipleFills: false } ) // Sign the order and calculate its hash const signature = await srcChainUser.signOrder(srcChainId, order); const orderHash = order.getOrderHash(srcChainId); ``` --- ### Escrow creation and deposit Create and deposit assets into source and destination chain escrows. ```javascript // Initialize resolver contract and log the order fill process const resolverContract = new Resolver(src.resolver, dst.resolver); console.log(`[${srcChainId}]`, `Filling order ${orderHash}`); // Fill the order on the source chain const fillAmount = order.makingAmount; const { txHash: orderFillHash, blockHash: srcDeployBlock } = await srcChainResolver.send( resolverContract.deploySrc( srcChainId, order, signature, Sdk.TakerTraits.default() .setExtension(order.extension) .setAmountMode(Sdk.AmountMode.maker) .setAmountThreshold(order.takingAmount), fillAmount ) ); console.log(`[${srcChainId}]`, `Order ${orderHash} filled for ${fillAmount} in tx ${orderFillHash}`); // Handle event and deposit on the destination chain const srcEscrowEvent = await srcFactory.getSrcDeployEvent(srcDeployBlock); const dstImmutables = srcEscrowEvent[0] .withComplement(srcEscrowEvent[1]) .withTaker(new Address(resolverContract.dstAddress)); console.log(`[${dstChainId}]`, `Depositing ${dstImmutables.amount} for order ${orderHash}`); const { txHash: dstDepositHash, blockTimestamp: dstDeployedAt } = await dstChainResolver.send( resolverContract.deployDst(dstImmutables) ); console.log(`[${dstChainId}]`, `Created dst deposit for order ${orderHash} in tx ${dstDepositHash}`); // Retrieve and calculate escrow contract addresses const ESCROW_SRC_IMPLEMENTATION = await srcFactory.getSourceImpl(); const ESCROW_DST_IMPLEMENTATION = await dstFactory.getDestinationImpl(); const srcEscrowAddress = new Sdk.EscrowFactory(new Address(src.escrowFactory)).getSrcEscrowAddress( srcEscrowEvent[0], ESCROW_SRC_IMPLEMENTATION ); const dstEscrowAddress = new Sdk.EscrowFactory(new Address(dst.escrowFactory)).getDstEscrowAddress( srcEscrowEvent[0], srcEscrowEvent[1], dstDeployedAt, new Address(resolverContract.dstAddress), ESCROW_DST_IMPLEMENTATION ); ``` --- ### Escrow verification and withdrawals Withdraw funds from the escrow after relayer validation ```javascript await increaseTime(11); // User shares secret after validation of dst escrow deployment console.log(`[${dstChainId}]`, `Withdrawing funds for user from ${dstEscrowAddress}`); await dstChainResolver.send( resolverContract.withdraw("dst", dstEscrowAddress, secret, dstImmutables.withDeployedAt(dstDeployedAt)) ); console.log(`[${srcChainId}]`, `Withdrawing funds for resolver from ${srcEscrowAddress}`); const { txHash: resolverWithdrawHash } = await srcChainResolver.send( resolverContract.withdraw("src", srcEscrowAddress, secret, srcEscrowEvent[0]) ); console.log( `[${srcChainId}]`, `Withdrew funds for resolver from ${srcEscrowAddress} to ${src.resolver} in tx ${resolverWithdrawHash}` ); const resultBalances = await getBalances( config.chain.source.tokens.USDC.address, config.chain.destination.tokens.USDC.address ); // User transferred funds to resolver on source chain expect(initialBalances.src.user - resultBalances.src.user).toBe(order.makingAmount); expect(resultBalances.src.resolver - initialBalances.src.resolver).toBe(order.makingAmount); // Resolver transferred funds to user on destination chain expect(resultBalances.dst.user - initialBalances.dst.user).toBe(order.takingAmount); expect(initialBalances.dst.resolver - resultBalances.dst.resolver).toBe(order.takingAmount); ``` --- ## Test 2: Partial fill order Create an order with multiple secrets, then execute those fills using each correct secret. ### Creating the order ```javascript it("should swap Ethereum USDC -> Bsc USDC. Multiple fills. Fill 100%", async () => { const initialBalances = await getBalances( config.chain.source.tokens.USDC.address, config.chain.destination.tokens.USDC.address ); // User creates order with 11 secrets (10 parts) // note: use a cryptographically secure random number for real-life scenarios const secrets = Array.from({ length: 11 }).map(() => uint8ArrayToHex(randomBytes(32))); const secretHashes = secrets.map((s) => Sdk.HashLock.hashSecret(s)); const leaves = Sdk.HashLock.getMerkleLeaves(secrets); const order = Sdk.CrossChainOrder.new( new Address(src.escrowFactory), { salt: Sdk.randBigInt(1000n), maker: new Address(await srcChainUser.getAddress()), makingAmount: parseUnits("100", 6), takingAmount: parseUnits("99", 6), makerAsset: new Address(config.chain.source.tokens.USDC.address), takerAsset: new Address(config.chain.destination.tokens.USDC.address) }, { hashLock: Sdk.HashLock.forMultipleFills(leaves), timeLocks: Sdk.TimeLocks.new({ srcWithdrawal: 10n, // 10s finality lock for test srcPublicWithdrawal: 120n, // 2m for private withdrawal srcCancellation: 121n, // 1sec public withdrawal srcPublicCancellation: 122n, // 1sec private cancellation dstWithdrawal: 10n, // 10s finality lock for test dstPublicWithdrawal: 100n, // 100sec private withdrawal dstCancellation: 101n // 1sec public withdrawal }), srcChainId, dstChainId, srcSafetyDeposit: parseEther("0.001"), dstSafetyDeposit: parseEther("0.001") }, { auction: new Sdk.AuctionDetails({ initialRateBump: 0, points: [], duration: 120n, startTime: srcTimestamp }), whitelist: [ { address: new Address(src.resolver), allowFrom: 0n } ], resolvingStartTime: 0n }, { nonce: Sdk.randBigInt(UINT_40_MAX), allowPartialFills: true, allowMultipleFills: true } ); const signature = await srcChainUser.signOrder(srcChainId, order); const orderHash = order.getOrderHash(srcChainId); ``` ### Filling the order ```javascript // Resolver fills order const resolverContract = new Resolver(src.resolver, dst.resolver); console.log(`[${srcChainId}]`, `Filling order ${orderHash}`); const fillAmount = order.makingAmount; const idx = secrets.length - 1; // last index to fulfill const { txHash: orderFillHash, blockHash: srcDeployBlock } = await srcChainResolver.send( resolverContract.deploySrc( srcChainId, order, signature, Sdk.TakerTraits.default() .setExtension(order.extension) .setInteraction( new Sdk.EscrowFactory(new Address(src.escrowFactory)).getMultipleFillInteraction( Sdk.HashLock.getProof(leaves, idx), idx, secretHashes[idx] ) ) .setAmountMode(Sdk.AmountMode.maker) .setAmountThreshold(order.takingAmount), fillAmount, Sdk.HashLock.fromString(secretHashes[idx]) ) ); console.log(`[${srcChainId}]`, `Order ${orderHash} filled for ${fillAmount} in tx ${orderFillHash}`); const srcEscrowEvent = await srcFactory.getSrcDeployEvent(srcDeployBlock); const dstImmutables = srcEscrowEvent[0] .withComplement(srcEscrowEvent[1]) .withTaker(new Address(resolverContract.dstAddress)); console.log(`[${dstChainId}]`, `Depositing ${dstImmutables.amount} for order ${orderHash}`); const { txHash: dstDepositHash, blockTimestamp: dstDeployedAt } = await dstChainResolver.send( resolverContract.deployDst(dstImmutables) ); console.log(`[${dstChainId}]`, `Created dst deposit for order ${orderHash} in tx ${dstDepositHash}`); const secret = secrets[idx]; const ESCROW_SRC_IMPLEMENTATION = await srcFactory.getSourceImpl(); const ESCROW_DST_IMPLEMENTATION = await dstFactory.getDestinationImpl(); const srcEscrowAddress = new Sdk.EscrowFactory(new Address(src.escrowFactory)).getSrcEscrowAddress( srcEscrowEvent[0], ESCROW_SRC_IMPLEMENTATION ); const dstEscrowAddress = new Sdk.EscrowFactory(new Address(dst.escrowFactory)).getDstEscrowAddress( srcEscrowEvent[0], srcEscrowEvent[1], dstDeployedAt, new Address(resolverContract.dstAddress), ESCROW_DST_IMPLEMENTATION ); await increaseTime(11); // finality lock passed // User shares secret after validation of dst escrow deployment console.log(`[${dstChainId}]`, `Withdrawing funds for user from ${dstEscrowAddress}`); await dstChainResolver.send( resolverContract.withdraw("dst", dstEscrowAddress, secret, dstImmutables.withDeployedAt(dstDeployedAt)) ); console.log(`[${srcChainId}]`, `Withdrawing funds for resolver from ${srcEscrowAddress}`); const { txHash: resolverWithdrawHash } = await srcChainResolver.send( resolverContract.withdraw("src", srcEscrowAddress, secret, srcEscrowEvent[0]) ); console.log( `[${srcChainId}]`, `Withdrew funds for resolver from ${srcEscrowAddress} to ${src.resolver} in tx ${resolverWithdrawHash}` ); const resultBalances = await getBalances( config.chain.source.tokens.USDC.address, config.chain.destination.tokens.USDC.address ); // User transferred funds to resolver on the source chain expect(initialBalances.src.user - resultBalances.src.user).toBe(order.makingAmount); expect(resultBalances.src.resolver - initialBalances.src.resolver).toBe(order.makingAmount); // Resolver transferred funds to user on destination chain expect(resultBalances.dst.user - initialBalances.dst.user).toBe(order.takingAmount); expect(initialBalances.dst.resolver - resultBalances.dst.resolver).toBe(order.takingAmount); }); ``` --- ## Test 3: Order cancellation Cancel an order by creating a new order with a hash lock and managing the withdrawal process. ```javascript // ---- Order cancellation ---- describe('Cancel', () => { it('should cancel swap Ethereum USDC -> Bsc USDC', async () => { const initialBalances = await getBalances( config.chain.source.tokens.USDC.address, config.chain.destination.tokens.USDC.address ) // User creates order // note: use a cryptographically secure random number for real-life scenarios const hashLock = Sdk.HashLock.forSingleFill(uint8ArrayToHex(randomBytes(32))) const order = Sdk.CrossChainOrder.new( new Address(src.escrowFactory), { salt: Sdk.randBigInt(1000n), maker: new Address(await srcChainUser.getAddress()), makingAmount: parseUnits('100', 6), takingAmount: parseUnits('99', 6), makerAsset: new Address(config.chain.source.tokens.USDC.address), takerAsset: new Address(config.chain.destination.tokens.USDC.address) }, { hashLock, timeLocks: Sdk.TimeLocks.new({ srcWithdrawal: 0n, // no finality lock for test srcPublicWithdrawal: ``` https://business.1inch.com/portal/assets/docs-v2/resolvers/exclusive-resolver-terms.md The Exclusive Resolver API is a new feature available for resolvers within the 1inch Fusion mode. This API allows a 1inch Fusion resolver to be chosen as an exclusive filler at the beginning of the auction. By providing a quote, a resolver commits to executing the order as soon as the user submits it to the 1inch Relayer API. The order must be executed before the end of the exclusivity period. The executed order's maker amount must match the amount provided by the resolver's API for a specific `initialRateBump` **Performance metrics:** - **Maximum response time (including network):** 500ms - **SLA:** 90%. Calculated from: _Number of executed exclusive orders / Total resolver's exclusive orders x 100%_. Only orders submitted by the user before the exclusivity period ends are counted.

### **API Endpoint:** `POST /quote` Returns a list of resolver's quotes, sorted by the maker's amount (descending), along with the corresponding taker amount rate bump from the minimal return amount. ### Body - `makerAsset`: the address of the token that the user is selling. - `takerAsset`: the address of the token that the user is buying. - `makerAmount`: the amount of the `makerAsset` that the user is selling. - `minTakerAmount`: the minimum amount of the `takerAsset` that the user expects to receive. - `makerAddress`: the wallet address of the user requesting a quote. - `quoteId`: a unique ID of the quote. - `gasBumpEstimate` _(New!)_: estimated gas cost adjustment based on order fill through the 1inch router. - `gasPriceEstimate` _(New!)_: estimated gas price for executing the order. These new fields allow resolvers to fine-tune their quotes more precisely based on gas costs. ### Response - `grid`: an array containing details of proposed price improvements (max items: 50): - `makerAmount`: partial or full fill amount. - `initialRateBump`: rate bump at the beginning of the Dutch auction. - **Min**: 0 - **Max**: 16777215 - **10000000** -> **100%** ### Example **Request:** ```text POST /quote ``` **Body:** ```json { "chainId": 1, "makerAsset": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "takerAsset": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "makerAmount": "1000000000000000000", "minTakerAmount": "2400000000", "makerAddress": "0xbe0eb53f46cd790cd13851d5eff43d12404d33e8", "quoteId": "cf872857-c456-4f4f-aff0-84f7bebb7df2", "gasBumpEstimate": "50000", "gasPriceEstimate": "30000000000" } ``` **Response (HTTP code: 200):** ```json { "grid": [ { "makerAmount": "1000000000000000000", // 1 ETH (100%) "initialRateBump": 10000 // improve minTakerAmount by 0.1% }, { "makerAmount": "500000000000000000", // 0.5 ETH (50%) "initialRateBump": 100000 // improve minTakerAmount by 1% }, { "makerAmount": "100000000000000000", // 0.1 ETH (10%) "initialRateBump": 1000000 // improve minTakerAmount by 10% } ] } ``` ## Errors Use these error codes in API response in case: - **_400 Bad Request_**: - Token pair is not supported. - Cannot provide a quote for specified `makerAmount` / `minTakerAmount` / `makerAddress`. - **_500 Internal Server Error_**: - Unexpected error on the server. ## Useful tips and formulas ## Calculate: "**How much does the user receive at the start of the auction?**" ```text takerAmount = minTakerAmount * (initialRateBump + 10000000) / 10000000 ``` ## Calculate: "**What initial rate bump is needed for a specific `takerAmount`?**" ```text initialRateBump = (takerAmount * 10000000 / minTakerAmount) - 10000000 ``` ## **Understanding `gasBumpEstimate` and `gasPriceEstimate`** - `gasBumpEstimate`: this value is calculated based on the estimated gas cost required to fill an order via the 1inch router. If you anticipate that your execution will be more gas-efficient, you can adjust your `initialRateBump` accordingly to reflect this advantage. - Detailed calculation: [Fusion SDK Gas Bump Calculation](https://github.com/1inch/fusion-sdk/blob/8452c43be3edfccf181e5c1003ffb55c6b32b2dc/src/auction-calculator/auction-calculator.ts#L90) - `gasPriceEstimate`: this estimate provides an expected gas price for executing the transaction, helping to determine the most optimal bid pricing strategy. - Detailed calculation: [Fusion SDK Gas Estimate Calculation](https://github.com/1inch/fusion-sdk/blob/8452c43be3edfccf181e5c1003ffb55c6b32b2dc/src/auction-calculator/auction-calculator.ts#L76) Additionally, for reference, you can check the related contract logic used in gas calculations: [Limit Order Settlement Contract Code](https://github.com/1inch/limit-order-settlement/blob/2eef6f86bf0142024f9a8bf054a0256b41d8362a/contracts/extensions/BaseExtension.sol#L140) ## **How to determine if my resolver is selected for exclusive execution?** To check whether your resolver is selected for exclusive execution, follow steps below. 1. Import the necessary modules from the 1inch Fusion SDK: ```typescript import { Extension, FusionOrder, now } from "@1inch/fusion-sdk"; import { Address } from "@1inch/fusion-sdk"; ``` 1. Create an object containing the details of the Fusion order: ```typescript const orderData = { ... } ``` 1. Provide the encoded order extension data ```typescript const ext = "0x..."; // Example encoded order extension ``` 4. Decode and process the Fusion order ```typescript const fusionOrder = FusionOrder.fromDataAndExtension(orderData, Extension.decode(ext)); ``` 5. Check if the resolver is exclusive. Define the resolver's promotee address and check whether it has exclusive execution rights using the updated Fusion SDK logic: ```typescript const resolverPromoteeAddress = Address.ZERO_ADDRESS; const isExclusive = fusionOrder.isExclusiveResolver(resolverPromoteeAddress); const isExclusivePeriod = fusionOrder.isExclusivityPeriod(now()); ``` 6. Interpret the results - `isExclusive` -> Returns `true` if the resolver is selected for exclusive execution. - `isExclusivePeriod` -> Returns `true` if the exclusivity period is still active. For further details, refer to the relevant Fusion SDK implementation: - [Fusion Order Exclusive Resolver Check](https://github.com/1inch/fusion-sdk/blob/5b112ec34fc12e61c496cbe54cc93987accfce4b/src/fusion-order/fusion-order.ts#L302) - [Fusion Order Exclusivity Period Check](https://github.com/1inch/fusion-sdk/blob/5b112ec34fc12e61c496cbe54cc93987accfce4b/src/fusion-order/fusion-order.ts#L588)
## FAQ https://business.1inch.com/portal/assets/docs-v2/faq/api-error-messages-explained.md If you're encountering issues with our API, this guide will help you understand some common error messages and their meanings. _Please note: This list is not comprehensive. Other errors involving the dynamics of generic EVM transaction estimations might require deeper investigation._ :::tip Save the `X-Request-Id` header Every API response includes an `X-Request-Id` header. When you encounter an error, save this value -- it uniquely identifies your request in server logs. You can share it with support or use the [MCP `debug` tool](/portal/documentation/ai-integration/tools-reference#debug) to pull matching log lines for your organization. ::: ## General Errors - **400 - Bad Request:** This indicates an incorrect parameter. Ensure that all required parameters are correctly set. - **403 - Forbidden:** This indicates that the origin address is a banned or restricted address (see [terms and conditions](/assets/legal-docs/terms_of_service_public_api_20260727.pdf)) - **403 - Unauthorized:** Your API key is missing or incomplete. Please double check that correct key was entered and try again. - **429 - Rate Limit:** You've made too many requests to the API within a 1 second time period. Reduce your requests per second (RPS) to stay below the free 1rps limit. If higher limits are required, please fill out the 1inch Network Enterprise API application. - **503 - Banned IP:** The IP address you're using to access the API is banned. Please ensure you're adhering to 1inch Network's terms of use. - **The API isn't behaving as expected:** In certain cases you may be passing in a parameter supported by the API but the wrong type or expected value. For example `disableEstimate=true` will work but `disableEstimate=True` will not. All parameters are CAsE SenSiTive. This is especially apparent when stringifying boolean values in python. ## Specific Errors - **Cannot sync token:** The token either doesn't exist on the blockchain or isn't a valid token. Also, double check that you are using the correct chainID in relation to the token used. - **Amount is not set:** The 'amount' field hasn't been specified. - **_Please note:_** All token amounts must be in the correct decimal format! The decimal format can be found by looking up the token contract on the block explorer. - **Example:** If the [source token is DAI](https://etherscan.io/token/0x6b175474e89094c44da98b954eedeac495271d0f), and you are sending 1 DAI, the amount with 18 decimals would be `1000000000000000000`. Token Decimals - **Src is not set:** The 'src' (source token contract) field is missing. - **Dst is not set:** The 'dst' (destination token contract) field is missing. - **Insufficient liquidity:** The aggregator can't find a route for the swap due to low liquidity. ## Swap-specific Errors - **Not enough src balance:** This means your source token balance is insufficient. It provides the required amount and your current balance. - **Not enough Allowance:** You haven't granted the necessary token allowance to the 1inch router. It'll indicate the amount, current allowance, and the spender address, e.g., "Spender: 0x1111111254eeb25477b68fb85ed929f73a960582". - **Insufficient liquidity:** As mentioned earlier, the aggregator couldn't route the swap. :::info For workloads that require higher RPS, expanded quotas, or sustained performance, explore the available paid plans on the [**pricing page**](/pricing). For organizations with significant trading volumes or advanced requirements, [**complete this application**](/get-enterprise) to explore tailored Enterprise options. ::: https://business.1inch.com/portal/assets/docs-v2/faq/api-troubleshooting.md ## What is the rate limit? As of August 2023, all free public API usage has been reduced to 1 requests per second (rps). :::info For workloads that require higher RPS, expanded quotas, or sustained performance, explore the available paid plans on the [**pricing page**](/pricing). For organizations with significant trading volumes or advanced requirements, [**complete this application**](/get-enterprise) to explore tailored Enterprise options. ::: ## Subscription usage headers Authenticated API responses include three headers describing your monthly subscription quota: - `X-SubscriptionLimit-Limit` -- the monthly call quota of the product group you invoked (top-up credits included). - `X-SubscriptionLimit-Remaining` -- the approximate number of calls remaining in the current billing period. - `X-SubscriptionLimit-Reset` -- the unix timestamp (seconds) when the billing period renews. :::info Values are approximate: they refresh within roughly 35-50 seconds and may briefly **understate** your consumption, but never overstate it -- an alert on `X-SubscriptionLimit-Remaining` cannot fire from a false "quota exhausted" reading. The one exception is the first minute after your billing period renews, when `X-SubscriptionLimit-Remaining` may briefly still show the previous period's value. Quotas are tracked per product group, so two endpoints in the same group report the same numbers. ::: The headers are absent on plans with an unlimited quota, on unauthenticated responses, and on `429` responses produced by the per-second RPS limiter (those carry `X-RateLimit-Retry-After` instead). `429` responses caused by an exhausted monthly subscription quota do include them. ## "Insufficient Liquidity" error This error can occur for a number of reasons: - The liquidity pool has not been queried - make sure one of the aggregated sources has at least 10k of a connector token listed below. - If you're getting a quote, please make sure to take into account the decimals of the token. Do not include decimal places in the "from token amount". The API returns the number of decimal places the token supports when calling /quote or /swap - There's an error in the server and is unable to find a path, this may require contacting the 1inch support team for more investigation. ## "Cannot Estimate" This error occurs when the eth_estimateGas RPC call throws an error. There are many reasons why this can throw an error, and they all tend to mean the transaction is guaranteed to fail. This is a protection, so a transaction is broadcast that's going to fail. To find the precise error, disableEstimate=true can be used in the query and an eth_estimateGas RPC call can be done locally. The transaction can also be simulated with a platform like tenderly or blocknative for a better look at the transaction trace. Some common reasons why a transaction may fail: - A token has a fee on transfer or swap and the slippage tolerance needs to be increased - A token has a fee on transfer _and_ the fee and referrer parameter is set causing the transaction to always fail - The rate has expired and changed in the latest block. ## Troubleshooting insufficient allowance / balance In the case of one of these errors, you may not have a[ token approval](https://help.1inch.io/en/articles/6147312-token-approvals) or you're missing the token balance. To do a token approval, you can use the API to approve the token to the 1inch router. Insufficient Allowance --- ## What are the "parts" parameters? The parts parameters is comprised of 3 things - mainRouteParts - virtualParts - parts Each of these has their own unique meaning. The main route parts denotes the initial split and can be seen on the UI. In this example it's the 70%/30% split Parts Parameter The parts parameter is how many individual blocks there can be. In the picture above we can see 4 blocks with the top being the first main route part with 2 parts to it and the bottom one being the second main route part with 2 parts in it. The virtual parts is the splits _inside_ of a part. We can see in the first main route part above, in the first block there are 2 virtual parts splitting the USDC between hashflow and uniswap. In the second main route part we can see three virtual parts in the first block, a hashflow route with 2 different rates and one to uniswap. ## What is the "Complexity level?" Complexity level is how many steps a swap can go through, below are the levels and what it means for the swap. Each level adds to the previous level. - **Complexity level 0:** the source token can be wrapped or unwrapped and swapped through one liquidity pool per main route part. - **Complexity level 1:** the source token can be wrapped or unwrapped and swapped through two liquidity pools. This means there can be up to one connector token in each main route part. - **Complexity level 2 (Default):** the source token can be wrapped or unwrapped and swapped through three liquidity pools. This means there can be up to two connector tokens in each main route part. - **Complexity level 3:** the source token can be wrapped or unwrapped and swapped through four liquidity pools. This means there can be up to three connector tokens in each main route part. ## How can I limit the liquidity sources in the query? To limit the liquidity sources to a query, you can manually enter the pool-contacts that you to use using the "protocols" parameter. All other liquidity sources will be excluded by default. ### Still have questions? Feel free to reach out to us in the live support chat! :::tip Debugging with `X-Request-Id` Every API response includes an `X-Request-Id` header that uniquely identifies your request. When contacting support, include this value for faster investigation. If you use the [1inch MCP Server](/portal/documentation/ai-integration/mcp-server), the `debug` tool can look up production logs by this request id. ::: https://business.1inch.com/portal/assets/docs-v2/faq/cors-error-faq.md When integrating the 1inch API in your dApp, you might experience an issue with CORS (Cross-Origin Resource Sharing). It can be confusing, especially when your API calls are successful via tools like [Curl](https://curl.se/) or [Postman](https://www.postman.com/), but fail when made from your custom dApp's UI. ## This article covers: [What is CORS?](#what-is-cors) [Why am I facing this issue?](#why-am-i-facing-this-issue) [How can I resolve CORS errors when calling the 1inch API?](#how-can-i-resolve-cors-errors-when-calling-the-1inch-api) [Can I call functions without a server and still avoid CORS errors?](#can-i-call-functions-without-a-server-and-still-avoid-cors-errors) ### What is CORS? Cross-Origin Resource Sharing (CORS) is a common security protocol implemented by most browsers to help prevent security risks with cross-origin requests. Essentially, a cross-origin request is when a web page from one domain tries to access resources, like data or images etc., from a different domain. For example, say you're browsing "[coindesk.com](https://www.coindesk.com/)" and it has a ETH/BTC ticker widget that pulls price data from "[tradingview.com](https://www.tradingview.com/)". When the "coindesk.com" domain tries to fetch the price data from the "tradingview.com" domain, it's making a cross-origin request. Browsers prevent HTTP requests to a different domain than the one hosting the web page, in order to protect users against cross-site security threats. This can be really helpful in blocking things like [request forgery](https://owasp.org/www-community/attacks/csrf), malicious [script inclusion](https://owasp.org/www-project-web-security-testing-guide/v41/4-Web_Application_Security_Testing/11-Client_Side_Testing/13-Testing_for_Cross_Site_Script_Inclusion). ### Why am I facing this issue? When the CORS protocol blocks your requests, it's a signal that the 1inch API hasn't granted explicit permissions to your domain. For 1inch integrations, this usually happens when trying to call the 1inch API from you're dApp's front-end. When sending a request through a frontend, the API key is embedded within the client-side code or headers, making it accessible to anyone who inspects the webpage's source or monitors its network traffic. This makes it so a malicious actor could easily extract and use your exposed key for their own benefit (or even to get around their own rate limits)! ### How can I resolve CORS errors when calling the 1inch API? The most common solution is to run your request through some type of middleware or proxy server. This additional component makes the call, receives the response, adds necessary headers, and then forwards it to your application. Then the browser will see the request as originating from the same domain, avoiding any CORS triggers. You can use your own custom proxy or a turnkey proxy server like [Lambda / AWS](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-integrations.html). Please note: Make sure that your API key and headers are entered correctly! ### Can I call functions without a server and still avoid CORS errors? Since non-server functions are still called on the front-end, your request will still likely be blocked. https://business.1inch.com/portal/assets/docs-v2/faq/how-to-read-tx-input-data.md In this article, we'll learn how to read and decode the raw calldata that's passed to Ethereum smart contracts. The calldata contains the function signature and arguments passed to the function. We can start with something simple like ```text 0xb7760c8f000000000000000000000000000000000000000000000000000000004d866d9200000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45; ``` To start, we need to extract the function signature from the first four bytes of the calldata. These four bytes represent the function selector, which uniquely identifies each function in the contract. We can use a web3 library to extract the function selector and match it to the corresponding function in the contract. If you don't have this, you can just use a tool like https://4byte.directory/ to look up the function selector. Once we have the function selector, we can use the function signature to determine the number and type of arguments that are passed to the function. For example, the function signature for the transfer function in the ERC20 standard is 0xa9059cbb, which corresponds to the function transfer(address to, uint256 value). The first argument is an address, which is 20 bytes long, and the second argument is a uint256, which is 32 bytes long. We can use this information to extract the arguments from the calldata. ```text // transfer(uint256 amount, address to) 0xb7760c8f // arguments 000000000000000000000000000000000000000000000000000000004d866d9200000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45 ``` Now that the function selector and arguments are separated, the arguments can be decoded. In their most basic form, every argument is padded to 32 bytes. This is because the calldata is stored in 32 byte slots. We can use this information to extract the arguments from the calldata. ```text // uint256 amount 000000000000000000000000000000000000000000000000000000004d866d92 // address to 00000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45 ``` This is a lot more readable now. The amount is `0x4d866d92` and the address is `0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45` ##Dynamic variables Transaction data containing information that is not a fixed size is much harder to decode. These types would be something like a string or bytes. To show how complicated strings can be, consider the following example of "hello world" encoded as a parameter. ```text 0x 0000000000000000000000000000000000000000000000000000000000000020 000000000000000000000000000000000000000000000000000000000000000c 48656c6c6f20576f726c64210000000000000000000000000000000000000000 ``` The first 32 bytes represent the offset to the string. The second 32 bytes represent the length of the string. The remaining bytes represent the string itself. The string is padded to 32 bytes, so the remaining bytes are 0s. This is a basic outline of decoding transaction data by hand. There are many more edge cases and complications that can arise, but this should give you a good idea of how to decode transaction data. If you want to learn more about reading transaction input data, we recommend reading [this article](https://degatchi.com/articles/reading-raw-evm-calldata/). https://business.1inch.com/portal/assets/docs-v2/faq/infrastructure-fee.md Infrastructure fees apply to Swap API transactions. These fees are deducted from the destination amount of each completed Network Request and are distinct from subscription fees. They help maintain and scale the infrastructure that powers the APIs--ensuring reliability, performance, and operational continuity. Following infrastructure fees apply by plan: | Plan | Fees for stable coins | Fees for non-stable coins | | :------- | --------------------: | ------------------------: | | Dev | 10 bps | 30 bps | | Startup | 7 bps | 20 bps | | Pro | 4 bps | 10 bps | | Business | 2 bps | 5 bps | All 1inch interfaces or channels (dApp, Wallet and others using the API) may incur infrastructure fees in the following ranges: 0-5 bps for stable pairs and 0-30 bps for non-stable pairs. ## Support https://business.1inch.com/portal/support/article/16886490 Email activation error - Users MUST register with SSO via Github or Google before they can generate a magic link. There is no method to only sign in via email or get a magic link before using SSO. https://business.1inch.com/portal/support/article/8422842 If you're encountering issues with our API, this guide will help you understand some common error messages and their meanings. General Errors 400 - Bad Request: This indicates an incorrect parameter. Ensure that all required parameters are correctly set. 401 - Unauthorized: Your API key is missing or invalid. Send it as Authorization: Bearer . Check that the full key was copied: keys are 32 characters, letters and numbers only, with no quotes, brackets, or spaces. If the error persists, generate a new key in your 1inch Business account and copy it again. 402 - Payment Required: Your organization has no available request credit, usually because the monthly quota is used up or an invoice is unpaid. Check Request usage on your dashboard, then top up or renew your plan. 403 - Forbidden ("Complete your KYC/KYB verification to gain access"): Your organization has not completed identity verification, which is required for API access. Complete it in your 1inch Business account; see Registration: KYC requirement . If your verification is already approved and you still get this error, contact support with your organization ID. 429 - Rate Limit: You've made too many requests to the API within a 1-second time period. Reduce your requests per second (RPS) to stay below the free 1 RPS limit . If higher limits are required, check out the current 1inch Business Plans . A 429 is also returned when your monthly call quota is used up. See 1inch API troubleshooting for the difference between the two limits. 503 - Banned IP: The IP address you're using to access the API is banned. Please ensure you're adhering to 1inch terms of use . Specific Errors Cannot sync token: The token either doesn't exist on the blockchain or isn't a valid token. Also, double check that you are using the correct chainID in relation to the token used. Amount is not set: The 'amount' field hasn't been specified. ***Please note: All token amounts must be in the correct decimal format! The decimal format can be found by looking up the token contract on the block explorer. Example: If the source token is DAI, and you are sending 1 DAI, the amount with 18 decimals would be 1000000000000000000. https://etherscan.io/token/0x6b175474e89094c44da98b954eedeac495271d0f Src is not set: The 'src' (source token contract) field is missing. Dst is not set: The 'dst' (destination token contract) field is missing. Insufficient liquidity: The aggregator can't find a route for the swap due to low liquidity. Swap-specific Errors 400 - Bad Request : This will be similar to the general 400 error; however, these error messages will also specify a missing parameter or more details of the problem. Not enough src balance: This means your source token balance is insufficient. It provides the required amount and your current balance. Not enough Allowance: You haven't granted the necessary token allowance to the 1inch router. It'll indicate the amount, current allowance, and the spender address, e.g., "Spender: 0x1111111254eeb25477b68fb85ed929f73a960582". Insufficient liquidity: As mentioned earlier, the aggregator couldn't route the swap. *** Please note: This list is not comprehensive. Other errors involving the dynamics of generic EVM transaction estimations might require deeper investigation. Questions, comments, concerns? Feel free to reach out to our support team by clicking the live chat below. https://business.1inch.com/portal/support/article/13212447 What is 1inch's role in RWA integration? 1inch acts as a non-custodial aggregation and execution layer, enabling access to RWA tokens via the Swap API. 1inch does not issue, custody, or manage the underlying real-world assets. Which RWA tokens are available via Intent Swap (Fusion) API? Only whitelisted, issuer-approved RWA tokens that meet 1inch's technical, liquidity, and compliance integration criteria are exposed. Is Robinhood Chain supported for RWA swaps? Yes. Robinhood Chain (an Arbitrum-based L2, chainId 4663) is supported for RWA / tokenized-stock swaps through 1inch -- roughly 100 stock tokens plus USDG and WETH, routed through on-chain liquidity (Uniswap v2/v3/v4 and Rialto). ETH is the native gas asset. What token standards are supported? Currently, ERC-20 tokens are supported for RWA swaps via the 1inch Intent Swap API. Does 1inch perform KYC or AML checks on users before they purchase RWA? No. 1inch does not perform KYC or AML. Compliance requirements are enforced by the RWA issuer at the token or contract level. What is the integrator responsible for? Integrators are responsible for: User eligibility checks Regulatory disclosures UI and UX warnings Jurisdictional compliance Are RWA considered securities? This depends on jurisdiction and issuer structure. 1inch does not provide legal classification. What are RWA tokens and what are the main risks? RWA tokens are blockchain tokens issued by third parties that track the price of a traditional asset such as a stock, ETF or commodity. Holding the token is not the same as owning the underlying asset: it typically carries no shareholder, voting or dividend rights, and your rights depend entirely on the issuer's terms. Key risks: issuer risk -- the token's value depends on the issuer maintaining backing and honoring redemptions; price divergence -- onchain prices can deviate from the official market price, especially outside traditional market hours; regulatory restrictions -- these tokens may be treated as securities in some jurisdictions and are not available for trading in the United States, the United Kingdom and other restricted jurisdictions; issuers may enforce allowlists, verification or transfer restrictions at the contract level; liquidity risk -- onchain liquidity can be thin and swaps may execute at unfavorable rates. 1inch does not issue, custody or back these tokens and does not verify issuer claims. https://business.1inch.com/portal/support/article/8186441 Currently, 1inch Business only supports English; however, keep an eye out for more languages to come! https://business.1inch.com/portal/support/article/16770984 The 1inch Business Portal is where you create API keys, manage your subscription, and monitor your API usage. Creating an account Registration is self-serve: sign up with your email at business.1inch.com . No approval is needed. New accounts start on the free Dev plan; see How to increase your 1inch API rate limits for the paid plans. Logging in Login uses a magic link sent to your email. If the email does not arrive within a few minutes, check your spam folder. If it still does not arrive, contact support. Using the APIs You can create your API key from the dashboard. To call the APIs, your organization also needs KYC/KYB verification; without it, requests return a 403. See Registration: KYC requirement . Joining your company's existing organization The organization's Owner invites you from Settings -> Users. A user can belong to only one organization, so delete your own account first if you already created one. The number of users depends on the plan; see Understanding and managing user roles in 1inch Business . 1inch does not create accounts for you: registration is self-serve, and access to an existing organization comes from that organization's Owner. https://business.1inch.com/portal/support/article/9821013 At 1inch, we're constantly working to improve platform security and build a compliant reputation within the overarching web3 ecosystem. In line with this commitment, when creating a brand new 1inch Business account, all users must pass a standard Know Your Customer (KYC) verification process. This is a prerequisite for accessing and building with all 1inch Business APIs. It involves a simple and quick process that consists of both liveness and ID verification. This step is in accordance with Section 8 of 1inch Business Terms of Service . Completing the required verification steps usually takes only a few minutes. However, the assessment itself may take up to two business days to be finalized. If it has been longer than two days, please reach out to 1inch Customer Support . FAQ How can I return to my verification session before the assessment is finalized? If you had to interrupt your verification session, you can resume it by logging in to 1inch Business, going to the Dashboard , and clicking Proceed to Verification on the banner at the top of the page. How do I know the status of my verification? Throughout the process, status updates will be sent to the email address you provided when creating your 1inch Business account. If additional documents or information are required, and email will be sent to notify you. Follow the instructions provided and submit the requested materials so that the verification can continue. What should I do if there is an issue with my verification process? 1inch Business verification processes are conducted through our trusted third-party provider, Sumsub. While most users complete these checks smoothly, there may be instances where: Documents are not accepted due to inconsistencies. Live photo or liveness checks encounter errors. Issues arise with document clarity or completeness. Additional documents or information are required. If you experience any challenges during the verification process, reach out directly to Sumsub's support team for prompt assistance. Which documents can I use for verification? The identity documents accepted for verification vary by country. Before starting the process, you can check Sumsub's supported documents and countries to see which document types are supported for your country. Please note that document support does not necessarily mean that registration with 1inch Business is available in every jurisdiction. Access to 1inch Business is subject to jurisdictional and sanctions-related restrictions. Users who qualify as Restricted Persons , including those connected to a Prohibited Locality as defined in the 1inch Business Terms of Service , cannot access or use the service. Are you onboarding as a Resolver? Resolver onboarding has its own dedicated documentation. For the complete onboarding process, including registering as a Resolver, providing contract information, completing KYC/KYB and the compliance survey, and receiving Access NFTs, see the Resolver Onboarding Setup Guide. I passed KYC, but I'm receiving a 403 error. What should I do? In some cases, your KYC verification may have been successfully completed, but requests to 1inch Business APIs may still return an HTTP 403 status code with the following error message: complete your KYC/KYB If your KYC has already been approved and you continue to receive this error, please contact 1inch Customer Support so the team can investigate the issue. https://business.1inch.com/portal/support/article/11581772 Keeping your billing address up to date ensures smooth invoicing and compliance with financial regulations. You can add or update your billing address at any time directly through your 1inch Business account. The updated billing information takes effect immediately and does not require additional approval. Steps to add or update your billing address Log in to your 1inch Business account. In the top-right corner, open the account menu, then go to Profile > Billing information . Select your entity type: Corporate Individual Fill in the fields shown. Click Save to apply the changes. Note that all fields must be completed. The only exception is the Organization URL field, which is optional for corporate entities . https://business.1inch.com/portal/support/article/9549117 Different user roles define the actions users can perform within 1inch Business. This guide will help you understand the various roles and the permissions they come with. User roles We have four main user roles: Owner , Admin , Developer , and Finance . Each role has specific permissions to help you perform your tasks efficiently. Here's a quick overview of what each role can do: Owner: full access to everything (applications, organizations, billing, payments, subscriptions), only one Owner per organization Admin: can manage applications and organizations and view subscriptions Developer: can manage applications and view organizations, payments, and subscriptions Finance: can manage billing and payments and view applications, organizations, and subscriptions By assigning the appropriate roles, you can ensure each user has the correct level of access to perform their tasks efficiently. Remember, there is only one Owner per account who holds the legal responsibility for the organization. Owner Note: There is only one Owner per organization, and the Owner is the legal entity responsible for the organization. The Owner has the most comprehensive access, allowing full control over all aspects of the portal. What can owners do? Modify applications and their keys View, modify, and delete organizations Update billing information Process payments Update subscriptions Admin Admins have broad access but with some limitations compared to Owners. What can admins do? View and modify applications and their keys View and modify organization details Check subscription details Developer Developers focus on managing applications and keys, with some read-only access to other areas. What can developers do? View and modify applications and their keys Check organization details Check payment details Check subscription details Finance Finance users handle billing and payments, with some additional read-only access. What can finance do? Update billing information Process payments Check application details Check organization details Check subscription details Number of users per account The maximum number of users per account tier is as follows: Dev -- 1 user Start-up -- 2 users Professional -- 3 users Business -- 5 users Enterprise -- 20 users How to add a new user to your account Important note: To add a user to an organization, they must first be removed from any existing organization. For example, the user must delete their current account before being assigned to a new organization. Visit https://business.1inch.com/portal/settings/users Within User management , click Invite . In the window opened, enter the new user's email and select one of the roles. Currently, the roles of Admin , Developer , or Finance can be selected. Click Invite user to send them the invitation to join. Questions, Comments, Concerns? Feel free to reach out to us in the live support chat! https://business.1inch.com/portal/support/article/8186443 To change 1inch Business theme: Open the account menu in the upper-right corner of any page. In the menu opened, select from light, automatic, or dark theme. Questions, comments, concerns? Feel free to reach out in the live support chat! https://business.1inch.com/portal/support/article/8186455 If you need to regenerate an API key for any reason (whether it be for security reasons or staff changes, etc.), it is quite easy! To get started, navigate to click Applications at the top of the page. Next, click the application key for which you want to regenerate, and click Regenerate on the following screen. Lastly, click Regenerate again to confirm the action on the following screen. Once a key is regenerated, the old one will be permanently disabled. After confirming, your old API key will be replaced with a new one that can be used immediately. Questions, comments, concerns? Feel free to reach out in the live support chat! https://business.1inch.com/portal/support/article/8186430 To delete your 1inch Business account: In the top-right corner of any page, open the account menu and select Profile . Navigate to Delete tab. Select Delete account . Read the disclaimer and confirm the account deletion on the following screen. Deleting your account will permanently remove all related data (API keys, settings, and history). Any applications using this API key will no longer be able to access 1inch APIs. Questions, comments, concerns? Feel free to reach out to us in the live support chat! https://business.1inch.com/portal/support/article/8186424 To change or modify your 1inch Business profile settings, open the account menu in the upper-right corner of the page and select Profile . From here, you will be able to change all of the details within your profile. Once you are finished updating the information, click Save to apply the changes. Questions, comments, concerns? Feel free to reach out in our Live Support Chat! https://business.1inch.com/portal/support/article/12132359 What's new 1inch has upgraded its Intent Swap API to support tokenized real-world assets (RWAs). With this update, RWAs are now accessible via the 1inch dApp and Wallet (outside of the US and other restricted regions). RWAs are also available to partners through the Intent Swap API on 1inch Business. One of the first partners to expand its integration is Trust Wallet , bringing RWA access to its community. RWA swaps are also available on Robinhood Chain (an Arbitrum-based Layer 2, chain ID 4663) -- a network focused on tokenized stocks -- with liquidity aggregated across Uniswap v2/v3/v4 and Rialto. Why it matters Liquidity has been one of the biggest hurdles for RWA adoption. The upgraded Intent Swap API addresses this by leveraging 1inch's leading aggregation technology, making RWA swaps as easy and efficient as crypto swaps. This milestone was achieved in partnership with Ondo Finance , a leader in tokenized real-world assets. Ondo recently announced the launch of Ondo Global Markets , which provides non-US investors seamless access to over 100 tokenized assets on Ethereum, with plans to expand to more than 1,000 by the end of the year. How it works Resolvers : 1inch routes trades through resolvers to execute orders quickly and securely. Aggregation : users get access to tokenized assets with efficient pricing and liquidity. Ecosystem access : all Intent Swap API integrators can now enable RWAs for their eligible users. Things to know Normally, Ondo GM tokens are tradable 24/5 in line with U.S. market hours (Sunday 8 pm ET-Friday 8 pm ET) The only supported pairs are USDC/USDon <> Ondo_RWA RWA access is jurisdiction-restricted . Not available for users in the United States, the United Kingdom, and other restricted regions . Token availability depends on issuer classification, local regulations, and risk filters. https://business.1inch.com/portal/support/article/8186363 If you need custom API limits, a tailored number of users, personalized pricing, or dedicated account support, request an Enterprise plan through the Get Enterprise option. Once your information has been submitted, we will review the application in the order that it was received. https://business.1inch.com/portal/support/article/8703748 Pricing and upgrades for 1inch Business subscription plans When you create an account yourself in 1inch Business, it's automatically assigned the Dev plan , which is free of charge and comes with limits of 1 Request Per Second (RPS) and a total of 100,000 monthly calls . When you reach a limit, requests are blocked with a 429 response. There are no overage charges. See 1inch API troubleshooting for how the limits work and the Pricing and subscriptions FAQ for billing details. Higher rate limits are available at the following predefined paid plans: Start-Up: 10 RPS within 1,000,000 monthly calls Professional: 20 RPS within 3,000,000 monthly calls Business: 40 RPS within 7,000,000 monthly calls The plans include access to all 1inch APIs: S wap API : Classic Swap , Intent Swap , Cross-Chain Swap Orderbook API . History API Traces API Portfolio API Balance API Gas Price API Spot Price API Token API Transaction Gateway API Web3 RPC API Charts API Domains API Token Details API If the predefined plans don't meet your needs in terms of request limits per second or month--or if you'd like more flexibility in accessing additional APIs--you can create a Custom plan tailored to your requirements . With a Custom plan, you can choose which APIs to include, define your preferred rate limits, select your payment cycle, and adjust other parameters to fit your specific use case. For details, see How to customize your subscription plan . Projects that reach a swap volume of US$10 million through 1inch Business will automatically be upgraded to a Custom plan, requiring communication with the 1inch representative. This plan typically includes a revenue share agreement based on the swap volume. The same condition is extended to entities requiring increased requests per second (RPS) for all other APIs (excluding the 1inch swap APIs). If neither the predefined nor custom plans fully meet your requirements--particularly if you expect high traffic volumes and need significantly higher rate limits-- submit a request for an Enterprise plan . The 1inch Team will be happy to help design a solution tailored to your specific needs. For a successful upgrade to the Enterprise, the following steps need to be fulfilled: Confirmation of the package and billing cycle. KYC/KYB verification, depending on your entity type. Filling in the billing information and accepting the terms of use. Payment of the total due using USDC cryptocurrency on the Ethereum network. Questions, comments, concerns? Feel free to reach out to us in the Live Support Chat! https://business.1inch.com/portal/support/article/10214433 The API top-up feature allows you to independently increase your monthly API call quota without the need to upgrade your subscription. This self-service capability allows you to efficiently respond to increased demand, ensuring operational continuity and flexibility. The top-up feature is especially useful during periods of unexpected demand or for handling special projects that require additional API capacity. Top-up details You can top up your current subscription plan with increments of 100,000 additional API calls. Additional calls are strictly available for the current billing cycle and provide a temporary enhancement to meet your immediate requirements. Note that any unused calls from the top-up do not carry over into the next month. The top-up feature also provides flexibility in specifying the type of 1inch API that needs additional capacity. You can allocate the top-up to specific APIs based on your project requirements, ensuring that resources are directed where they are most needed. The API top-up feature exclusively increases the number of API calls available. The rate of requests per second (RPS) remains unchanged and continues to align with the limits defined by your current subscription plan. If an increase in RPS is required, consider upgrading to a higher-tier plan. Steps to top up your account with additional requests Step 1: Fill in your basket Navigate to the main dashboard of your 1inch Business account. Under Requests usage, click Top up . From the drop-down, select the product to which you want to allocate additional requests and specify the amount. If you need several products to be topped up, click Add product and repeat the step for each product. Click Confirm to proceed. Step 2: Complete KYC or KYB verification (optional) At this stage, KYC/KYB completion may be required. This is a one-time action, and you won't have to verify your identity with each subsequent top-up. The verification is conducted by the third-party service, Sumsub. To learn more about the process, see Registration / KYC requirement . To start the process, click Proceed to Verification . Follow the on-screen instructions to complete your verification. Step 3: Confirm billing details If you previously entered your billing information, it will be auto-filled. However, the fields are still editable. If you edit your billing details in this step, they will be automatically updated throughout 1inch Business after you proceed to the next step. If some or all of your billing information is missing, add it manually. Review your billing details and ensure you have filled in all mandatory fields. Accept the terms of usage and click Confirm to proceed. Step 4: Make a payment Confirm the payment and proceed using your wallet. If you haven't connected a wallet before, you will be prompted to connect one at this stage. Follow the on-screen instructions to connect your crypto wallet and proceed with the payment. Once your wallet is connected, select the currency and network for your payment. Review the payment details and click Send to finalize the payment. https://business.1inch.com/portal/support/article/16119187 Integrator (partner) fees: why your referrer fee may not be paid What are integrator fees? If you build on the 1inch APIs, you can charge your users a fee on each swap: the fee + referrer parameters on the Classic Swap API, or the integrator-fee parameters on Intent Swap (Fusion) and Cross-Chain (Fusion+) quotes. Fees are paid on-chain, per swap, in the swap token, on the same chain the swap ran on -- there is no dashboard, claiming step, or consolidation across chains. Integrator fees require a paid plan Partner/integrator fee collection is not included in the free Dev plan. It is available for organizations on a paid 1inch Business plan and is enabled per organization -- if you are on a paid plan and fees still aren't applied, contact support to have the feature checked/enabled for your organization. How to tell this is what's happening Classic Swap: the request succeeds and the swap executes normally, but fee and referrer are silently ignored -- no error is returned and the referrer address receives nothing (it won't appear in the transaction's token transfers). Intent Swap (Fusion) / Cross-Chain Swap (Fusion+): the quote request is rejected with a 400 error: FEE_NOT_ALLOWED , INTEGRATOR_FEE_DISABLED , or CUSTOMER_FEE_DISABLED -- all three indicate the same missing fee entitlement. If your referrer address used to receive fees and stopped, check your organization's plan first -- this is the most common cause, not a bug in your request. How to enable integrator fees Upgrade to a paid plan in the 1inch Business Portal or reach out via live chat with your organization ID -- the team can confirm whether fee collection is enabled for your account. Note this is separate from the infrastructure fee described in Do I pay fees when using 1inch? and from the rate-limit tiers in How to increase your 1inch API rate limits . https://business.1inch.com/portal/support/article/12867971 Before you start, make sure you are logged in to your 1inch Business account and have the Owner role . Upgrading your subscription Navigate to the 1inch Business pricing page . In the comparison table, select Get under the plan you want to upgrade to. In the next screen, review your purchase and check the details for the selected subscription. Any unused calls from your current billing period are automatically applied as a discount toward the new subscription when upgrading from a paid plan. Once ready, select Confirm . Add or review your billing address details and accept the Commercial API Terms of Use. Once ready, select Subscribe . Connect your wallet and make a payment for your new subscription. Once the payment is finalized, your new subscription is ready to be used. If you are on an active custom plan, for upgrades or downgrades, please contact support. Downgrading your subscription Navigate to the 1inch Business pricing page . In the comparison table, select Downgrade to under the plan of your choice. In the next screen, review your purchase and check the details for the selected subscription. Your current subscription stays active until the end of your current billing period (monthly or yearly). No refunds are issued for the remaining time. Once ready, select Confirm . In the pop-up, review the warning and select Downgrade subscription . Add or review your billing address details and accept the Commercial API Terms of Use. Once ready, select Subscribe . A downgrade takes effect at the start of your next billing cycle, and a new invoice will be sent to you 4 days prior to that. If the request is submitted less than 4 days before renewal, the invoice for the upcoming cycle is generated immediately. For the downgraded plan to become active, the invoice for the new plan must be created and paid. Otherwise, the account will be automatically moved to the Dev (Free) Plan. Cancelling an active, downgrading, or upgrading subscription If you have changed your mind, you can cancel: Active subscription Upgrade request Downgrade request To cancel: In the upper-right corner of the page, open the account menu and select Subscription . In the list, select the subscription you want to cancel (upgrade and downgrade requests are in Pending status). Under the subscription summary, select Cancel subscription . In the pop-up, review the warning and select Cancel subscription . When cancelling an active subscription, it remains active until the end of the current billing period. No refunds are issued for the remaining time of the active period. https://business.1inch.com/portal/support/article/12398075 How are API plans priced for usage spikes and overages? API plans are not billed dynamically for short usage spikes. Each plan has fixed request limits (RPS and RPM). If you exceed those limits, requests are blocked with a 429 response rather than incurring overage charges. To handle higher or spiky workloads, you can move to a higher tier, customize your plan , or negotiate enterprise terms . Can I have separate rate limits per product or API keys? Standard plans apply limits broadly. However, you can use the Custom plan configurator to set the RPS and call limits for each type of API. API keys share the RPS and call limits across the account. Do paid tiers include historical data retention (API calls and statistics)? Yes. All paid plans of 1inch API include at least 30 days of data retention, while higher tiers may extend this to 90 days, depending on the package. Does 1inch provide extended data retention for Enterprise plan? Yes. Enterprise plans provide extended statistics retention for up to 180 days. This allows deeper insights into historical API usage and performance monitoring. You can explore this feature in the statistics section of the portal. Is SSO (Single Sign-On) available in 1inch API plans? Yes, with the Enterprise plan, SSO can be negotiated and configured based on the SAML provider used. How are quotas and billing managed across teams and projects? Billing and quotas are tied to your subscription plan in 1inch Business. Each plan defines limits, such as Requests Per Second (RPS) and Requests Per Month (RPM), which are reset monthly. You can choose between monthly or annual billing intervals or even negotiate it with your sales associate, depending on your needs. If you occasionally exceed your quota, you can purchase top-ups for extra requests beyond your plan's limits. Where can I find the security audits done by 1inch? Security is a top priority at 1inch. Every major protocol upgrade and smart contract deployment undergoes independent audits by established blockchain security firms. All completed audit reports are published on the official 1inch GitHub repository for audits . Does the enterprise plan include dedicated support? Every enterprise customer is assigned a dedicated relationship manager who guides them through the entire enterprise lifecycle. The onboarding and support process is fully tailored to a customer's specific needs, ensuring personalized assistance and long-term partnership. https://business.1inch.com/portal/support/article/11703585 Introduction The 1inch custom plan configurator is a flexible tool designed for users who want to build a subscription plan tailored to their specific business needs and use cases. It provides the freedom to select the APIs you require, set custom rate limits for Requests Per Second (RPS) and Requests Per Month (RPM), and choose your preferred payment cycle. The configurator is available to anyone with an account in 1inch Business and completed KYC/KYB verification . If this verification hasn't been completed yet, users can easily start the process directly from the configurator page without interrupting their session. With the custom plan, you can: Choose which APIs to include in your plan. Define custom RPS and RPM limits for each API. Select your payment cycle. Add additional users. Manage API keys. Add applications that will use the selected APIs. How to create a custom plan While logged in to your 1inch Business account, go to the custom plan configurator . Click + next to the APIs you want to customize for your plan. For each added API, select the desired RPM and RPS limits. From Pre-payment period , select how often you would like to pay for the plan. Review your choice and click Continue to proceed. In the next screen, customize the number of Users , Applications , and API keys if necessary. Click Continue to proceed. If your KYC/KYB verification is not finished yet, you'll have to complete it before moving on to the payment. Review your billing information, agree to the terms of use, and click Continue . Review your purchase and click Confirm and Pay to finalize the payment through your wallet of choice. If you haven't connected a wallet yet, click Connect wallet and follow the on-screen instructions. After your payment is successfully processed, your custom plan will become active. The start date of your new billing cycle will be set based on the date your invoice was paid. If you had any unused limits remaining from your previous paid plan, the value of those unused limits will be deducted from the first payment for your custom plan. https://business.1inch.com/portal/support/article/9037727 If you've completed a new plan purchase or your renewal is due, there are two ways to get the invoice: For every payment that is due, we dispatch the corresponding invoice directly to your billing email address. This document is provided as a PDF attachment for your convenience. In your 1inch Business account, navigate to Subscription > Invoices and download the invoice you require in PDF format. Questions, comments, concerns? Feel free to reach out in the live support chat! https://business.1inch.com/portal/support/article/9389950 To ensure seamless management of your subscription, it's important to use the designated 1inch Business payment system when settling your bill. Steps to complete an invoice payment 1. Log in to your 1inch Business account. 2. On the payment awaiting banner, click Proceed with payment . 3. Connect your wallet via 1inch Business wallet connect plugin and follow the on-screen instructions to complete the payment. 4. Review the amount, then click Confirm and Pay . Once the transaction succeeds on-chain, your payment will be complete. Any payments made outside of 1inch Business payment plugin will not be automatically recognized and associated with your account. This might lead to delays in the activation or renewal of your plan, so it's highly recommended that you process your payments through 1inch Business only. Payments FAQ When is the invoice for subscription renewal issued? The invoice is issued 4 days before the start of your new subscription cycle. Will paying the renewal invoice earlier change the renewal date of my subscription cycle? No, paying the invoice earlier does not alter the renewal date of your subscription cycle. When will all plan features activate for my renewed subscription? All plan features will activate on the scheduled start date of the cycle. Unless the payment has been made outside 1inch Business payment plugin. If you encounter any issues or have questions regarding the payment process, contact our Support Team for assistance through the live support chat. We're here to help ensure that your experience is smooth and hassle-free. https://business.1inch.com/portal/support/article/12351066 What is Ethereum and why build on Ethereum Mainnet? Ethereum Mainnet is the primary public Ethereum production blockchain where ETH and tokens carry real economic value, where real assets are traded, and smart contracts run with full decentralization and security. Building on Mainnet gives access to the largest liquidity, most users, and a mature ecosystem--which is critical for many DeFi use cases. What is the Ethereum JSON-RPC, and which methods do dApps use most? The Ethereum JSON-RPC is a standard protocol that lets dApps and wallets interact with Ethereum nodes to read blockchain state (balances, blocks, logs) or send transactions. Common methods include eth_call , eth_getBalance , eth_getTransactionReceipt , eth_estimateGas , eth_getLogs , and block-query methods (e.g., eth_blockNumber , eth_getBlockByNumber ). Do 1inch endpoints on Ethereum support WebSocket RPC? 1inch offers Web3 RPC API endpoints over HTTP for major EVM chains, as well as WebSocket support for all existing Web3 RPC methods. Is the archive state (including eth_getLogs) available on Ethereum via 1inch? Yes. The 1inch Web3 RPC API offers access to historical data (logs, past blocks, events), which lets you query older state and past logs via methods like eth_getLogs . How do I estimate gas on Ethereum via API? You can use the 1inch Gas Price API, which provides up-to-date gas price data for various blockchains, including Ethereum. How to avoid RPC rate limits on high-throughput Ethereum apps? To avoid rate limits, you can: Use 1inch Business plan configurator to configure and customize the RPS and Web3 calls based on your needs, rather than public or shared endpoints. Use WebSockets. https://business.1inch.com/portal/support/article/12380515 What are 1inch APIs and how can I use them? 1inch APIs give you plug-and-play access to DeFi--swap tokens, fetch prices, track balances, and connect to top chains like Ethereum, Solana, BNB Chain, Unichain, Base, Sonic, Robinhood Chain, Arc, and more. For Arc requests, use chain ID 5042 with the same API endpoints and credentials as other supported EVM networks. Why choose a multi-chain API over single-chain tools? A multi-chain API provides a unified interface to multiple blockchain networks, reducing complexity and accelerating development. Instead of integrating separate tools and maintaining distinct codebases for Ethereum, or other chains, developers can use one API layer for consistent access across ecosystems. This not only streamlines cross-chain applications but also future-proofs products as user demand shifts between networks. What does an API SLA typically cover in Web3? An API Service Level Agreement (SLA) generally defines performance and reliability commitments. In Web3, this usually covers availability (e.g., 99.9% uptime guarantees), latency (maximum response times for queries), throughput (assured request volumes per second), and support and escalation procedures (response times if issues occur). How do I get an API key on 1inch Business? Sign up or log in to 1inch Business, complete a KYC/KYB procedure, create a project. Your API key will be available under the application details page, in the project details. https://business.1inch.com/portal/support/article/12380488 How are API plans priced for usage spikes and overages? API plans are not billed dynamically for short usage spikes. Each plan has fixed request limits per second and per minute (RPS and RPM). If you exceed those limits, requests are blocked with a 429 response rather than incurring overage charges. To handle higher or spiky workloads, you can upgrade, customize your plan, or negotiate enterprise terms. Where can I check API status and uptime? For uptime or incident updates, contact 1inch Support. Can I have separate rate limits per product or API keys? Standard plans apply limits broadly. But you can use the custom plan configurator and set the RPS and calls limit at the API Product level. The API keys share the RPS and call limits across the account. Do paid tiers include historical data retention (API calls and statistics)? Yes, all 1inch Business paid plans include a minimum data retention of 30 days, with some tiers offering up to 90 days of retention depending on the package selected. What happens when I upgrade to a higher plan? The upgrade takes effect immediately after you pay the invoice. Any unused calls or credits from your current billing period are automatically applied as a discount toward the new plan, and you only pay the difference. What happens when I downgrade my plan? Downgrades take effect at the start of your next billing cycle. No refunds or unused quota transfers are provided when moving to a lower plan. I submitted a downgrade right before renewal. What happens? The downgrade will still take effect at the end of your current billing cycle. However, if you submit it less than 4 days before renewal, the invoice for the next cycle is generated immediately. What if my renewal payment has already been processed? If the renewal has already gone through, your downgrade will apply after one more billing cycle: a month or a year, depending on your subscription. How do I activate my downgraded plan? To activate your downgraded plan, you must create and pay the invoice for it. If payment isn't completed, your account will be switched to the Dev (Free) Plan. I changed my mind--can I undo the downgrade? Sure! You can cancel your downgrade request anytime before the next billing cycle begins. What happens when I cancel my subscription? Your subscription stays active until the end of your current billing period (monthly or yearly). No refunds are issued for the remaining time--you'll keep access until the period ends. ## Payment methods https://business.1inch.com/portal/assets/docs-v2/payment-methods/overview.md ## Payment methods for 1inch APIs There are two ways to pay for access to 1inch APIs. Pick the one that fits your usage, or combine them. ## Supported payment methods - **[Subscription plans](/portal/documentation/payment-methods/subscriptions)**: a monthly quota of API calls for a fixed price, authenticated with an **API key**. Best when your usage is predictable or sustained. Plans range from a Free tier to Business, plus negotiated Enterprise and configurable Custom plans; paid in **USDC on Ethereum**. - **[x402 pay-per-call](/portal/documentation/payment-methods/x402)**: pay per request in **USDC on Base** with no subscription and no API key, using the open **x402** standard. Best for occasional, variable, or agent-driven usage, and as overflow when a plan's quota is exhausted. ## Which one should I use? | | **[Subscription](/portal/documentation/payment-methods/subscriptions)** | **[x402 pay-per-call](/portal/documentation/payment-methods/x402)** | | :------------- | :---------------------------------------------------------------------- | :------------------------------------------------------------------ | | **Billing** | Fixed monthly price, prepaid quota | Pay per request in USDC on Base | | **Auth** | API key (`Authorization: Bearer`) | Signed on-chain payment, no API key | | **Best for** | Predictable, sustained usage | Occasional, variable, or agentic usage | | **Commitment** | Monthly (1 to 12 month intervals) | None | | **Payment** | USDC on Ethereum | USDC on Base | :::info The two methods work together: keep a subscription for your baseline traffic and let **x402** cover overflow when you exceed your plan quota. See [Managing your subscription](/portal/documentation/payment-methods/subscriptions/managing-your-subscription#top-ups). ::: ## Next steps - [Subscription plans](/portal/documentation/payment-methods/subscriptions): compare tiers, purchase a plan, and make your first authenticated request. - [x402 pay-per-call](/portal/documentation/payment-methods/x402): link and fund a wallet, then pay per request in USDC on Base. https://business.1inch.com/portal/assets/docs-v2/payment-methods/subscriptions/subscriptions.md ## Subscription plans for 1inch APIs A **subscription** gives you a monthly quota of 1inch API calls for a fixed price, authenticated with an **API key**. It is the standard way to use 1inch APIs when your usage is predictable or sustained. For occasional, variable, or agent-driven usage, see [x402 pay-per-call](/portal/documentation/payment-methods/x402), and you can combine the two. One API key and one subscription cover every product (Aqua is enabled per plan). See [Authentication](/portal/documentation/apis/authentication) for how to send the key. ## Plans Public, self-service plans and their core limits: | Plan | Applications | API keys / app | Team members | API calls / month | Web3 RPC calls / month | Rate limit | | :----------- | :----------- | :------------- | :----------- | :---------------- | :--------------------- | :---------- | | **Free** | 1 | 1 | 1 | 100,000 | 25,000 | 60 / minute | | **Startup** | 2 | 2 | 2 | 1,000,000 | 50,000 | 10 / second | | **Pro** | 5 | 2 | 3 | 3,000,000 | 100,000 | 20 / second | | **Business** | 10 | 5 | 5 | 7,000,000 | 250,000 | 40 / second | **Enterprise** and **Custom** plans are also available. Enterprise is negotiated (custom limits, SLAs, and support), and a **Custom** plan can be configured to your needs. :::info Prices depend on the billing interval you choose (1, 3, 6, or 12 months). Longer commitments lower the effective monthly rate. The [pricing page](https://business.1inch.com/pricing) shows the exact price and the full feature comparison for each plan and interval. ::: ## Subscription vs x402 | | **Subscription** | **x402 pay-per-call** | | :------------- | :--------------------------------- | :------------------------------------- | | **Billing** | Fixed monthly price, prepaid quota | Pay per request in USDC on Base | | **Auth** | API key (`Authorization: Bearer`) | Signed on-chain payment, no API key | | **Best for** | Predictable, sustained usage | Occasional, variable, or agentic usage | | **Commitment** | Monthly (1 to 12 month intervals) | None | You are not locked in: when a plan's quota runs out you can [top up](/portal/documentation/payment-methods/subscriptions/managing-your-subscription#top-ups) extra calls, or use [x402](/portal/documentation/payment-methods/x402) for overflow. ## Next steps - [Getting started](/portal/documentation/payment-methods/subscriptions/getting-started): choose a plan, pay, and make your first authenticated request. - [Managing your subscription](/portal/documentation/payment-methods/subscriptions/managing-your-subscription): upgrade, downgrade, renew, cancel, and top up. - [Monitoring usage](/portal/documentation/payment-methods/subscriptions/monitoring-usage): track calls against your plan quota. https://business.1inch.com/portal/assets/docs-v2/payment-methods/subscriptions/getting-started.md ## Getting started with a subscription ## 1. Create your account Sign in or register at the [1inch Business Portal](/portal/login). During onboarding you create your first application. Applications organize your API keys, usage, and logs. ## 2. Complete verification (KYC / KYB) Verification is required to purchase or change a paid plan and to manage API keys (create, rotate, or delete). Complete **KYC** (individual) or **KYB** (company) for your organization from the portal when prompted. ## 3. Choose and pay for a plan - Compare plans on the [pricing page](https://business.1inch.com/pricing) and pick the one that fits your usage. - The plan CTA opens checkout in the portal. Select your **billing interval** (1, 3, 6, or 12 months; longer intervals lower the monthly rate). - Pay in **USDC on Ethereum** with a connected wallet (WalletConnect). Your subscription activates once the payment is confirmed. :::tip The **Free** plan needs no payment. You can start calling APIs within the free quota as soon as you have created your account and application. ::: ## 4. Get your API key Your API key is available under [Applications](/portal/applications). A key is created automatically with your first application. Keys are **bearer tokens**. Keep them out of client-side code and public repositories. Add more keys or applications up to your plan limits. ## 5. Make an authenticated request Send the key as an `Authorization: Bearer` header (or an `apiKey` query parameter, see [Authentication](/portal/documentation/apis/authentication)): ```bash curl -X GET \ "https://api.1inch.com/swap/v6.0/1/quote?amount=1000000000000000000&src=0xEeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&dst=0xdAC17F958D2ee523a2206206994597C13D831ec7" \ -H "Authorization: Bearer YOUR_API_KEY" ``` You receive `200 OK` with the API response. Each call counts against your plan's monthly quota. :::info The same key works across every product and both REST and WebSocket endpoints. For user-consent flows and server-to-server access without shared secrets, the platform also supports [OAuth 2.1](/portal/documentation/apis/authentication). ::: ## Next steps - [Managing your subscription](/portal/documentation/payment-methods/subscriptions/managing-your-subscription): upgrade, downgrade, renew, cancel, and top up. - [Monitoring usage](/portal/documentation/payment-methods/subscriptions/monitoring-usage): track calls against your plan quota. https://business.1inch.com/portal/assets/docs-v2/payment-methods/subscriptions/managing-your-subscription.md ## Managing your subscription Manage your plan from the portal under your organization's subscription settings. ## Billing cycle & renewal - Plans are billed per **interval**: 1, 3, 6, or 12 months, chosen at checkout. The total charge is the monthly rate multiplied by the number of months. - Paid plans **auto-renew** at the end of each interval. You are reminded before the invoice is due, and there is a **7-day grace period** to complete payment. - Renewals are paid in **USDC on Ethereum**, the same way as the initial purchase. ## Upgrade Upgrade at any time from the portal. When you upgrade mid-cycle, the unused portion of your current plan (remaining time and unused request quota) is **credited** against the new plan, so you only pay the difference. ## Downgrade Downgrade from the portal. A downgrade is scheduled as **pending** and takes effect at the start of your next billing cycle, so you keep your current limits until then. ## Cancellation Cancel from the portal. Your paid plan stays active until the **end of the current period**, then your organization moves to the **Free** plan automatically. :::note Free is not a selectable target in the change-plan flow. You reach it by cancelling your paid plan, which then takes effect at the end of the period. ::: ## Top-ups If you exhaust your monthly quota before the cycle ends, buy a **top-up** for extra API calls (in steps of 100,000) for the current cycle, paid in **USDC on Ethereum**. When your quota is exhausted and you have no top-up, requests are rejected until the next cycle (or a top-up). Once you have set up [x402](/portal/documentation/payment-methods/x402) yourself, calls can instead continue on a **pay-per-call** basis as overflow. x402 is self-serve for any verified organization, so there is nothing for 1inch to switch on. See [x402 pay-per-call](/portal/documentation/payment-methods/x402). ## Next steps - [Monitoring usage](/portal/documentation/payment-methods/subscriptions/monitoring-usage): track calls against your plan quota. https://business.1inch.com/portal/assets/docs-v2/payment-methods/subscriptions/monitoring-usage.md ## Monitoring your subscription usage Track how much of your plan quota you have used in the Business Portal under [**Observability -> Usage**](/portal/observability/usage) (keep the billing toggle on **Plan**). The view shows: - **Requests**: API calls made in the selected period, measured against your monthly quota. - Breakdowns by **API, method, application, and path**, with filters for period, product, and application. - **Logs** and analytics retention according to your plan. :::tip Watch your consumption against the monthly quota and [upgrade](/portal/documentation/payment-methods/subscriptions/managing-your-subscription#upgrade) or [top up](/portal/documentation/payment-methods/subscriptions/managing-your-subscription#top-ups) before you hit the limit to avoid interrupted calls. To see x402 pay-per-call spend instead, switch the toggle to **x402**. See [Monitoring your x402 usage](/portal/documentation/payment-methods/x402/monitoring-usage). ::: ## Avoid service interruptions When your monthly quota runs out, requests are rejected until the next billing cycle or a top-up. To keep calls flowing without waiting, set up **x402 pay-per-call** in advance: link and fund an application wallet and add the x402 client to your app or agent. x402 is self-serve for any verified organization, so there is nothing for 1inch to switch on. Once your own setup is in place, calls continue on a pay-per-call basis as overflow instead of failing. - [x402 pay-per-call](/portal/documentation/payment-methods/x402): how pay-per-call works and when it applies. - [Getting started with x402](/portal/documentation/payment-methods/x402/getting-started): link a wallet and fund a prepaid balance before you need it. https://business.1inch.com/portal/assets/docs-v2/payment-methods/x402/x402.md ## x402 pay-per-call for 1inch APIs 1inch APIs support **pay-per-call** access through the open **x402** standard: pay per request in **USDC on Base**, with no subscription and no monthly commitment. It runs alongside [subscription plans](/portal/documentation/payment-methods/subscriptions). Use it on its own, or as overflow when a plan's quota is exhausted. Each paid call is a **two-request** flow: the server answers an unpaid request with `402 Payment Required` and a machine-readable challenge, then you retry the same request with a signed payment. In practice an x402 client library handles this for you. See [Client library](/portal/documentation/payment-methods/x402/client-library). :::info Base is only the **payment rail**. The APIs still cover every chain 1inch supports; USDC on Base is used to settle the per-request payments. ::: ## When x402 applies - You call a 1inch API **without an API key**. The gateway answers `402 Payment Required` with the payment requirements: per-request price (product-specific), payment scheme (`batch-settlement`), network and token (USDC on Base), and the minimum prepaid deposit. - Sending an API key uses the normal plan / subscription path instead, with no `402`. ## Before you start | Requirement | Details | | :------------------ | :-------------------------------------------------------- | | **Network** | **Base** | | **Token** | **USDC** | | **Minimum deposit** | **$2.00 USDC** to open your prepaid balance | | **Wallet** | Any EVM wallet (MetaMask, WalletConnect, etc.) | | **Audience** | Verified Business Portal organization (KYC/KYB completed) | ## Pricing Per-call list prices, settled in **USDC on Base**. **$2.00** is the minimum prepaid deposit that opens your balance. | API | Price per call | Calls per $1 | Calls per $2 (min. deposit) | | :------------------ | :------------- | :----------- | :-------------------------- | | Web3 RPC | $0.00018 | ~5,560 | ~11,110 | | Domains | $0.00018 | ~5,560 | ~11,110 | | Swap | $0.00024 | ~4,170 | ~8,330 | | Orderbook | $0.00024 | ~4,170 | ~8,330 | | Spot Price | $0.00024 | ~4,170 | ~8,330 | | Token | $0.00024 | ~4,170 | ~8,330 | | Transaction Gateway | $0.00024 | ~4,170 | ~8,330 | | Gas Price | $0.00024 | ~4,170 | ~8,330 | | NFT | $0.00024 | ~4,170 | ~8,330 | | Traces | $0.0003 | ~3,330 | ~6,670 | | History | $0.0003 | ~3,330 | ~6,670 | | Charts | $0.0003 | ~3,330 | ~6,670 | | Balance | $0.00048 | ~2,080 | ~4,170 | | Portfolio | $0.00048 | ~2,080 | ~4,170 | | Token Details | $0.00048 | ~2,080 | ~4,170 | :::note **$0 Swap fees for the time being.** During the launch, 1inch is waiving its Swap infrastructure fee. For the Swap API you pay only the x402 per-call price shown above, with no additional 1inch swap fee. ::: ## Next steps - [Getting started](/portal/documentation/payment-methods/x402/getting-started): set up your account, link and fund a wallet, and make your first paid request. - [Client library](/portal/documentation/payment-methods/x402/client-library): automate the payment flow with the x402 SDK for TypeScript, Go, or Python. - [Wallet management & refunds](/portal/documentation/payment-methods/x402/wallet-and-refunds): unlink a wallet and recover your prepaid balance. - [Avoiding interruptions](/portal/documentation/payment-methods/x402/avoiding-interruptions): keep paid calls flowing without a `402` or `401`. - [Monitoring usage](/portal/documentation/payment-methods/x402/monitoring-usage): track requests, spend, and channel balance. https://business.1inch.com/portal/assets/docs-v2/payment-methods/x402/getting-started.md ## Getting started with x402 This guide takes you from an empty account to your first paid x402 request. Every paid call is a **two-request** flow: the server challenges with `402 Payment Required`, then you retry with a signed payment. ## 1. Set up your account - Sign in or register at [business.1inch.com/portal/login](https://business.1inch.com/portal/login). - Create an application (you need at least one to link a wallet). ## 2. Connect and link your wallet - Open **Applications** and select your application. - On the application details page, go to **x402 Linked wallets** and click **Link wallet**. - In the dialog, click **Connect wallet**, choose your wallet provider (e.g. MetaMask) and approve the connection. - Sign the verification message to confirm ownership. This is **gas-free** and does not send a transaction. - When linking succeeds, the wallet appears on the application detail page. ## 3. Fund your wallet - On your application detail page, copy your linked wallet address. - In your wallet, switch network to **Base** (mainnet) and fund it with **real USDC on Base**: deposit at least **$2.00 USDC** (the minimum to open your prepaid balance). :::info You do **not** need ETH for gas to start. The deposit is gasless (EIP-3009 `receiveWithAuthorization`, broadcast for you), paying per request is off-chain, and 1inch pays the gas for automatic and cooperative refunds. The only path that needs a little **ETH on Base** for gas is the optional on-chain [self-withdraw](/portal/documentation/payment-methods/x402/wallet-and-refunds) fallback, which you broadcast yourself. ::: ### What each call costs Per-call list prices, settled in **USDC on Base**. Use this to size your deposit above the **$2.00** minimum: the more you prepay, the more calls you cover before the next automatic top-up. | API | Price per call | Calls per $1 | Calls per $2 (min. deposit) | | :------------------ | :------------- | :----------- | :-------------------------- | | Web3 RPC | $0.00018 | ~5,560 | ~11,110 | | Domains | $0.00018 | ~5,560 | ~11,110 | | Swap | $0.00024 | ~4,170 | ~8,330 | | Orderbook | $0.00024 | ~4,170 | ~8,330 | | Spot Price | $0.00024 | ~4,170 | ~8,330 | | Token | $0.00024 | ~4,170 | ~8,330 | | Transaction Gateway | $0.00024 | ~4,170 | ~8,330 | | Gas Price | $0.00024 | ~4,170 | ~8,330 | | NFT | $0.00024 | ~4,170 | ~8,330 | | Traces | $0.0003 | ~3,330 | ~6,670 | | History | $0.0003 | ~3,330 | ~6,670 | | Charts | $0.0003 | ~3,330 | ~6,670 | | Balance | $0.00048 | ~2,080 | ~4,170 | | Portfolio | $0.00048 | ~2,080 | ~4,170 | | Token Details | $0.00048 | ~2,080 | ~4,170 | :::note **$0 Swap fees for the time being.** During the launch, 1inch is waiving its Swap infrastructure fee. For the Swap API you pay only the x402 per-call price shown above, with no additional 1inch swap fee. ::: The same list is on the [x402 overview](/portal/documentation/payment-methods/x402#pricing). ## 4. Make your first API request Every paid call is a **two-request** flow: the server challenges with `402 Payment Required`, then you retry with a signed payment. ### 4a. Send a request without an API key or payment x402 is triggered by calling the API **without an API key** (sending a key uses the subscription path instead). With no API key and no payment, the gateway answers with a `402` challenge: ```bash curl -i "https://api.1inch.com/swap/v6.0/1/quote?amount=1000000000000000000&src=0xEeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&dst=0xdAC17F958D2ee523a2206206994597C13D831ec7" ``` ### 4b. Receive 402 Payment Required, then retry with a signed payment The `402` response includes the payment requirements: per-request price (product-specific), payment scheme (`batch-settlement`), network and token (USDC on Base), and the minimum prepaid deposit (**$2.00**). Retry the same request with the signed payment header: ```bash curl -i "https://api.1inch.com/swap/v6.0/1/quote?amount=1000000000000000000&src=0xEeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee&dst=0xdAC17F958D2ee523a2206206994597C13D831ec7" \ -H "PAYMENT-SIGNATURE: " ``` You receive `200 OK` with the API response. Each call is charged per request from your prepaid balance, with no monthly subscription on the x402 path. :::tip Signing the payment header by hand is only practical for a few test calls. For real usage, use the [x402 client library](/portal/documentation/payment-methods/x402/client-library): it runs the whole `402` -> deposit -> voucher -> retry flow for you. ::: https://business.1inch.com/portal/assets/docs-v2/payment-methods/x402/client-library.md ## Automate payments with the x402 client library The manual two-request flow (`402` challenge -> signed retry) is handled for you by the official **x402 client SDK** maintained by the x402-foundation, the same SDK family the 1inch gateway is built on. It opens the prepaid channel, signs the deposit and per-request vouchers, attaches the `PAYMENT-SIGNATURE` header, and resyncs automatically. You just make normal HTTP requests. The 1inch gateway accepts **only** the `batch-settlement` scheme, so you register `BatchSettlementEvmScheme`. :::note Set `depositMultiplier` yourself. The SDK default of **5** escrows far less than the **$2.00** minimum deposit and the channel open is rejected. The examples below target **swap** (`GET /swap/v6.0/1/quote`), whose minimum is **8334**. See [Sizing depositMultiplier](/portal/documentation/payment-methods/x402/client-library#sizing-depositmultiplier) below for other products. ::: ### TypeScript ```bash npm install @x402/fetch @x402/evm @x402/core viem ``` ```typescript import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; import { toClientEvmSigner } from "@x402/evm"; import { BatchSettlementEvmScheme } from "@x402/evm/batch-settlement/client"; import { createPublicClient, http } from "viem"; import { base } from "viem/chains"; // Base mainnet (eip155:8453) import { privateKeyToAccount } from "viem/accounts"; // Use the wallet you linked to your application in the Business Portal. const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`); const publicClient = createPublicClient({ chain: base, transport: http() }); const signer = toClientEvmSigner(account, publicClient); // The 1inch gateway accepts ONLY the batch-settlement scheme. const batchScheme = new BatchSettlementEvmScheme(signer, { // Deposit = depositMultiplier x per-request price, and it must clear the // $2.00 minimum. Swap costs $0.00024/request -> ceil(2.00 / 0.00024) = 8334. // The SDK default (5) would escrow $0.0012 and be rejected with // 402 "deposit_below_minimum". Raise it for high call volumes. depositPolicy: { depositMultiplier: 8334 } }); const client = new x402Client(); client.register("eip155:*", batchScheme); // A fetch that transparently does: 402 challenge -> deposit/voucher -> retry. const fetchWithPayment = wrapFetchWithPayment(fetch, client); const res = await fetchWithPayment( "https://api.1inch.com/swap/v6.0/1/quote?amount=1000000000000000000" + "&src=0xEeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "&dst=0xdAC17F958D2ee523a2206206994597C13D831ec7" ); console.log(res.status, await res.text()); ``` ### Higher RPS (TypeScript) Batch-settlement serializes **one in-flight paid request per channel**. Extra throughput is extra channels: register several `BatchSettlementEvmScheme` instances with distinct `salt` values. Each channel still needs a **$2** deposit (five channels = **$10**). ### Go & Python StackBlitz cannot run Go or Python. Copy the snippet below, or run the example locally. Account setup, wallet linking, and refunds are in the [x402 docs](/portal/documentation/payment-methods/x402). One channel on `api.1inch.com` (Base). Each deposit is **$2**; the SDK tops up when that balance runs out, up to **$10**. ```go // go get github.com/x402-foundation/x402/go/v2 github.com/ethereum/go-ethereum package main import ( "context" "fmt" "log" "net/http" "os" "time" x402 "github.com/x402-foundation/x402/go/v2" x402http "github.com/x402-foundation/x402/go/v2/http" batchedclient "github.com/x402-foundation/x402/go/v2/mechanisms/evm/batch-settlement/client" evmsigners "github.com/x402-foundation/x402/go/v2/signers/evm" "github.com/ethereum/go-ethereum/ethclient" ) const ( swapQuoteURL = "https://api.1inch.com/swap/v6.0/1/quote" + "?amount=1000000000000000000" + "&src=0xEeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "&dst=0xdAC17F958D2ee523a2206206994597C13D831ec7" // settlement is on Base depositMultiplier = 8334 // ceil($2.00 / $0.00024). SDK default (5) is rejected. maxDeposits = 5 // 5 x $2 = $10 cap on auto top-ups paidCalls = 10 ) func main() { // Base mainnet RPC. The signer reads chain state for the deposit. ethClient, err := ethclient.Dial("https://mainnet.base.org") if err != nil { log.Fatal(err) } // Sign with the wallet you linked to your application in the Business Portal. signer, err := evmsigners.NewClientSignerFromPrivateKeyWithClient(os.Getenv("EVM_PRIVATE_KEY"), ethClient) if err != nil { log.Fatal(err) } deposits := 0 // The 1inch gateway accepts ONLY the batch-settlement scheme. batchScheme := batchedclient.NewBatchSettlementEvmScheme(signer, &batchedclient.BatchSettlementEvmSchemeOptions{ DepositMultiplier: depositMultiplier, // Called before each on-chain deposit (open and auto top-up). Stop at $10. DepositStrategy: func(_ context.Context, _ batchedclient.DepositStrategyContext) (batchedclient.DepositStrategyResult, error) { if deposits >= maxDeposits { return batchedclient.DepositStrategyResult{}, fmt.Errorf("refusing deposit: would exceed $10") } return batchedclient.DepositStrategyResult{}, nil }, }) client := x402.Newx402Client().Register("eip155:*", batchScheme) // Count deposits so the $10 cap and the "wait for finalization" step know when one happened. client.OnAfterPaymentCreation(func(ctx x402.PaymentCreatedContext) error { if ctx.Payload != nil && ctx.Payload.GetPayload()["type"] == "deposit" { deposits++ } return nil }) // http.Client that runs 402 challenge -> deposit/voucher -> retry for you. httpClient := x402http.WrapHTTPClientWithPayment( http.DefaultClient, x402http.Newx402HTTPClient(client), ) for call := 1; call <= paidCalls; call++ { before := deposits req, err := http.NewRequest(http.MethodGet, swapQuoteURL, nil) if err != nil { log.Fatal(err) } resp, err := httpClient.Do(req) if err != nil { log.Fatal(err) } resp.Body.Close() fmt.Println("call", call, "HTTP", resp.StatusCode) if deposits > before { // First call (and later top-ups) settle on-chain. Wait before the next voucher. time.Sleep(3 * time.Second) } } } ``` ```python # pip install "x402[evm]" httpx import asyncio import os import httpx from eth_account import Account from x402 import x402Client from x402.http.clients import x402_httpx_transport from x402.mechanisms.evm import EthAccountSignerWithRPC from x402.mechanisms.evm.batch_settlement.client import ( BatchSettlementDepositPolicy, BatchSettlementDepositStrategyContext, BatchSettlementEvmScheme, BatchSettlementEvmSchemeOptions, InMemoryClientChannelStorage, ) SWAP_QUOTE_URL = ( "https://api.1inch.com/swap/v6.0/1/quote" "?amount=1000000000000000000" "&src=0xEeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" "&dst=0xdAC17F958D2ee523a2206206994597C13D831ec7" ) # settlement is on Base DEPOSIT_MULTIPLIER = 8334 # ceil($2.00 / $0.00024). SDK default (5) is rejected. MAX_DEPOSITS = 5 # 5 x $2 = $10 cap on auto top-ups PAID_CALLS = 10 # Sign with the wallet you linked to your application in the Business Portal. account = Account.from_key(os.environ["EVM_PRIVATE_KEY"]) # Base mainnet RPC. The signer reads chain state for the deposit. signer = EthAccountSignerWithRPC(account, rpc_url="https://mainnet.base.org") deposits = {"n": 0} # Called before each on-chain deposit (open and auto top-up). Stop at $10. def deposit_strategy(_ctx: BatchSettlementDepositStrategyContext) -> None: if deposits["n"] >= MAX_DEPOSITS: raise ValueError("refusing deposit: would exceed $10") return None def on_created(ctx) -> None: inner = getattr(ctx.payment_payload, "payload", None) or {} if inner.get("type") == "deposit": deposits["n"] += 1 # The 1inch gateway accepts ONLY the batch-settlement scheme. batch_scheme = BatchSettlementEvmScheme( signer, BatchSettlementEvmSchemeOptions( deposit_policy=BatchSettlementDepositPolicy(deposit_multiplier=DEPOSIT_MULTIPLIER), deposit_strategy=deposit_strategy, storage=InMemoryClientChannelStorage(), ), ) client = x402Client().register("eip155:*", batch_scheme) client.on_after_payment_creation(on_created) async def main() -> None: # httpx client that runs 402 challenge -> deposit/voucher -> retry for you. async with httpx.AsyncClient(transport=x402_httpx_transport(client)) as http: for call in range(1, PAID_CALLS + 1): before = deposits["n"] resp = await http.get(SWAP_QUOTE_URL) print("call", call, "HTTP", resp.status_code) if deposits["n"] > before: # First call (and later top-ups) settle on-chain. Wait before the next voucher. await asyncio.sleep(3) asyncio.run(main()) ``` ## Sizing depositMultiplier The SDK sizes **every** deposit (the initial one and each automatic top-up) as _multiplier x per-request price_. It does **not** read the minimum deposit from the `402` challenge, so the multiplier is yours to get right. Because per-request prices are fractions of a cent, it belongs in the thousands: ```text depositMultiplier >= ceil(minDeposit / per-request price) ``` Both inputs are advertised in the `402` challenge, in atomic USDC units (6 decimals): `accepts[].maxAmountRequired` is the per-request price and `accepts[].extra.minDeposit.amount` is the minimum prepaid deposit (`2000000` = $2.00). For **swap** at $0.00024 per request that is `ceil(2.00 / 0.00024)` = **8334**, i.e. a $2.00016 deposit covering 8334 paid calls. | Per-request price | `maxAmountRequired` | Minimum `depositMultiplier` | | :------------------- | :------------------ | :-------------------------- | | $0.00024 (e.g. swap) | `240` | **8334** | | $0.00018 (e.g. web3) | `180` | **11112** | | $0.00030 | `300` | **6667** | | $0.00048 | `480` | **4167** | These are minimums: they escrow just above the $2.00 floor, which is also exactly the number of calls each deposit buys. Pick a **larger** multiplier for real volume. The same multiplier sizes every automatic top-up, so a bigger one means fewer on-chain deposits. ## Good to know - TypeScript, Go, and Python all ship an official **batch-settlement** client: pick the one that matches your stack. The gateway behaves identically for all three. - Sign with your **linked wallet**: payments only succeed for a wallet linked to your application (otherwise the gateway returns `401 Wallet not linked`). - Production runs on **Base** mainnet (`eip155:8453`). Import the `base` viem chain and target `https://api.1inch.com`. - `depositPolicy` controls how much USDC is escrowed when a channel opens, and it must clear the **$2.00** minimum: `depositMultiplier: 8334` for swap, _not_ the SDK default of 5 (see [Sizing depositMultiplier](/portal/documentation/payment-methods/x402/client-library#sizing-depositmultiplier) above). The SDK re-deposits automatically when the balance runs low. - The SDK handles the deposit (EIP-3009 `receiveWithAuthorization`), cumulative voucher signing, and corrective `402` resync after any channel state loss, with no manual header building required. The protocol is open, so other languages can integrate directly against the [x402 standard](https://docs.x402.org). https://business.1inch.com/portal/assets/docs-v2/payment-methods/x402/wallet-and-refunds.md ## Managing your wallet: revoke & refunds ## Revoking (unlinking) a wallet You can unlink a wallet from an application in the Business Portal at any time (**Applications -> Application details -> x402 Linked wallets -> remove**). After unlinking: - x402 requests signed by that wallet are rejected with `401 Wallet not linked`: the wallet can no longer pay for new API calls. - 1inch **automatically closes the unlinked wallet's payment channel**: it settles any outstanding usage, refunds the remaining deposit to your wallet within **~10 minutes**, and pays the gas for you. - A wallet can be linked to **only one application at a time**. Once unlinked, it is free to link to another application. - Wallet addresses are mapped **per application**. **Deleting an application** unlinks the wallet(s) mapped to it. Link the wallet to another application to keep x402 service uninterrupted; otherwise its channel is refunded automatically within **~10 minutes**, like any other unlink. - Any USDC you already prepaid into your payment channel **stays safe on-chain in the escrow contract**. It is bound to your _wallet address_, not to the application, and is never custodied by 1inch. :::info Unlinking stops spending **and** returns your money: the remaining channel balance is refunded to your wallet automatically within **~10 minutes** -- no action and no gas needed on your side. The refund paths below remain available if you need your funds sooner or want to withdraw without depending on 1inch. ::: ## Refunds: recovering your prepaid balance Your residual channel balance (what you deposited minus what you have already spent on API calls) is always recoverable to your wallet. There are four ways: | Path | When to use it | What you do | | :-------------------------------- | :-------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Automatic refund (unlink)** | You are done paying with this wallet. | Unlink the wallet in the portal. 1inch settles any outstanding usage and refunds the remaining deposit to your wallet within **~10 minutes**, and pays the gas for you. | | **Automatic refund (inactivity)** | Default: you took no other action. | Nothing. 1inch automatically refunds a channel that stays idle for **1 week** back to your payer wallet, and pays the gas for you. | | **Cooperative refund (via SDK)** | You want your funds back immediately and the wallet is _still linked_. | Request a refund through the SDK **before** unlinking. The server claims any outstanding vouchers and returns the unused balance in a single step. | | **On-chain self-withdraw** | Guaranteed fallback: works even after unlinking and never depends on 1inch. | From your wallet, call `initiateWithdraw` on the escrow contract, wait the withdraw delay (~15 min), then call `finalizeWithdraw` to receive your USDC. Requires **ETH on Base** for gas. | :::note **Refund timing:** while the wallet stays linked, a channel is considered idle once it stops making paid calls, and the automatic refund is triggered after **1 week** of inactivity. Unlinking the wallet skips the inactivity window entirely: the channel is refunded within **~10 minutes**. Use the cooperative SDK refund if you need your balance back immediately while staying linked. A refund is deferred while a paid request is still in flight and runs once it completes. ::: ## Recommended - If you plan to keep using the wallet, **link it to another application** first: your channel and prepaid balance keep working, with no interruption. If the wallet stays unlinked, its channel is refunded within **~10 minutes** and a later re-link starts with a fresh deposit. - If you are done with the wallet, just **unlink it**: the remaining balance comes back automatically within **~10 minutes**. Alternatively, request a **cooperative refund via the SDK before unlinking**, or simply stop calling -- idle balances are returned after 1 week without any action. - The on-chain self-withdraw (`initiateWithdraw` -> wait -> `finalizeWithdraw`) is the fallback that never depends on 1inch. The withdraw delay is advertised in the `402` challenge (`extra.withdrawDelay`, default **900 seconds**). https://business.1inch.com/portal/assets/docs-v2/payment-methods/x402/avoiding-interruptions.md ## Avoiding interruptions Paid x402 calls only fail for a few reasons, and all are easy to avoid: | What happens | Why | How to avoid it | | :------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Requests start returning `402 Payment Required` again | Your prepaid channel balance ran out, so the next voucher is no longer covered. | Keep **USDC on Base** in the payer wallet so the SDK can top up automatically (see [Keep the channel funded](#keep-the-channel-funded)). | | The first paid request returns `402` with `deposit_below_minimum` | Your `depositMultiplier` sized the channel-opening deposit below the **$2.00** minimum. The SDK default of **5** always does. | Set `depositMultiplier` to at least `ceil($2.00 / per-request price)`: **8334** for swap, **11112** for web3. The response repeats the required minimum in `minDeposit`. | | Requests return `401 Wallet not linked` | The wallet was unlinked from the application (or linked to a different one). | Do not unlink the wallet you are actively paying with. If you migrate, **link it to the new application first**: a wallet can be linked to only one application at a time. A wallet that stays unlinked has its channel [refunded automatically within ~10 minutes](/portal/documentation/payment-methods/x402/wallet-and-refunds), so a delayed migration starts over with a fresh deposit. | | Requests return `401 Wallet not linked` after **deleting an application** | Wallet addresses are mapped **per application**. Deleting an application unlinks the wallet(s) mapped to it, so they can no longer pay. | Before or right after deleting an application, **map the wallet to another application** so x402 payments continue without discontinuation -- re-link promptly: an unlinked wallet's channel is refunded automatically after **~10 minutes**. | ## Keep the channel funded - **Set `depositMultiplier`, then let the SDK manage deposits.** It sizes each deposit as _multiplier x per-request price_ and **re-deposits automatically** when the balance runs low. The SDK default of **5** is far below the **$2.00** minimum deposit, so use at least `ceil($2.00 / per-request price)` (**8334** for swap, **11112** for web3) and raise it further for high call volumes to reduce the number of on-chain top-ups. See [Sizing depositMultiplier](/portal/documentation/payment-methods/x402/client-library#sizing-depositmultiplier). - **Fund ahead of demand.** Hold enough USDC on Base for your expected volume. You do not need ETH to deposit or pay (both are gasless for you). Keep a little **ETH on Base** only if you plan to use the on-chain [self-withdraw](/portal/documentation/payment-methods/x402/wallet-and-refunds) fallback, which you broadcast yourself. - **Persist client channel state** for long-lived clients (file storage, or Redis/Valkey for multi-instance) so a restart reuses the existing channel instead of opening a redundant one. Even without it, the SDK self-heals via corrective `402` resync and on-chain state on the next paid request. - **Watch your balance** and top up before it reaches zero: see [Monitoring your x402 usage](/portal/documentation/payment-methods/x402/monitoring-usage). https://business.1inch.com/portal/assets/docs-v2/payment-methods/x402/monitoring-usage.md ## Monitoring your x402 usage Track live x402 consumption in the Business Portal under [**Observability -> Usage**](/portal/observability/usage) (switch the toggle from **Plan** to **x402**). The view shows: - **Requests**: number of paid x402 calls in the selected period. - **Spent**: how much of your prepaid balance has been consumed. - **Wallets balance** and **Deposited balance**: what is left in your channel versus what you have deposited. Use these to decide when to top up. - A spend/volume chart with breakdowns by **API, method, app, path, and wallet**, plus filters for period, product, app, and wallet. :::tip Because per-request prices differ by product, monitor **Spent** (not just Requests) to understand your real cost, and top up before **Wallets balance** hits zero to avoid a `402` interruption. ::: ## Optional https://business.1inch.com/portal/assets/docs-v2/apis/swap/cross-chain-swap/sdk/web-socket-api.md The WebSocket API enables real-time order tracking for cross-chain swaps. Instead of repeatedly polling the API for status updates, your application receives instant notifications when order events occur. ## Why Use WebSocket? For applications that need responsive user interfaces or automated trading systems, WebSocket provides significant advantages: - **Instant Updates** -- Receive order events within milliseconds instead of waiting for poll intervals - **Reduced API Load** -- A single persistent connection replaces hundreds of status requests - **Complete Event History** -- Track all state changes including partial fills, not just final status ## Order Events The WebSocket API notifies you of these order lifecycle events: - **order_created** -- Your order has been submitted and is visible to resolvers - **order_filled** -- The order was completely filled by a resolver - **order_filled_partially** -- Part of your order was filled (for large orders) - **order_invalid** -- The order became invalid due to cancellation, expiry, or insufficient balance ## Connection The WebSocket connects to: ``` wss://api.1inch.com/fusion-plus/ws ``` The SDK appends the version path automatically. ## Interactive Code Example https://business.1inch.com/portal/assets/docs-v2/apis/swap/intent-swap/migration/migration-v10-v20.md Following changes must be done to migrate from v1.0 to v2.0. ## Quoter Request Data changed ### Request Adding of parameter:\ `isPermit2`: permit2 allowance transfer encoded call ## Relayer Request Data changed ### Request Removing of parameter:\ `allowedSender`\ `offsets`\ `interactions` Adding of parameter:\ `extension` https://business.1inch.com/portal/assets/docs-v2/apis/swap/classic-swap/migration/migration-v50-v52.md Following changes must be done to migrate from v5.0 to v5.2 ## Parameter change for /quote ### Request Renaming of parameter:\ `fromTokenAddress` -> `src`\ `toTokenAddress` -> `dst` ### Added new parameter optional query params: `includeTokensInfo: boolean` - Return fromToken and toToken info in response\ `includeProtocols: boolean` - Return used swap protocols in response\ `includeGas: boolean` - Return approximated gas in response ### Response: Renaming of parameter:\ `estimatedGas` -> `gas`\ `toTokenAmount` -> `toAmount` ### Specialities: By default only `toAmount` will be in the response. Additional parameter can be activated by additional parameters. ## Parameter change for /swap ### Request Renaming of parameter:\ `fromTokenAddress` -> `src`\ `toTokenAddress` -> `dst`\ `fromAddress` -> `from`\ `destReceiver` -> `receiver`\ `referrerAddress` -> `referrer`\ `compatibilityMode` -> `compatibility` ### Added new parameter optional query params: `includeTokensInfo: boolean` - Return fromToken and toToken info in response\ `includeProtocols: boolean` - Return used swap protocols in response\ `includeGas: boolean` - Return approximated gas in response ### Response: Renaming of parameter:\ `toTokenAmount` -> `toAmount` ### Specialities: By default only `toAmount` and `tx` will be in the response. Additional parameter can be activated by additional parameters. https://business.1inch.com/portal/assets/docs-v2/apis/swap/classic-swap/migration/migration-v52-v60.md ## Parameter change for /quote ### Response Renaming of parameter:\ `fromToken` -> `srcToken`\ `toToken` -> `dstToken`\ `toAmount` -> `dstAmount` ## Parameter change for /swap ### Added new parameter optional query params: `usePermit2: boolean` - use Permit2 for approval ### Response: Renaming of parameter:\ `fromToken` -> `srcToken`\ `toToken` -> `dstToken`\ `toAmount` -> `dstAmount` https://business.1inch.com/portal/assets/docs-v2/apis/swap/classic-swap/migration/migration-v60-v61.md The following changes must be made when migrating from v6.0 to v6.1. ## Changes in `/swap` and `/quote` responses ### Structural updates - The `protocols[][][]` structure remains, but the format of protocol data has changed. - `fromTokenAddress` and `toTokenAddress` have been removed. Instead, a single `token` field is used at a higher level. ### Changed and renamed parameters - `fromTokenAddress` -> Removed (source token is now referenced at the top level). - `toTokenAddress` -> Removed (destination is now handled via `dst`). - `name` -> Moved inside protocols array within `swaps`. - `part` -> Now nested inside `swaps` and `protocols`. Compare the old version of that part of response with a new one below. ### Old response example ```json "protocols": [ [ [ { "name": "UNISWAP_V2", "part": 2, "fromTokenAddress": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", "toTokenAddress": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599" }, { "name": "UNISWAP_V4", "part": 2, "fromTokenAddress": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", "toTokenAddress": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599" }, { "name": "SUSHI", "part": 4, "fromTokenAddress": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", "toTokenAddress": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599" }, { "name": "UNISWAP_V3", "part": 6, "fromTokenAddress": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", "toTokenAddress": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599" }, { "name": "UNISWAP_V3", "part": 86, "fromTokenAddress": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", "toTokenAddress": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599" } ], //... ] ] ``` ### New response example ```json "protocols": [ { "token": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", "hops": [ { "part": 62.5, "dst": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "fromTokenId": 0, "toTokenId": 1, "protocols": [ { "name": "UNISWAP_V4", "part": 1 }, { "name": "CURVE_V2_TRICRYPTO_NG", "part": 1 }, { "name": "UNISWAP_V3", "part": 1 }, { "name": "UNISWAP_V2", "part": 3 }, { "name": "UNISWAP_V3", "part": 5 }, { "name": "UNISWAP_V3", "part": 20 }, { "name": "FLUID_DEX_T1", "part": 69 } ] } //... ] } ] ``` For full schemas, see [Swagger documentation for the `/swap` and `/quote` endpoints](../methods/swagger.md). :::info If you disabled `includeProtocols`, change the version to a newer one. No additional actions are required. ::: https://business.1inch.com/portal/assets/docs-v2/apis/orderbook/migration/migration-v3-v4.md Following changes must be done to migrate from v3.0 to v4.0 ## Request ### Typechange of parameter: - `statuses`: change from `array` (`[1,2,3]`) to `comma-separated list` (`1,2,3`) ## LimitOrder Request Data changed ### Removing of parameters: - `allowedSender` - `offsets` - `interactions` ### Adding of parameter: - `extension`: An interaction call data. ABI encoded set of makerAssetSuffix, takerAssetSuffix, makingAmountGetter, takingAmountGetter, predicate, permit, preInteraction, postInteraction.If extension exists then lowest 160 bits of the order salt must be equal to the lowest 160 bits of the extension hash - `makerTraits`: Includes some flags like, allow multiple fills, is partial fill allowed or not, price improvement, nonce, deadline etc. ## Response ### Removing of parameters: - `allowedSender` - `offsets` - `interactions` ### Adding of parameters: - `extension`: An interaction call data. ABI encoded set of makerAssetSuffix, takerAssetSuffix, makingAmountGetter, takingAmountGetter, predicate, permit, preInteraction, postInteraction.If extension exists then lowest 160 bits of the order salt must be equal to the lowest 160 bits of the extension hash - `makerTraits`: Includes some flags like, allow multiple fills, is partial fill allowed or not, price improvement, nonce, deadline etc. ## New get order by order hash endpoint added: - `orderbook/v4.1/{chain}/order/{orderHash}` `GET` endpoint was created to enable getting orders filtered by the specified order hash https://business.1inch.com/portal/assets/docs-v2/apis/portfolio/migration/migration-v40-v50.md ## Key Difference between V4 and V5 **V5 introduces a fundamental separation between `snapshot` and `metrics`:** - `snapshot` -- a state at a specific point in time - `metrics` -- historical metrics over a selected time range (e.g. 1day, 1week) ## Migration Guide: From `/v4` to `/v5.0` When calling through the **unified gateway** at `https://api.1inch.com` or the **MCP `product_api` tool**, use the gateway prefix `/portfolio/portfolio/v5.0/...` (prepend an extra `/portfolio`). The tables below show **service / OpenAPI** paths `/portfolio/v5.0/...` after routing. ### General | V4 Endpoint | V5.0 Endpoint | Notes | | ------------------------------------------- | --------------------------------------------- | ------------------ | | `/portfolio/v4/general/is_available` | `/portfolio/v5.0/general/status` | Renamed | | `/portfolio/v4/general/supported_chains` | `/portfolio/v5.0/general/supported_chains` | Same functionality | | `/portfolio/v4/general/supported_protocols` | `/portfolio/v5.0/general/supported_protocols` | Same functionality | | `/portfolio/v4/general/current_value` | `/portfolio/v5.0/general/current_value` | Wrapped response | | `/portfolio/v4/general/profit_and_loss` | **Removed** -> see `/metrics` | Moved to metrics | | `/portfolio/v4/general/value_chart` | `/portfolio/v5.0/general/chart` | Renamed | | _(none)_ | `/portfolio/v5.0/general/address_check` | New endpoint | ### Tokens | V4 Endpoint | V5.0 Endpoint | Notes | | ---------------------------------------------- | --------------------------------- | ------------------------------------ | | `/portfolio/v4/overview/erc20/current_value` | `/portfolio/v5.0/tokens/snapshot` | Snapshot replaces current value | | `/portfolio/v4/overview/erc20/profit_and_loss` | `/portfolio/v5.0/tokens/metrics` | Historical metrics | | `/portfolio/v4/overview/erc20/details` | Covered by snapshot + metrics | Merged into `snapshot` and `metrics` | ### Protocols | V4 Endpoint | V5.0 Endpoint | Notes | | -------------------------------------------------- | ------------------------------------ | ------------------------------------ | | `/portfolio/v4/overview/protocols/current_value` | `/portfolio/v5.0/protocols/snapshot` | Snapshot replaces current value | | `/portfolio/v4/overview/protocols/profit_and_loss` | `/portfolio/v5.0/protocols/metrics` | Historical metrics | | `/portfolio/v4/overview/protocols/details` | Covered by snapshot + metrics | Merged into `snapshot` and `metrics` |