This is the full developer documentation for Ekubo Protocol
# Introduction
> Open-source, permissionless, free-by-default AMM infrastructure
Ekubo Protocol is AMM infrastructure featuring super-concentrated liquidity, a singleton architecture with flash accounting, and extensions. It is deployed on [Starknet](/reference/contracts/starknet/) and, with Ekubo V3, on [EVM chains](/reference/contracts/evm-v3/) — Ethereum, Base, Arbitrum, Robinhood Chain, and more — at the same contract addresses on every chain.
The contracts are open source under the [Ekubo DAO Shared Revenue License](https://ekubo-license-v1.eth.link/), and are heavily optimized for gas and for precise control over where liquidity sits.
## Where to go
[Section titled “Where to go”](#where-to-go)
* **New to Ekubo?** Start with [Features](/about-ekubo/features/), then browse the [products](/products/).
* **Trading or providing liquidity?** See [Trading](/products/trading/), [Providing liquidity](/products/liquidity/), and the [user guides](/user-guides/add-liquidity/).
* **Building on Ekubo?** Start with [Integrating Ekubo](/integration-guides/).
* **Curious how it works?** Read [Protocol architecture](/concepts/architecture/).
* **Looking up an address, API, or audit?** See the [reference](/reference/contracts/).
# Features
> What sets Ekubo Protocol apart from other AMM protocols
## Free by default
[Section titled “Free by default”](#free-by-default)
The core contracts are ownerless and permissionless. They are deployed to the same address on every chain using a script that anyone can run. Core itself collects no protocol fee — swap fees accrue entirely to the pool. Protocol fees, where they exist, are applied at the periphery, for example by the Positions contract when a provider collects their fees. This is the key design decision that allows Ekubo V3 to serve many different [licensees](/about-ekubo/v3-whitepaper/) on one shared liquidity layer.
## Gas efficiency
[Section titled “Gas efficiency”](#gas-efficiency)
Ekubo uses the [“till” pattern](/concepts/architecture/) and a singleton design to provide the cheapest trades across many pools. All pools live in a single contract, and token transfers are deferred until the end of the transaction. Advanced users don’t have to transfer tokens at all: balances can be saved inside Ekubo for later use, avoiding repeated token transfers entirely.
The result is that you can execute many actions across many pools while making only the minimum number of token transfers. Combined with contracts optimized down to the storage-slot and calldata level (see [Price representation](/reference/price-representation/)), this keeps the gas cost of a trade low, which matters most for routes that touch several pools. Measured numbers against Uniswap v4 and v3 are on the [Gas efficiency](/reference/gas-efficiency/) page.
## Concentrated liquidity
[Section titled “Concentrated liquidity”](#concentrated-liquidity)
Concentrated liquidity allows market makers to [provide liquidity](/user-guides/add-liquidity/) within a specified price range. Each liquidity provider chooses the exact parameters of their position, but from a swapper’s perspective all positions in a pool are aggregated into a single curve. Concentrating liquidity near the market price deepens the book where trades actually happen, so swappers see less price impact — and providers are free to deploy the capital they did not have to post elsewhere.
Ekubo’s ticks are 1/100th of a basis point — 100 times finer than the usual concentrated-liquidity convention. Finer ticks make narrower ranges expressible, so a position can be placed precisely where the provider wants it. A narrower range concentrates the same capital into less price space, which raises the fees earned per dollar of principal *while the price stays in range* — and equally raises the rate at which the position converts into the losing asset when the price moves against it, and the frequency with which it goes out of range entirely.
## Multiple pool types
[Section titled “Multiple pool types”](#multiple-pool-types)
Beyond concentrated liquidity, Ekubo V3 pools can be configured as stableswap pools (liquidity concentrated around a center price with an amplification factor) or full-range pools — all in the same Core contract, sharing the same routing and integration surface.
## Extensions
[Section titled “Extensions”](#extensions)
[Extensions](/concepts/extensions/) allow third-party developers to permissionlessly create new kinds of pools on Ekubo that plug into the same ecosystem of aggregators and interfaces. Deployed extensions include price [oracles](/reference/contracts/evm-v3/), TWAMM ([DCA orders](/user-guides/dollar-cost-average-orders/)), limit orders, MEV capture, and [Ve33](/products/ve33/) token-governed liquidity.
For what you can actually do with all of this — trading, providing liquidity, running incentive campaigns, indexing the data — see [Products](/products/).
# Ekubo V3: Shared Liquidity as a Public Good
> Ekubo V3's shared liquidity architecture: a common AMM and settlement layer for independent products, integrations, and AI agents.
**Date:** November 19, 2025 · **Last updated:** September 9, 2026
## Motivation
[Section titled “Motivation”](#motivation)
Launching an automated market maker (AMM) often means deploying another copy of familiar contracts, attracting a separate base of liquidity, and rebuilding the integrations around it. Traders encounter fragmented markets, while liquidity providers and developers must decide which deployments to support. Each new brand or version can repeat much of the same work without improving the underlying market design.
Ekubo V3 separates the market infrastructure from the products built on it. Its central contract, Core, implements the AMM, holds tokens, and accounts for swaps and liquidity positions. Independent teams can build their own interfaces, contracts, and revenue models around the same deployment. A new product can use existing pools instead of requiring users to move their liquidity to another fork.
The aim is to make liquidity and settlement common infrastructure. Teams can compete on distribution, execution workflows, asset curation, and user experience while contributing to a shared market. Tooling developed for that market can serve many products, and transactions that combine its pools can settle without transferring intermediate tokens between separate AMM contracts.
This paper describes the V3 EVM architecture. Sharing occurs within a Core deployment on a particular chain; deploying the same code on another chain does not combine their balances or liquidity.
## The AMM Encoded in Core
[Section titled “The AMM Encoded in Core”](#the-amm-encoded-in-core)
Core implements concentrated liquidity using constant-product swap math. Liquidity providers choose price ranges, and their liquidity participates while the market price is inside those ranges. The underlying tick grid advances in increments of 0.01 basis points, with each pool’s tick spacing determining the available position boundaries. This gives market makers fine control over where they supply liquidity.
Core also supports full-range and stableswap configurations. These configurations belong to the shared implementation, so a team can select the appropriate pool design without maintaining its own copy of the swap math. The implementation is available in the [Core contract](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/Core.sol) and [pool configuration code](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/types/poolConfig.sol).
A pool is identified by its token pair and configuration, including its fee, pool type parameters, and extension. Products using the same pool key on the same Core access the same pool. Different configurations remain distinct markets, even though Core holds their tokens. Position ownership and accounting also remain separate: sharing a pool does not give one product control over another product’s positions.
## Engineering for Gas Efficiency
[Section titled “Engineering for Gas Efficiency”](#engineering-for-gas-efficiency)
Core reduces the storage access and token movement needed to execute a trade. Frequently accessed state is packed into compact representations, and critical arithmetic uses low-level operations. These choices matter because every transaction pays for the work performed by the EVM.
The price representation illustrates this approach. Core stores the square-root price in a 96-bit dynamic fixed-point format whose two highest bits select the scale. This leaves room for the current tick and active liquidity in the same 256-bit storage word. The encoding and layout are defined in [SqrtRatio](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/types/sqrtRatio.sol) and [PoolState](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/types/poolState.sol).
Compact state and careful arithmetic reduce overhead, but their effect depends on the transaction. Tick crossings, extension logic, token behavior, and the route itself still affect gas consumption. The design should be evaluated through measurements of comparable operations; it does not establish that further optimization is impossible.
## Licensees and White-Labeled AMMs
[Section titled “Licensees and White-Labeled AMMs”](#licensees-and-white-labeled-amms)
Independent teams can build products around Core under the Ekubo DAO Shared Revenue License. These licensees choose how users access the market, which assets and pools they feature, and how they earn revenue. Their products may use routers, position managers, and extensions to compose the underlying operations into a particular trading or liquidity-management experience.
The standard positions contract provides one way to manage user liquidity above Core. A licensee can deploy it with its own fee parameters, including a share of collected swap fees and a fee on liquidity withdrawals. In the reference [Positions implementation](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/Positions.sol), those parameters are set at construction and are immutable for that deployment. Different position managers can therefore offer different economics while supplying liquidity to the same Core pool.
This arrangement allows products to share liquidity without sharing their entire business model. A trader can reach a pool through several interfaces, and liquidity in that pool can serve the resulting order flow. The benefit depends on products actually selecting and routing into common pools; using the same Core alone does not eliminate fragmentation across configurations.
## Extensions: Shared Protocol Features
[Section titled “Extensions: Shared Protocol Features”](#extensions-shared-protocol-features)
Extensions are contracts that add behavior at defined points in Core’s operations. They let builders reuse protocol features while keeping the underlying AMM implementation in one place. A pool’s configuration selects its extension, so extension behavior is part of the identity and execution requirements of that pool.
The reference implementation includes an [Oracle](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/extensions/Oracle.sol) that records cumulative observations for supported pools, a [TWAMM](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/extensions/TWAMM.sol) that executes orders over time, and [MEV Capture](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/extensions/MEVCapture.sol), which collects additional swap fees based on price movement and accounts for them as pool fees. These features support different market requirements without requiring each licensee to reproduce them.
Products can offer access to pools with different extensions or develop new extensions of their own. A single pool key contains one extension address; combining several behaviors in one pool requires an extension designed to compose them. Integrators must account for those behaviors when quoting and executing trades, even when they already support Core.
## Network Effects From a Singleton Core
[Section titled “Network Effects From a Singleton Core”](#network-effects-from-a-singleton-core)
### Tooling and Analytics
[Section titled “Tooling and Analytics”](#tooling-and-analytics)
A common contract interface and event model give indexers, analytics platforms, and monitoring tools a reusable foundation. Once an integration understands Core’s pools, swaps, and liquidity accounting, it can support additional products using that deployment without indexing another fork of the AMM.
Product-specific attribution, position managers, and extension behavior may still require additional work. The shared architecture concentrates that work around a common market model, allowing improvements to pool discovery and monitoring to benefit multiple teams.
### Integrations and Routing
[Section titled “Integrations and Routing”](#integrations-and-routing)
Routers can discover and compare pools within Core using a common pool identity and accounting model. A new frontend that uses an existing pool does not create another venue for the router to integrate. A new pool configuration can be evaluated within the same framework, subject to any requirements imposed by its extension.
This makes it possible for distribution to grow independently of the number of AMM deployments. More products can bring order flow to existing liquidity, while more accessible liquidity can make those products useful to additional traders. The architecture enables this feedback; it does not guarantee deeper liquidity or better execution for every trade.
### Gas Efficiency Across Licensees
[Section titled “Gas Efficiency Across Licensees”](#gas-efficiency-across-licensees)
A route spanning separate AMM contracts generally needs to transfer intermediate tokens between their custody addresses, either directly or through a router. Within a single Core, the output of one operation can instead offset the input obligation of another through internal accounting.
For example, an A-to-B swap followed by a B-to-C swap can settle the intermediate B balance without an ERC-20 transfer of B out of Core and back in. Both swaps still execute and update their respective pools. The saving comes from netting token obligations, not from treating a route through two pools as one swap.
The same principle applies when products operated by different licensees compose operations on a shared Core. Their branding does not require separate custody, and their use of different position managers need not introduce additional AMM hops.
## Flash Accounting as a Supporting Feature
[Section titled “Flash Accounting as a Supporting Feature”](#flash-accounting-as-a-supporting-feature)
Flash accounting makes this settlement model possible. During a lock, Core tracks token obligations as operations execute, allowing swaps and liquidity changes to be combined before settlement. The EVM implementation uses transient storage for this accounting and reverts if any debt remains outstanding when the lock ends. These checks are implemented in [FlashAccountant](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/base/FlashAccountant.sol).
Core also supports saved balances that contracts can retain and reuse across transactions. Those balances are accounted for by the locker address, token pair, and a salt; custody in the same contract does not make them freely accessible to other callers.
Together, net settlement and saved balances let builders compose operations with fewer token transfers. The singleton provides a common place for those operations to occur, and flash accounting provides the mechanism for settling them efficiently.
## Permissionless, Ownerless, and Fee-Externalized
[Section titled “Permissionless, Ownerless, and Fee-Externalized”](#permissionless-ownerless-and-fee-externalized)
The V3 EVM Core has no owner role or administrative switch for imposing a global protocol fee. Pool creation and extension registration are governed by contract validation rather than a discretionary approval process. These properties concern Core itself; position managers, extensions, and other contracts must be evaluated separately.
The code can be deployed and integrated under the [Ekubo DAO Shared Revenue License](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/LICENSE). The license governs revenue sharing and other obligations, including notices and distribution terms. Its definition of Protocol Revenue is broader than the fee setting on a particular positions contract, so a zero setting alone does not establish that a product has no revenue-sharing obligation.
Core’s accounting is separate from those licensing obligations. Products can implement their revenue models in contracts above Core, while the shared AMM continues to account for pool liquidity and trading fees. This separation places product economics at the integration layer and allows the same market infrastructure to support different commercial arrangements.
## Shared Liquidity for AI Agents
[Section titled “Shared Liquidity for AI Agents”](#shared-liquidity-for-ai-agents)
AI lowers the cost of writing software, including another AMM fork. It also lowers the cost of comparing and using the markets that software creates. Our thesis is that the second effect will make execution efficiency more important: as agents take on more trading and liquidity-management work, measurable outcomes should carry more weight in where activity goes.
An agent can be more patient and less influenced by branding than a person navigating trading websites. Given a user’s objectives and deadline, it can compare more routes, revisit quotes, wait for an acceptable execution cost, or evaluate an order that executes over time. It need not stop at the first familiar interface. That behavior depends on the agent’s instructions, available tools, and the cost of running them; agents are not automatically impartial or optimal. But tools that expose comparable quotes and explicit transaction costs make it practical to choose on economic grounds.
Ekubo provides that access through its [hosted MCP server](/products/mcp-server/) at `https://mcp.ekubo.org/mcp`. Agents can resolve tokens, request swap quotes with unsigned execution plans, inspect pools and positions, and prepare liquidity operations or TWAMM orders. The service exposes these operations as structured tools, so an agent can work directly with the market without navigating a frontend. Its swap quoting includes external providers alongside Ekubo, making comparison part of the workflow. The [live service catalog](https://mcp.ekubo.org/) describes the available capabilities.
[Ekubo Wallet](/wallet/) completes the execution path. An agent passes the selected unsigned plan to the local wallet, which independently retrieves and verifies it, simulates the exact calls, and checks the user’s signing policy. The wallet signs and submits transactions allowed by that policy, queues transactions requiring owner review, and rejects those prohibited by policy. The hosted MCP server supplies data and prepares actions; it does not hold wallet keys or authorize spending. The [agent integration guide](/wallet/agents/) explains how the two tools work together.
For example, a user can ask an agent to acquire an asset within a specified budget and time window. The agent can obtain current quotes, compare expected proceeds and transaction costs, and bring a selected plan to the wallet for simulation and authorization. If immediate execution does not meet the user’s limits, the agent can revisit the decision later or propose a TWAMM order where supported. Waiting introduces price risk, and simulation does not guarantee later execution, so the user’s constraints remain part of the decision.
This is where Core’s engineering becomes commercially relevant. Lower gas overhead can improve a route’s net economics; shared liquidity lets multiple products reach an existing market; and flash accounting avoids intermediate token transfers when operations settle within Core. An agent comparing execution plans has a reason to account for each of these properties. A fork may be easier to write, but it still has to attract liquidity and order flow, support integrations, and compete on actual execution costs. We expect competition among agents to increase the value of those technical advantages, while making it easier for new products to access them through the same tools.
## How It Feels to Use Ekubo
[Section titled “How It Feels to Use Ekubo”](#how-it-feels-to-use-ekubo)
### For Traders
[Section titled “For Traders”](#for-traders)
Traders choose an interface and submit a trade against the pools it supports. Several interfaces can reach the same liquidity, so choosing a different product need not mean moving to a different market. Execution still depends on the selected route, available liquidity, fees, and transaction limits.
### For Liquidity Providers
[Section titled “For Liquidity Providers”](#for-liquidity-providers)
A liquidity provider’s position can serve trades arriving from any product that routes into its pool. Supporting another frontend does not inherently require creating another position or migrating funds. Concentrating liquidity increases exposure to fees while the position is in range, but also increases divergence-loss exposure and the likelihood of going out of range; shared distribution does not remove those tradeoffs.
### For Licensees and Builders
[Section titled “For Licensees and Builders”](#for-licensees-and-builders)
Builders can focus on the product around the market: how users discover opportunities, express their intentions, manage positions, and authorize transactions. They can reuse Core and compatible integrations while taking responsibility for their own contracts, extension choices, and economics.
## Summary
[Section titled “Summary”](#summary)
Ekubo V3 gives independent products a common AMM and settlement layer. Concentrated liquidity, configurable pool designs, and extensions support different market needs within a shared Core deployment. Compact state and flash accounting reduce overhead, while common interfaces make tooling reusable across products.
The long-term opportunity is a market that can support many brands, business models, and automated agents without requiring each to recreate its liquidity infrastructure. As agents make market comparison cheaper, we expect execution costs and accessible liquidity to become stronger reasons to choose that foundation. Ekubo’s hosted MCP server and wallet turn this architecture into a workflow agents can use today.
# Vision
> Shared Liquidity as a Public Good
Ekubo is a next-generation AMM protocol built around a **singleton Core contract** that all deployments share. Instead of fragmenting liquidity across countless forks and versions, every licensee — each with its own branding, economics, and frontend — settles directly in the same Core. This design transforms liquidity itself into a **public good**, eliminating redundant work, consolidating integrations, and maximizing on-chain efficiency.
At the heart of Ekubo Core is a high-precision, concentrated-liquidity AMM engineered for extreme gas efficiency. The Core contract encodes all swap and pool logic, while **licensees** build products, interfaces, and fee models on top. This separation keeps the Core neutral and durable, while allowing permissionless innovation at the edges. Shared extensions — like on-chain oracles, TWAMMs, and MEV-capture modules — further expand the protocol’s capabilities without fragmenting liquidity.
By unifying liquidity, tooling, and integrations under one canonical implementation, Ekubo creates strong network effects across the DeFi ecosystem. Traders benefit from deeper pools and lower costs; liquidity providers can serve order flow from every venue built on the same Core rather than splitting capital across forks; and builders can focus on user experience instead of re-implementing AMMs. Ekubo turns the AMM layer into open, composable infrastructure — a foundation for many brands, products, and communities to thrive together.
Read the full [V3 whitepaper](/about-ekubo/v3-whitepaper/).
# Protocol architecture
> How Ekubo is put together: one singleton Core contract, deferred settlement via the "till" pattern, and a thin periphery
Ekubo is built as a **singleton**: a single Core contract holds every pool, every position, and all token balances. Rather than a separate contract per pool or per version, all liquidity lives in one place. This is what makes shared liquidity, cheap multi-pool routing, and a single integration surface possible — see the [V3 whitepaper](/about-ekubo/v3-whitepaper/) for the reasoning.
## The “till” pattern
[Section titled “The “till” pattern”](#the-till-pattern)
Every interaction that moves tokens starts with a call to `lock`. Core calls back into your contract, you perform any number of operations (swap, add or remove liquidity, collect fees), and only the **net** token amounts are settled at the end. A few operations that move no funds — notably pool initialization and extension registration — are callable directly, without a lock. Payments are deferred until you have finished — like a shop till that is reconciled once, rather than per item.
This is the “till” pattern, [publicly introduced](https://www.youtube.com/watch?v=xFp8RlRq0qU) at EthCC\[5] and described in more detail [here](https://github.com/OpenZeppelin/openzeppelin-contracts/issues/4361#issuecomment-1595095135).
```plaintext
your contract Core
│ lock() ────────▶ │
│ ◀──── locked() │ ← callback: you are now inside the lock
│ swap() ────────▶ │
│ swap() ────────▶ │ ← any number of operations, no transfers yet
│ withdraw()/pay() │ ← settle the net difference
│ ◀────────────── │ ← lock ends; Core asserts all balances are settled
```
Core tracks what you owe and are owed as **deltas** during the lock, and requires every delta to be zero before the lock can close. Until then, no tokens move.
Note
Even though the entrypoint for all methods is named `lock`, Ekubo supports reentrancy. Locks can be nested, so any contract you call from inside a lock can itself interact with Ekubo.
## Flash accounting
[Section titled “Flash accounting”](#flash-accounting)
Deferred settlement is what makes Ekubo cheap for anything more complex than a single swap. Trading across many pools, or opening several positions, requires only the minimum number of token transfers — the net difference — instead of one transfer per operation.
Two consequences worth knowing:
* **Saved balances.** Rather than withdrawing tokens at the end of a lock, you can leave them inside Ekubo for later use, avoiding token transfers entirely across repeated interactions.
* **Free flash loans.** Because balances are only checked when the lock closes, you can `withdraw` tokens and repay them within the same transaction at no cost.
## Pools
[Section titled “Pools”](#pools)
A pool is identified by its **pool key**: the two tokens (sorted, so `token0 < token1`), the fee, the pool type parameters, and the [extension](/concepts/extensions/) address. Pool state is deliberately compact — on EVM, the current price, tick, and liquidity pack into a single storage word (see [Price representation](/reference/price-representation/)) so that swaps touch as little storage as possible.
Ekubo V3 supports several pool types in the same Core contract: concentrated liquidity, stableswap (liquidity concentrated around a center price with an amplification factor), and full range.
## Extensions
[Section titled “Extensions”](#extensions)
[Extensions](/concepts/extensions/) are separate contracts that Core calls at defined points in a pool’s lifecycle — before and after initialization, swaps, position updates, and fee collection. They let developers add behavior (oracles, order types, custom fee logic) without reimplementing the AMM, and without fragmenting liquidity into a separate protocol.
## Periphery
[Section titled “Periphery”](#periphery)
Core charges no protocol fee, and on EVM it is ownerless — there is no privileged account at all. (The Starknet Core is owner-upgradeable by governance, but likewise charges no protocol fee.) Everything user-facing lives in periphery contracts that hold locks on your behalf:
* **Positions** wraps liquidity positions as NFTs, and is where protocol fees are applied. It supports a fee on collected swap fees and, separately, one on withdrawn principal — the canonical deployment sets the latter to zero (see [Providing liquidity](/products/liquidity/#fees))
* **Orders** manages TWAMM ([DCA](/user-guides/dollar-cost-average-orders/)) orders
* **Routers** execute swap routes, including the gas-optimized [Yul Router](/integration-guides/yul-router/) used in production on EVM
* **Lens contracts** provide read-only helpers for prices, quotes, and pool state
This separation is deliberate: Core stays neutral and durable, while fee models and user experience live at the edges. See [Contract addresses](/reference/contracts/) for what is deployed where.
# Extensions
> Customize pool behavior by writing extension contracts
Extensions let you insert custom logic at defined points in a pool’s lifecycle. Like any other contract, an extension can also re-enter Core to place its own swaps or position updates before or after the calling contract interacts with a pool. This lets third-party developers build on Ekubo’s efficient, audited AMM — concentrated liquidity included — without reimplementing any of the math. From this small interface you can build oracles, new order types, trading strategies, and privacy solutions.
Note
Each pool has its own separate state, meaning the capital deposited into each pool with an extension is isolated from all other pools, including other pools with the same extension.
## Rationale
[Section titled “Rationale”](#rationale)
Liquidity fragmentation is inevitable. In the absence of extensions, others will make small improvements to the Ekubo design or add new features, and deploy a variant of the core AMM. As a result, users must split their trades across many different sources of liquidity.
Ekubo aims to solve this problem by reducing the cost of fragmentation to near-zero. This is the purpose of the [singleton design and the till pattern](/concepts/architecture/). Ekubo is a platform for an ecosystem of different types of pools that are all aggregated with every aggregator and arbitrageur, so markets can operate as efficiently as possible. This ecosystem of different kinds of liquidity also has the benefit of providing traders the best possible execution.
## What belongs in an extension
[Section titled “What belongs in an extension”](#what-belongs-in-an-extension)
Extensibility is for creating new *pool functionality*: behavior that plugs into the [flash accounting system](/concepts/architecture/#flash-accounting) and works as if it had been part of the original design. A TWAMM pool, an oracle pool, a vote-governed ve(3,3) pool — each changes what a pool *is*, and every router, aggregator, and arbitrageur settles against it through the same lock as any other pool.
What an extension is not is a place to add third-party dependencies. An extension is an immutable part of its pools’ keys and sits in the hot path of every interaction with them, so coupling one to an external protocol welds that protocol’s risks, upgrades, and failure modes into the pool itself — permanently, for every LP and trader in it.
The line is easy to draw in practice. Take a product someone might plausibly want to build. A position spanning a few basis points around the current price offers the same depth *at* that price as a far larger wide-range position, so the capital the wide position would have tied up is not needed in the pool at all. The product keeps a narrow position centered on the market, parks the capital it frees in a money market, and moves funds between the two as the price drifts — drawing on the money market to re-center or top up the position, returning what the position no longer needs. Nothing in that changes how the pool works. Swaps quote, fill, and settle exactly as before, and from the pool’s point of view the whole arrangement is indistinguishable from an LP who mints and burns positions by hand. What it decides is *where capital sits and when it moves*: that is a **strategy**, and it belongs on top of the AMM, not inside it.
On top is also where an idea like this can be judged and adjusted. A narrower range concentrates fee income and divergence loss in equal measure, the price leaves it more often, and every re-centering pays gas and swap fees — so whether the arrangement beats simply holding a wider position depends on volatility, gas prices, and money-market rates, and the answer moves as they do. A strategy on top can be re-tuned, or abandoned, in the next block. An extension cannot: it is fixed in the pool key, so every revision is a new pool that must attract liquidity from scratch, and until then one particular money market sits in the hot path of every swap for everyone in the pool, LPs and traders alike, whether they want that exposure or not.
There are many ways to run a strategy on top. Vaults in the style of Yearn run them autonomously as contract code, at the cost of a newly audited contract per strategy. Hedge funds run them with people, who take a share of the profits and make mistakes. The most forward-looking way is to hand the strategy to an AI agent and let it run autonomously — which is precisely what the [MCP server](/products/mcp-server/) is for: an agent gets the same tokens, quotes, position data, and execution plans the interface uses, so the strategy lives in the agent’s instructions, where rewriting it costs a sentence, rather than in anyone’s pool.
## Flexibility
[Section titled “Flexibility”](#flexibility)
You may want a different trading algorithm entirely — a different curve. You can approximate almost any curve by overlapping several `x*y=k` positions, so Ekubo’s core components serve a wide range of AMM designs. At the extreme, where you want to quote every trade individually, Ekubo’s very small ticks let you use it as an order book: place one-tick orders at whatever prices your extension decides, whether from an oracle or as a function of time.
Because an extension can re-enter the core Ekubo contract to perform its own actions within these lifecycle events, the simple interface allows for a huge amount of customization of pool behavior. For example, on the before-swap call point you could add your own liquidity ahead of the trade, improving the price the swapper gets; or you could record the pool’s price *before* the trade moves it, which is exactly how the Oracle extension builds its history — it snapshots on before-swap and before-position-update, so every observation is a pre-trade price.
## Immutability
[Section titled “Immutability”](#immutability)
Extensions are specified as part of the pool key. The specified extension is an immutable configuration of a pool. Before a pool can be initialized with an extension, the extension must be registered with Core along with the set of pool lifecycle events (“call points”) at which it should be called.
Note
Declare every hook you might need up front. How call points are declared — and how permanent they are — differs by deployment. On EVM (V3) they are encoded in the top byte of the extension’s own address (addresses are mined so the address itself declares the hooks), and Core rejects a second registration, so they are genuinely immutable. On Starknet the extension registers them with Core via `set_call_points`, which can be called again; a change applies to all of that extension’s existing pools, so treat them as immutable by convention.
The full list of call points is:
* Before pool initialization
* After pool initialization
* Before position update
* After position update
* Before collect fees
* After collect fees
* Before a swap
* After a swap
Because an extension holds its own state like any other contract, you can use that state to implement new kinds of orders. Limit orders, for example, work like this:
* Expose functions that let users create limit orders through your extension
* Immediately add liquidity to the pool for each new order
* After each swap, remove any orders that were fully executed
Note
An extension is immutable for a given pool, so either make the extension itself upgradeable or keep it simple enough that it never needs upgrading. We recommend immutable extensions, deploying a new version when one is needed.
## Available extensions
[Section titled “Available extensions”](#available-extensions)
These extensions are already built and deployed. Each one is a pool type you can trade against or build on — see [Contract addresses](/reference/contracts/) for deployments.
| Extension | Chains | What it does |
| ------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| **Oracle** | Both | Records on-chain price history for a token against a designated quote asset — native ETH on EVM, a configured oracle token on Starknet |
| **TWAMM** | Both | Executes orders gradually over time, powering [DCA orders](/user-guides/dollar-cost-average-orders/) |
| **Limit orders** | Starknet | Narrow positions exactly one tick spacing wide (128 ticks, about 1.28 bps) that are pulled automatically once fully executed |
| **MEV capture** | EVM | Charges additional fees on swaps that move the price significantly within a block, directing that value back to liquidity providers |
| **Boosted fees** | EVM | Streams externally funded fee rewards to a pool’s liquidity providers |
| **[Ve33](/products/ve33/)** | EVM | Token-governed liquidity: stakers vote to direct emissions and set pool fees, and earn the fees of the pools they support |
| **[Signed exclusive swaps](/integration-guides/signed-exclusive-swaps/)** | EVM | RFQ-style pools where a controller signs each swap off-chain with its own fee and bounds |
Source code: [EVM extensions](https://github.com/EkuboProtocol/evm-contracts/tree/v3.2.0/src/extensions) and [Starknet extensions](https://github.com/EkuboProtocol/starknet-contracts/tree/v5.0.3/src/extensions).
For how this design compares to Uniswap v4 hooks — and why the `forward` primitive replaces v4’s custom-accounting machinery — see [Extensions vs. Uniswap v4 hooks on the Ekubo Blog](https://blog.ekubo.org/ekubo-extensions-vs-uniswap-v4-hooks/).
# Key concepts
> The concepts behind Ekubo Protocol, and the platforms it runs on
## Protocol concepts
[Section titled “Protocol concepts”](#protocol-concepts)
### Automated Market Maker (AMM)
[Section titled “Automated Market Maker (AMM)”](#automated-market-maker-amm)
A decentralized finance protocol that lets users trade against pooled liquidity at algorithmically determined prices, allowing liquidity providers to automatically “buy low, sell high.” AMMs differ primarily in the amount of customization available to liquidity providers. Depositing into an AMM is a bet that trading fees will outweigh the loss due to price divergence of the two assets in a pair.
### Constant product
[Section titled “Constant product”](#constant-product)
The `x*y=k` formula that forms the basis of most AMMs, where `x` is the amount of one token (`token0`), `y` is the amount of the other (`token1`), and `k` is held constant as users trade with the pool. This formula is how trades are computed on Ekubo within regions of constant liquidity — see the [pool math](/reference/pool-math/).
### Concentrated liquidity
[Section titled “Concentrated liquidity”](#concentrated-liquidity)
The main feature that allows Ekubo to provide better pricing than other AMMs. In simple constant-product AMMs, deposited capital backs trading at every possible price — including unrealistic ones — so most of it sits unused. Concentrated liquidity lets you choose the price range each position trades in. If you think ETH will only trade between 1,800 and 2,200 USDC, you can provide liquidity in just that range and deploy the rest of your capital elsewhere — or concentrate more capital into the range. Concentration cuts both ways: it raises fees earned per dollar while the price is in range, and equally amplifies divergence loss when the price moves. It does not make providing liquidity more profitable on its own.
**Capital efficiency** measures this concentration: how much more capital a full-range position would need to match a concentrated position’s depth at the current price. It describes how the same exposure is expressed with less principal — not a higher expected return.
### Ticks
[Section titled “Ticks”](#ticks)
Ticks are the discrete price points that can serve as position boundaries. Ekubo divides the price range logarithmically: tick `i` corresponds to the price `1.000001^i`, so each tick is 1/100th of a basis point — finer precision than most centralized limit order books. One-tick positions behave like limit orders, which makes Ekubo suitable as an on-chain order book.
**Tick spacing** is the minimum distance between the ticks a pool’s positions may use, and is set per pool. Smaller tick spacing allows tighter ranges; larger tick spacing makes swaps cheaper to compute. Volatile pairs, where tiny price differences don’t matter, are better served by a larger tick spacing.
### Flash accounting
[Section titled “Flash accounting”](#flash-accounting)
All token balance accounting happens inside Ekubo before any tokens are transferred: you can trade with many pools and update many positions, then settle only the net difference at the end. This is what the [“till” pattern](/concepts/architecture/) enables, and it is why multi-pool routing on Ekubo is cheap.
## Platform concepts
[Section titled “Platform concepts”](#platform-concepts)
### Ethereum
[Section titled “Ethereum”](#ethereum)
[Ethereum](https://ethereum.org/en/) is a blockchain that supports “smart contracts”: autonomous programs that anyone can deploy and use. Ekubo Protocol V3 is deployed to Ethereum mainnet and many EVM-compatible networks — at the same contract addresses on every chain.
### Layer 2
[Section titled “Layer 2”](#layer-2)
A network that uses Ethereum to provide a more scalable platform without sacrificing decentralization or security. Sites such as [L2BEAT](https://l2beat.com/) track L2s and score them along multiple dimensions. Several of Ekubo’s deployment chains (Arbitrum, Base, Optimism, Robinhood Chain, Starknet) are Ethereum L2s.
### Starknet
[Section titled “Starknet”](#starknet)
Starknet is a Layer 2 that scales Ethereum using STARK [zero-knowledge proofs](https://en.wikipedia.org/wiki/Zero-knowledge_proof), which let Ethereum verify the correctness of Starknet state transitions without re-executing every transaction. Starknet contracts are written in [Cairo](https://www.cairo-lang.org/), so ordinary Ethereum wallets are not compatible; wallets such as [Ready](https://www.ready.co/) and [Braavos](https://braavos.app/) work with Starknet. Ekubo’s original deployment is on Starknet.
# Integrating Ekubo
> The different ways to integrate Ekubo Protocol, and production integrations you can use as reference implementations
There are several ways to integrate Ekubo, depending on what you’re building.
## Swap on-chain from a smart contract
[Section titled “Swap on-chain from a smart contract”](#swap-on-chain-from-a-smart-contract)
Every interaction with Ekubo goes through the Core singleton’s [`lock` callback pattern](/concepts/architecture/): call `lock`, receive a callback, perform swaps and settle net token amounts at the end. See [Swapping](/integration-guides/swapping/) for the flow. On EVM, production swaps go through the gas-optimized [Yul Router](https://github.com/EkuboProtocol/yul-router) with routes encoded by [`@ekubo/yul-router-sdk`](https://www.npmjs.com/package/@ekubo/yul-router-sdk); the Router contracts ([Starknet](https://github.com/EkuboProtocol/starknet-contracts/blob/v5.0.3/src/router.cairo), [EVM](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/Router.sol)) are reference implementations for building your own.
## Quote Ekubo liquidity off-chain (aggregators, solvers)
[Section titled “Quote Ekubo liquidity off-chain (aggregators, solvers)”](#quote-ekubo-liquidity-off-chain-aggregators-solvers)
To route trades through Ekubo pools you need to simulate swaps off-chain. The easiest path is the [Quoter API](/api/#quoter), which returns block-pinned split routes ready to execute. To compute quotes yourself, use the [SDKs](/integration-guides/sdks/) — the Rust SDK implements every pool type and extension — or see [Price representation](/reference/price-representation/) and [Pool math](/reference/pool-math/) to implement the math directly. Remember that pools with [extensions](/concepts/extensions/) can modify swap behavior; see the [Aggregators guide](/integration-guides/aggregators/) for how to handle them safely.
## Index Ekubo data
[Section titled “Index Ekubo data”](#index-ekubo-data)
The open source [indexer](https://github.com/EkuboProtocol/indexer) ingests Ekubo events on any supported chain into Postgres — it is the same code that powers the [Ekubo API](/api/). Run your own instance for low-latency or high-volume needs.
## Connect an AI agent
[Section titled “Connect an AI agent”](#connect-an-ai-agent)
The public [MCP server](/products/mcp-server/) at `mcp.ekubo.org` exposes quoting, pool and position data, and unsigned execution plans to any MCP-capable agent. [Compare swap quotes with an AI agent](/integration-guides/compare-swap-quotes-with-an-agent/) walks through token resolution, base units, and a read-only quote comparison with a runnable script.
## Move from Uniswap v3
[Section titled “Move from Uniswap v3”](#move-from-uniswap-v3)
[Migrating from Uniswap v3](/integration-guides/migrating-from-uniswap-v3/) maps pool identity, ticks, fees, quoting, extensions, and licensing to Ekubo on EVM chains, and reads the same market from both protocols.
## Provide exclusive, quoted liquidity
[Section titled “Provide exclusive, quoted liquidity”](#provide-exclusive-quoted-liquidity)
Market makers can run RFQ-style pools where each swap is signed off-chain with its own fee and bounds, while still settling in Ekubo Core — see [Signed exclusive swaps](/integration-guides/signed-exclusive-swaps/).
## SDKs
[Section titled “SDKs”](#sdks)
[`ekubo_sdk`](https://crates.io/crates/ekubo_sdk) (Rust) computes quotes across every pool type; [`@ekubo/sdk`](https://www.npmjs.com/package/@ekubo/sdk) provides pool math in TypeScript; [`@ekubo/yul-router-sdk`](https://www.npmjs.com/package/@ekubo/yul-router-sdk) encodes swap routes for EVM execution. See [SDKs](/integration-guides/sdks/).
## Reference integrations
[Section titled “Reference integrations”](#reference-integrations)
Production integrations of Ekubo you can use as working examples:
| Integration | Language | What it shows | Ekubo code |
| ----------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [KyberSwap dex-lib](https://github.com/EkuboProtocol/kyberswap-dex-lib) | Go | Aggregator pool discovery, state tracking, and swap simulation | [`pkg/liquidity-source/ekubo`](https://github.com/EkuboProtocol/kyberswap-dex-lib/tree/main/pkg/liquidity-source/ekubo) |
| [ParaSwap dex-lib](https://github.com/EkuboProtocol/paraswap-dex-lib) | TypeScript | Aggregator integration with on-chain quoting and event-based state | [`src/dex/ekubo`](https://github.com/EkuboProtocol/paraswap-dex-lib/tree/master/src/dex/ekubo) |
| [Tycho](https://github.com/EkuboProtocol/tycho) | Rust | Substreams-based indexing and off-chain swap simulation of Ekubo V3 | [`protocols/substreams/ethereum-ekubo-v3`](https://github.com/EkuboProtocol/tycho/tree/main/protocols/substreams/ethereum-ekubo-v3), [`crates/tycho-simulation/src/evm/protocol/ekubo_v3`](https://github.com/EkuboProtocol/tycho/tree/main/crates/tycho-simulation/src/evm/protocol/ekubo_v3) |
Questions about an integration? Ask in the [Ekubo Discord](https://discord.ekubo.org).
# Aggregators
> Take advantage of liquidity in Ekubo pools to provide better pricing for users
Note
The code samples on this page are written in Cairo for the Starknet deployment. The same lock/callback flow applies on EVM chains — see [Swapping](/integration-guides/swapping/) and the [EVM contracts repository](https://github.com/EkuboProtocol/evm-contracts) for Solidity equivalents.
## Reference implementations
[Section titled “Reference implementations”](#reference-implementations)
Several production aggregators publish their Ekubo V3 integrations. They all need the same three capabilities — discover pools, keep pool state current, and turn a route into executable calldata — but divide the work differently.
| Implementation | State acquisition | Quoting | Execution |
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| [Tycho](https://github.com/propeller-heads/tycho) | Substreams produce protocol components and per-block state deltas | Rust simulators reconstruct each supported pool type | A Rust encoder targets a dedicated Solidity executor |
| [ParaSwap](https://github.com/VeloraDEX/paraswap-dex-lib/tree/master/src/dex/ekubo-v3) | A subgraph bootstrap transitions to event-driven pool state | TypeScript pool models quote exact-input and exact-output amounts locally | The adapter encodes a call to Ekubo’s router |
| [KyberSwap](https://github.com/KyberNetwork/kyberswap-dex-lib/tree/main/pkg/liquidity-source/ekubo) | Indexed pool discovery plus log application, with an RPC refresh fallback | Go pool simulators quote and update cloned route state locally | The simulator returns the pool key and swap metadata to Kyber’s routing layer |
### Tycho
[Section titled “Tycho”](#tycho)
Tycho separates the integration into independent ingestion, simulation, and execution layers:
* The [`ethereum-ekubo-v3` Substreams package](https://github.com/propeller-heads/tycho/tree/main/protocols/substreams/ethereum-ekubo-v3) maps initialization and swap-related events into protocol components, balances, active ticks, liquidity, and extension-specific rate changes.
* The [`ekubo_v3` simulation module](https://github.com/propeller-heads/tycho/tree/main/crates/tycho-simulation/src/evm/protocol/ekubo_v3) decodes those components and simulates concentrated, full-range, stableswap, oracle, TWAMM, MEV-capture, and boosted-fees pools. Its default filter excludes pools whose extensions cannot be simulated safely; signed-exclusive swaps have an explicit opt-in path because they require off-chain user data.
* The [`EkuboV3SwapEncoder`](https://github.com/propeller-heads/tycho/blob/main/crates/tycho-execution/src/encoding/evm/swap_encoder/ekubo_v3.rs) converts a selected pool and amount into compact calldata for [`EkuboV3Executor.sol`](https://github.com/propeller-heads/tycho/blob/main/crates/tycho-execution/contracts/src/executors/EkuboV3Executor.sol), including the optional signed-swap payload.
This architecture is useful when an indexer serves many routing clients: normalized state changes can be streamed once, while each client maintains a local simulator and the execution service remains a separate concern.
### ParaSwap
[Section titled “ParaSwap”](#paraswap)
ParaSwap keeps the integration together in its TypeScript DEX adapter:
* [`EkuboV3PoolManager`](https://github.com/VeloraDEX/paraswap-dex-lib/blob/master/src/dex/ekubo-v3/ekubo-v3-pool-manager.ts) pages through pool initializations from the subgraph, verifies the handoff against the canonical chain, subscribes before that handoff to avoid a gap, and then forwards Core and extension logs to the relevant in-memory pool.
* The [`pools` directory](https://github.com/VeloraDEX/paraswap-dex-lib/tree/master/src/dex/ekubo-v3/pools) implements pool-specific state transitions and math. The adapter’s [`getPricesVolume`](https://github.com/VeloraDEX/paraswap-dex-lib/blob/master/src/dex/ekubo-v3/ekubo-v3.ts) quotes each requested size locally, rejects pools that cannot consume the complete amount, and returns gas estimates and `skipAhead` hints to the route search.
* Once a route is chosen, `getDexParam` in the same adapter encodes `swapAllowPartialFill` against Ekubo’s router with the pool key, signed amount, direction, skip-ahead value, and recipient.
The notable pattern is the guarded transition from indexed history to live logs. If you bootstrap from a subgraph and then follow RPC events, you need an equivalent continuity and reorg strategy or your local state can silently miss a block.
### KyberSwap
[Section titled “KyberSwap”](#kyberswap)
KyberSwap uses a Go liquidity-source package with explicit discovery, tracking, and simulation stages:
* [`pools_list_updater.go`](https://github.com/KyberNetwork/kyberswap-dex-lib/blob/main/pkg/liquidity-source/ekubo/v3/pools_list_updater.go) pages through indexed pool initializations, recognizes supported pool and extension configurations, and fetches the initial on-chain state in batches.
* [`pool_tracker.go`](https://github.com/KyberNetwork/kyberswap-dex-lib/blob/main/pkg/liquidity-source/ekubo/v3/pool_tracker.go) applies ordered Core, TWAMM, boosted-fees, and ve33 logs to the stored pool. A removed log, missing log batch, or failed state transition triggers a full refresh through the data fetchers.
* [`pool_simulator.go`](https://github.com/KyberNetwork/kyberswap-dex-lib/blob/main/pkg/liquidity-source/ekubo/v3/pool_simulator.go) supports exact-input and exact-output quotes, reports consumed amount, fees and gas, clones mutable swap state for route exploration, and updates balances after a simulated hop.
This design makes recovery behavior especially explicit: event application is the fast path, while authoritative on-chain reads are the correctness fallback.
## Summary
[Section titled “Summary”](#summary)
Ekubo is a singleton AMM that utilizes the “till” pattern. The till pattern was publicly introduced at EthCC\[5] and is also described [here](https://github.com/OpenZeppelin/openzeppelin-contracts/issues/4361#issuecomment-1595095135).
Every interaction with Ekubo starts with `ICore#lock`. In order to interact with Ekubo, you must first implement the `ILocker` interface in your calling contract:
```rust
#[starknet::interface]
trait ICore {
// ...
// Main entrypoint for any actions, which must be called before any other pool functions can be called.
// Other functions must be called within the callback to lock. The ILocker#locked function is called with the input data,
// and the returned array is passed through to the caller.
fn lock(ref self: TStorage, data: Array) -> Array;
// Make a swap against a pool.
// You must call this within a lock callback.
fn swap(ref self: TStorage, pool_key: PoolKey, params: SwapParameters) -> Delta;
// ...
}
// This interface must be implemented by any contract that intends to call ICore#lock
#[starknet::interface]
trait ILocker {
// This function is called on the caller of lock, i.e. a callback
// The input is the data passed to ICore#lock, the output is passed back through as the return value of #lock
fn locked(ref self: TStorage, id: u32, data: Array) -> Array;
}
```
You must then perform your swaps within the callback. To know which swaps you need to do, encode your parameters, such as the pools against which you’d like to swap, into the data argument
An example locker might look like this:
```rust
// ILocksCoreExample interface and imports are implied here
#[starknet::contract]
mod Example {
#[storage]
struct Storage {
ekubo: ContractAddress,
}
#[derive(Copy, Drop, Serde)]
struct SwapData {
// the list of pools that you'd like to swap against, etc.
}
#[derive(Copy, Drop, Serde)]
struct SwapResult {
// the result of the swap
}
#[external(v0)]
impl LocksCoreExample of ILocksCoreExample {
fn swap(ref self: ContractState, swap_data: SwapData) -> SwapResult {
let mut arr: Array = ArrayTrait::new();
Serde::::serialize(@swap_data, ref arr);
let result = ICoreDispatcher { contract_address: self.ekubo.read() }.lock(arr);
let mut result_data = result.span();
let mut result: SwapResult = Serde::::deserialize(
ref result_data
)
.expect('DESERIALIZE_RESULT_FAILED');
result
}
fn locked(ref self: ContractState, id: u32, data: Array) -> Array {
let caller = get_caller_address();
let ekubo = self.ekubo.read();
// Only allow Ekubo's core contract to call this method.
assert(caller == ekubo, 'UNAUTHORIZED_CALLBACK');
let mut swap_data_span = data.span();
let mut swap_data: SwapData = Serde::::deserialize(ref swap_data_span)
.expect('DESERIALIZE_FAILED');
// Do your swaps here! e.g.:
// let delta = ICoreDispatcher { contract_address: ekubo }.swap(pool_key, params);
// Each swap generates a "delta", but does not trigger any token transfers.
// A negative delta indicates you are owed tokens. A positive delta indicates core owes you tokens.
// To take a negative delta out of core, do (assuming token0):
// ICoreDispatcher { contract_address: ekubo }.withdraw(token, recipient, delta.amount0.mag);
// To pay tokens you owe, do (assuming token1):
// IERC20Dispatcher {
// contract_address: token
// }.transfer(ekubo, u256 { low: delta.mag, high: 0 });
// assert(
// ICoreDispatcher { contract_address: ekubo }.deposit(token) == delta.amount1.mag,
// 'DEPOSIT_FAILED'
// );
let mut arr: Array = ArrayTrait::new();
Serde::::serialize(@result, ref arr);
arr
}
}
}
```
Note
Instead of withdrawing a delta, you can also save it for use later using `#save` or load it using `#load`.
## Locker utility method
[Section titled “Locker utility method”](#locker-utility-method)
You may wish to use this shared code to call core with some calldata and automagically deserialize the result:
```rust
use serde::Serde;
use starknet::{call_contract_syscall, ContractAddress, SyscallResultTrait};
use ekubo::interfaces::core::{ICoreDispatcher, ICoreDispatcherTrait};
use array::{ArrayTrait};
use option::{OptionTrait};
fn call_core_with_callback<
TInput, impl TSerdeInput: Serde, TOutput, impl TSerdeOutput: Serde,
>(
core: ICoreDispatcher, input: @TInput
) -> TOutput {
let mut input_data: Array = ArrayTrait::new();
Serde::serialize(input, ref input_data);
let mut output_span = core.lock(input_data).span();
Serde::deserialize(ref output_span).expect('DESERIALIZE_RESULT_FAILED')
}
```
## Note on extensions
[Section titled “Note on extensions”](#note-on-extensions)
Extensions are third-party code that can change the result of swapping against a pool, usually by updating liquidity positions before the swap — though an extension may also front-run a swap with one of its own.
There are two ways to handle extensions:
* read the code for the extension and support it by off-chain simulation
* use the quoter to simulate swaps across pools, which always includes extension behavior
With the latter approach you remain exposed to per-block changes in an extension’s behavior, but this is no different from exposure to any other front-runner. Always check the resulting output amount against an expected slippage tolerance, protecting users from a misbehaving extension exactly as you would from front-running.
# Compare swap quotes with an AI agent
> Resolve tokens, convert amounts to base units, and compare indicative EVM swap quotes from the Ekubo MCP server, then hand the chosen plan to a wallet
This guide builds the read-only first half of an agent swap: resolve two tokens on one EVM chain, convert a human amount to base units, and compare the quotes every available source returns. It ends where execution would begin and describes that continuation without running it. Nothing here signs, submits, or needs a funded wallet.
It uses the public [Ekubo MCP server](/products/mcp-server/), which needs no API key. An agent connected to the server makes the same calls; the script below makes them directly so you can check the exact inputs and outputs.
## Scope
[Section titled “Scope”](#scope)
| Item | Covered here |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Chains | Same-chain swaps on the EVM chains the server indexes. The example uses Ethereum (chain ID 1). The quote tool takes EVM chain IDs only. |
| Sources | Every source the server returns for the request. On 24 October 2026 that was Ekubo and 0x for same-chain swaps; cross-chain requests go to Across, LayerZero, and LI.FI instead. |
| Server | Observed on server version `0.53.2`, tool catalog revision `2026-10-21.legacy-route-removal`. The live catalog at [`https://mcp.ekubo.org/tools`](https://mcp.ekubo.org/tools) is authoritative where this page has drifted. |
| Quote kind | Indicative only: no `sender` and no `slippage_bps`, so no calldata is produced. |
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
* Node.js 20 or later. The example was run with Node.js 22.23.4 in a fresh `node:22-slim` container.
* Outbound HTTPS to `mcp.ekubo.org`.
* No wallet, private key, RPC endpoint, or API key.
## Run the example
[Section titled “Run the example”](#run-the-example)
Create an empty directory and install the official MCP TypeScript client:
```sh
npm init -y
npm install @modelcontextprotocol/sdk@1.32.2
```
Save this as `compare-quotes.mjs`. It connects to the `ekubo` endpoint, which serves only Ekubo’s core tools; the bundled `https://mcp.ekubo.org/mcp` endpoint accepts the same calls.
```js
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const CHAIN_ID = 1;
const SELL = "ETH";
const BUY = "USDC";
const AMOUNT = "0.5";
const client = new Client({ name: "ekubo-quote-comparison", version: "1.0.0" });
await client.connect(
new StreamableHTTPClientTransport(new URL("https://mcp.ekubo.org/mcp/ekubo")),
);
async function call(name, args) {
const result = await client.callTool({ name, arguments: args });
if (result.isError) throw new Error(`${name}: ${result.content?.[0]?.text}`);
return result.structuredContent;
}
async function resolve(symbol) {
const { tokens } = await call("list_tokens", {
chain_id: CHAIN_ID,
search: symbol,
page_size: 20,
});
const token = tokens.find(
(t) => t.symbol.toLowerCase() === symbol.toLowerCase(),
);
if (!token) throw new Error(`no exact ${symbol} on chain ${CHAIN_ID}`);
return token;
}
function toBaseUnits(amount, decimals) {
const [whole, fraction = ""] = amount.split(".");
if (fraction.length > decimals) throw new Error("too many decimal places");
return BigInt(whole + fraction.padEnd(decimals, "0")).toString();
}
function fromBaseUnits(value, decimals) {
const digits = value.padStart(decimals + 1, "0");
return `${digits.slice(0, -decimals)}.${digits.slice(-decimals)}`;
}
const sell = await resolve(SELL);
const buy = await resolve(BUY);
for (const t of [sell, buy]) {
console.log(
`${t.symbol.padEnd(5)} chain ${t.chain_id} ${t.address} decimals ${t.decimals}`,
);
}
const amount = toBaseUnits(AMOUNT, sell.decimals);
console.log(`\nexact_input ${AMOUNT} ${sell.symbol} = ${amount} base units\n`);
const result = await call("get_quotes_with_plans", {
chain_id: CHAIN_ID,
token_in: sell.address,
token_out: buy.address,
quote_type: "exact_input",
amount,
});
for (const { source, normalized: q, jurisdiction } of result.quotes) {
console.log(
[
source.padEnd(6),
`amount_out ${fromBaseUnits(q.amount_out, buy.decimals)} ${buy.symbol}`,
`estimated_gas ${q.estimated_gas ?? "not reported"}`,
`execution_hold ${jurisdiction.execution_hold}`,
].join(" "),
);
}
console.log(
"\nunavailable_sources",
JSON.stringify(result.unavailable_sources),
);
console.log("comparison_complete", result.comparison.comparison_complete);
console.log(
"execution_plans_included",
result.execution.execution_plans_included,
);
await client.close();
```
Run it:
```sh
node compare-quotes.mjs
```
## Expected output
[Section titled “Expected output”](#expected-output)
This is the output observed at 15:49 UTC on 24 October 2026. Token metadata should match exactly; the amounts and gas move with the market on every run.
```text
ETH chain 1 0x0000000000000000000000000000000000000000 decimals 18
USDC chain 1 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 decimals 6
exact_input 0.5 ETH = 500000000000000000 base units
ekubo amount_out 1143.880660 USDC estimated_gas 111459 execution_hold false
0x amount_out 1147.935609 USDC estimated_gas 246604 execution_hold false
unavailable_sources []
comparison_complete true
execution_plans_included false
```
In this run 0x quoted the larger output and the higher gas estimate. Which source quotes more depends on the pair, the size, and the block, so do not assume either way; read the numbers each time.
## What each step is guarding against
[Section titled “What each step is guarding against”](#what-each-step-is-guarding-against)
### Token identity
[Section titled “Token identity”](#token-identity)
`list_tokens` matches a symbol prefix or suffix, so a search for `USDC` also returns tokens such as `SPUSDC` and `REUSDC`, and a search for `ETH` returns `cbETH` and `rETH`. The script accepts only an exact symbol match and takes the first one, because results are ordered by descending `visibility_priority`. Show the user the chosen chain ID and address before quoting, and look up an address the user supplied with `get_token` or `get_tokens` rather than by symbol.
A symbol does not identify a token across chains: USDC on Base is a different contract from USDC on Ethereum. Resolve each token on the chain you will quote on. On EVM chains the all-zero address denotes the chain’s native token.
### Units
[Section titled “Units”](#units)
`amount` is an integer string in base units, so 0.5 ETH with 18 decimals is `500000000000000000`. Convert with string or integer arithmetic, never floating point: in JavaScript, `BigInt(1.1 * 10 ** 18)` is `1100000000000000128n`, not `1100000000000000000n`. Read `decimals` from the token record instead of assuming 18; USDC uses 6.
### Gross and net output
[Section titled “Gross and net output”](#gross-and-net-output)
`amount_out` is each source’s reported output before the gas the sender pays. A gross comparison is what the server reports in `comparison.comparison_basis` (`highest_calculated_amount_out`), and it is not the whole cost. To compare net outcomes, value each option as `amount_out` minus `estimated_gas × gas price` converted to the output token, using a current gas price from your own RPC.
That is only possible when a source reports gas. Both sources reported `estimated_gas` in the run above, but a source can return it as `null`; treat that as unknown, not as zero, and say so rather than ranking on half the data. A wallet simulation of the chosen plan gives a fresher gas estimate; only the transaction receipt establishes the gas actually used. For cross-chain options, LayerZero charges `native_fee` outside `amount_out`, so add it to that option’s cost.
`price_impact` is reported by some sources and not others. It is a provider-computed figure, not a comparable cost, so it should not decide between sources on its own.
### Unavailable sources
[Section titled “Unavailable sources”](#unavailable-sources)
If a provider fails, it is listed in `unavailable_sources` and the others still return. `comparison_complete` is then `false` and `retry_instruction` says what to do. Tell the user the comparison is incomplete instead of presenting the remaining quote as the best available.
### Jurisdiction metadata
[Section titled “Jurisdiction metadata”](#jurisdiction-metadata)
Every quote carries a `jurisdiction` object. The script prints its `execution_hold` flag; when that flag is `true`, or `coverage` is `unknown`, the quote must not proceed to execution. The full set of conditions, including what the agent must establish from the user before any signature, is defined by the live [`get_quotes_with_plans` tool description](https://mcp.ekubo.org/tools?protocol=ekubo) and the server’s `ekubo://docs/quoter-api` resource. Read them there; this page does not restate them.
## Continuing to execution
[Section titled “Continuing to execution”](#continuing-to-execution)
Execution is a separate, deliberate step, and this guide does not run it. When the user has decided to swap, the documented continuation is:
1. Establish the user’s eligibility as the tool description requires, and stop if any quote carries an execution hold.
2. Bind the actual signing account first, then call `get_quotes_with_plans` once more with that account as `sender` and a `slippage_bps` sized by the rule in that parameter’s live schema. Each option now carries `execution.execution_plan_reference`.
3. Pass the chosen option’s `execution_plan_reference` to the wallet unchanged. The wallet fetches the plan body, verifies its integrity, simulates it, applies its policy, and asks for authorization.
4. Re-quote only after an expiry, a revert, or a change to the request. Re-quoting to refresh a plan you already hold replaces the quote the user approved.
The rules for that handoff are in [The execution plan boundary](/products/mcp-server/#the-execution-plan-boundary) and, authoritatively, in the server’s `ekubo://docs/execution-plan` resource. You can read it with the same client:
```js
const { contents } = await client.readResource({
uri: "ekubo://docs/execution-plan",
});
console.log(contents[0].text);
```
[Build a plan producer](/wallet/producers/) describes what the wallet does with the reference.
## Troubleshooting
[Section titled “Troubleshooting”](#troubleshooting)
| Symptom | Cause and fix |
| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `no exact XYZ on chain N` | The symbol is not in the canonical list for that chain at the default visibility threshold. Look the token up by address with `get_token`, or pass a negative `min_visibility_priority` to `list_tokens`. |
| `amount must be a positive base-unit integer` | `amount` was passed in human units. Convert it to an integer string in base units first. |
| HTTP `429` | A rate limit was hit. Wait for the `Retry-After` seconds; `data.scope` names the budget. See [Rate limits](/products/mcp-server/#rate-limits). |
| `comparison_complete` is `false` | A provider failed. Read `unavailable_sources` and `retry_instruction`. |
| `estimated_gas` is `null` | That source did not report gas in its normalized quote. Its net cost is unknown from this response. |
| `execution_plans_included` is `false` | Expected for an indicative request. Plans are only built when `sender` and `slippage_bps` are both supplied. |
| `SyntaxError: Cannot use import statement outside a module` | Save the file with the `.mjs` extension, or set `"type": "module"` in `package.json`. |
| `ERR_PACKAGE_PATH_NOT_EXPORTED` or a missing transport export | A different major version of the SDK is installed. Install the exact version above. |
## Limitations
[Section titled “Limitations”](#limitations)
* Indicative quotes are not firm. They are recomputed on every call and are not reserved, and the planned quote returned later is a new quote.
* The server returns every source it obtained and selects none. It does not assess execution quality beyond the reported amounts.
* Only the sources the server queries are compared. A venue it does not call is not represented.
* An agent connected to the server sees the same tools, but its own model decides how to present the results. The checks above are what to verify in its answer.
# Migrating from Uniswap v3
> Map a Uniswap v3 integration onto Ekubo on EVM chains: pool identity, ticks and prices, fees, swapping and quoting, extensions, deployments, and licensing
This guide is for developers who already read from or route through Uniswap v3 and want to add Ekubo, or move to it, on an EVM chain. It maps each Uniswap v3 concept to its Ekubo counterpart, then reads the same ETH/USDC market from both protocols with one script so you can check the mapping against live state.
The two protocols are not interchangeable at the code level. Pools are identified differently, prices and fees are encoded differently, and the SDKs have different surfaces. Plan for a new adapter rather than a configuration change.
## Scope
[Section titled “Scope”](#scope)
| Item | Covered here |
| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Ekubo | Ekubo V3 on EVM chains. Types and lens contracts are cited from [`evm-contracts` v3.4.0](https://github.com/EkuboProtocol/evm-contracts/tree/v3.4.0); Core, the pool types, and `CoreDataFetcher` are unchanged from the `v3.2.0` deployment listed in [EVM Contracts (V3)](/reference/contracts/evm-v3/). Starknet differs in price and fee encoding (see [Price representation](/reference/price-representation/)) and is not covered. |
| Uniswap | Uniswap v3, cited from [`v3-core` at `5ea68a7`](https://github.com/Uniswap/v3-core/tree/5ea68a77cb541c6b3812df783be71ba8c081e25f) and [`v3-periphery`](https://github.com/Uniswap/v3-periphery). Uniswap v4 is a different design and is not covered. |
| Example | Ethereum mainnet, read-only. No transaction is sent. |
## Concept mapping
[Section titled “Concept mapping”](#concept-mapping)
| Concept | Uniswap v3 | Ekubo V3 on EVM |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Pool identity | One contract per `(token0, token1, fee)`, found with `UniswapV3Factory.getPool` | No pool contracts. Every pool lives in the [Core](/concepts/architecture/) singleton and is identified by a `PoolKey` of `token0`, `token1`, and a packed `bytes32 config`. The pool ID is `keccak256(abi.encode(poolKey))`. |
| Native ETH | Not supported in pools; WETH is used | Native ETH is `address(0)`, so it is always `token0`. WETH pools are separate pools. |
| Price | `slot0().sqrtPriceX96`, Q64.96 | `CoreDataFetcher.poolPrice(poolKey)` returns a 64.128 fixed-point sqrt ratio. Core stores a packed 96-bit `SqrtRatio`; see [Price representation](/reference/price-representation/). |
| Tick | Price `1.0001^tick`, range ±887,272 | Price `1.000001^tick`, range ±88,722,835 |
| Tick spacing | Fixed per fee tier by the factory: 100 → 1, 500 → 10, 3000 → 60, 10000 → 200 on Ethereum | A free parameter of each concentrated pool, up to 698,605. Stableswap and full-range pools have no tick spacing. |
| Swap fee | `uint24` in millionths: `500` is 0.05% | `uint64` fraction of `2^64`: 0.05% is `9223372036854775` or `9223372036854776`, depending on rounding. Each value is a different pool. |
| Protocol fee | Per pool, `slot0().feeProtocol`: a share `1/N` of each swap fee, taken at swap time. The factory owner can change it with `setFeeProtocol` | Core takes none. A Positions contract takes `SWAP_PROTOCOL_FEE_X64 / 2^64` of the fees it collects. The value is an `immutable` set at deployment, so it differs only between manager deployments; read it from the manager you use. |
| Swap entry | `pool.swap(...)` with `uniswapV3SwapCallback`, or a router | `Core.lock()`, then swaps inside the lock callback, settling net balances once at the end. Production swaps use the [Yul Router](/integration-guides/yul-router/); see [Swapping](/integration-guides/swapping/). |
| Quoting | `QuoterV2`, called with `eth_call` | The [Quoter API](/api/#quoter) returns block-pinned split routes over HTTP. On-chain, the Yul Router’s `quote(bytes)` evaluates a route with `eth_call`. The [Rust SDK](/integration-guides/sdks/) quotes locally. |
| Hooks | None | A pool’s [extension](/concepts/extensions/) is part of its key and can run at eight call points. Some, such as MEVCapture, must be swapped through `Core.forward` rather than `Core.swap`. |
| LP positions | `NonfungiblePositionManager` ERC-721 | `Positions` ERC-721, with a manager generation that varies by chain; see [Position and order managers](/reference/contracts/evm-v3/#position-and-order-managers). |
| Price oracle | `observe()` on every pool | Only pools using the Oracle extension record history, and only against native ETH. |
| SDK | [`@uniswap/v3-sdk`](https://docs.uniswap.org/sdk/v3/overview): `Pool`, `Route`, `Trade`, position helpers | [`@ekubo/sdk`](/integration-guides/sdks/) for math and pool-key encoding, `@ekubo/yul-router-sdk` for route encoding, and the Rust `ekubo_sdk` for quoting. There is no `Trade` equivalent. |
| Deployments | Per-chain factory and periphery addresses; see [Uniswap’s deployment list](https://docs.uniswap.org/contracts/v3/reference/deployments/) | Core and lens contracts share one address on every supported chain; see [EVM Contracts (V3)](/reference/contracts/evm-v3/) for chains and the per-chain manager addresses. |
| License | `v3-core`: BUSL-1.1 until a change date of 2023-04-01 at the latest, GPL-2.0-or-later after it. `v3-periphery`: GPL-2.0-or-later | [Ekubo DAO Shared Revenue License 1.0](https://github.com/EkuboProtocol/evm-contracts/blob/v3.4.0/LICENSE). Read section 7 before shipping a contract that is derived from, or incorporates, the contracts or their ABI. |
### Converting a tick
[Section titled “Converting a tick”](#converting-a-tick)
The two tick bases differ, so a Uniswap tick does not map to a whole Ekubo tick. For the same token order, `ekuboTick ≈ uniswapTick × ln(1.0001) / ln(1.000001)`, a factor of about 99.99505. When native ETH replaces WETH, the token order can flip, as it does for USDC: USDC is `token0` of the Uniswap pool and `token1` of the Ekubo pool, so the sign flips too. In the example below, Uniswap tick `198935` corresponds to about Ekubo tick `-19892515`.
Converting through the price is safer than converting ticks, because a position bound must also be a multiple of the target pool’s tick spacing. Round each bound to that spacing and show the user the resulting price range: it will not equal the original range exactly.
## Run the example
[Section titled “Run the example”](#run-the-example)
The script reads the Uniswap v3 USDC/WETH 0.05% pool and the Ekubo ETH/USDC pool with a 0.05% fee, a tick spacing of 4988, and no extension, at the same block. It derives the Ekubo pool ID locally with `@ekubo/sdk`.
### Prerequisites
[Section titled “Prerequisites”](#prerequisites)
* Node.js 20 or later. The example was run with Node.js 22.23.4 in a fresh `node:22-slim` container.
* An Ethereum mainnet RPC URL in `RPC_URL`. Without one, the script uses the public `https://ethereum-rpc.publicnode.com` endpoint, which can rate-limit.
```sh
npm init -y
npm install viem@2.57.11 @ekubo/sdk@0.0.10
```
`@ekubo/sdk` is an alpha release, so pin the exact version.
Save this as `compare-pools.mjs`:
```js
import {
createPublicClient,
http,
keccak256,
parseAbi,
zeroAddress,
} from "viem";
import { mainnet } from "viem/chains";
import {
decodeEvmPoolConfig,
deriveEvmPoolId,
encodeEvmConcentratedPoolConfig,
} from "@ekubo/sdk";
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL ?? "https://ethereum-rpc.publicnode.com"),
});
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const UNISWAP_V3_FACTORY = "0x1F98431c8aD98523631AE4a59f267346ea31F984";
const EKUBO_CORE_DATA_FETCHER = "0xF68F25CA6C817733b7B15a42191AE72A34d56a2B";
const EKUBO_POSITIONS = "0x02D9876A21AF7545f8632C3af76eC90b5ad4b66D";
const blockNumber = await client.getBlockNumber();
const read = (args) => client.readContract({ ...args, blockNumber });
// USDC per ETH, to 2 decimals, from a sqrt ratio with `shift` fractional bits.
function usdcPerEth(sqrtRatio, shift, ethIsToken0) {
const ratio = sqrtRatio * sqrtRatio;
const one = 1n << (2n * shift);
const scaled = ethIsToken0
? (ratio * 10n ** 14n) / one
: (one * 10n ** 14n) / ratio;
return `${scaled / 100n}.${(scaled % 100n).toString().padStart(2, "0")}`;
}
const uniPool = await read({
address: UNISWAP_V3_FACTORY,
abi: parseAbi([
"function getPool(address,address,uint24) view returns (address)",
]),
functionName: "getPool",
args: [USDC, WETH, 500],
});
const uniAbi = parseAbi([
"function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16, uint16, uint16, uint8 feeProtocol, bool)",
"function liquidity() view returns (uint128)",
"function fee() view returns (uint24)",
"function tickSpacing() view returns (int24)",
"function token0() view returns (address)",
]);
const [slot0, uniLiquidity, uniFee, uniSpacing, uniToken0] = await Promise.all(
["slot0", "liquidity", "fee", "tickSpacing", "token0"].map((functionName) =>
read({ address: uniPool, abi: uniAbi, functionName }),
),
);
const [sqrtPriceX96, uniTick, , , , feeProtocol] = slot0;
const poolKey = {
token0: zeroAddress,
token1: USDC,
config: encodeEvmConcentratedPoolConfig({
fee: 9223372036854776n,
tickSpacing: 4988,
extension: zeroAddress,
}),
};
const poolKeyAbi = "(address token0, address token1, bytes32 config)";
const [sqrtRatioFixed, ekuboTick] = await read({
address: EKUBO_CORE_DATA_FETCHER,
abi: parseAbi([
`function poolPrice(${poolKeyAbi}) view returns (uint256, int32)`,
]),
functionName: "poolPrice",
args: [poolKey],
});
const [, , ekuboLiquidity] = await read({
address: EKUBO_CORE_DATA_FETCHER,
abi: parseAbi([
`function poolState(${poolKeyAbi}) view returns (uint96, int32, uint128)`,
]),
functionName: "poolState",
args: [poolKey],
});
const swapProtocolFeeX64 = await read({
address: EKUBO_POSITIONS,
abi: parseAbi(["function SWAP_PROTOCOL_FEE_X64() view returns (uint64)"]),
functionName: "SWAP_PROTOCOL_FEE_X64",
});
const config = decodeEvmPoolConfig(poolKey.config);
console.log(`Ethereum block ${blockNumber}\n`);
console.log("Uniswap v3 USDC/WETH");
console.log(` pool address ${uniPool}`);
console.log(` token0 ${uniToken0}`);
console.log(` fee ${uniFee} / 1e6`);
console.log(` tick spacing ${uniSpacing}`);
console.log(` sqrtPriceX96 ${sqrtPriceX96}`);
console.log(` tick ${uniTick}`);
console.log(` liquidity ${uniLiquidity}`);
console.log(
` feeProtocol ${feeProtocol % 16} (token0), ${feeProtocol >> 4} (token1)`,
);
console.log(` USDC per ETH ${usdcPerEth(sqrtPriceX96, 96n, false)}\n`);
console.log("Ekubo ETH/USDC");
console.log(` pool id ${deriveEvmPoolId(poolKey, keccak256)}`);
console.log(` token0 ${poolKey.token0}`);
console.log(` config ${poolKey.config}`);
console.log(` fee ${config.fee} / 2^64`);
console.log(` tick spacing ${config.tickSpacing}`);
console.log(` extension ${config.extension}`);
console.log(` sqrtRatio 64.128 ${sqrtRatioFixed}`);
console.log(` tick ${ekuboTick}`);
console.log(` liquidity ${ekuboLiquidity}`);
console.log(` Positions swap protocol fee ${swapProtocolFeeX64} / 2^64`);
console.log(` USDC per ETH ${usdcPerEth(sqrtRatioFixed, 128n, true)}`);
```
Run it:
```sh
node compare-pools.mjs
```
### Expected output
[Section titled “Expected output”](#expected-output)
Observed at block 26263507 (15:48:59 UTC on 24 October 2026). The addresses, pool ID, config, fee, tick spacing, and Positions swap protocol fee should match exactly. Uniswap’s `feeProtocol` is the value at that block and can change. Prices, ticks, and liquidity change with every block.
```text
Ethereum block 26263507
Uniswap v3 USDC/WETH
pool address 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640
token0 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
fee 500 / 1e6
tick spacing 10
sqrtPriceX96 1653817353788712817544262592047087
tick 198935
liquidity 1847104564058872996
feeProtocol 4 (token0), 4 (token1)
USDC per ETH 2295.00
Ekubo ETH/USDC
pool id 0xde39735c66c6f51669c80e1fde362e42a1b1b927d49dee8d6577765a4e4d38c6
token0 0x0000000000000000000000000000000000000000
config 0x00000000000000000000000000000000000000000020c49ba5e353f88000137c
fee 9223372036854776 / 2^64
tick spacing 4988
extension 0x0000000000000000000000000000000000000000
sqrtRatio 64.128 16300619316558869431231032374329344
tick -19892666
liquidity 8069515514150211
Positions swap protocol fee 1844674407370955161 / 2^64
USDC per ETH 2294.71
```
### Reading the output
[Section titled “Reading the output”](#reading-the-output)
The two prices agree to within a few hundredths of a percent, which confirms the conversions: Uniswap’s price is WETH per USDC in Q64.96 and has to be inverted, while Ekubo’s is USDC per ETH in 64.128.
`feeProtocol` of `4` means a quarter of this Uniswap pool’s swap fees went to the protocol at that block. It is mutable: the Uniswap v3 factory owner can change it per pool with `setFeeProtocol`, so read it at the block you care about.
`1844674407370955161 / 2^64` is 10%: the share the canonical Ethereum Positions contract takes from fees it collects for Ekubo liquidity providers. In [`Positions.sol`](https://github.com/EkuboProtocol/evm-contracts/blob/v3.4.0/src/Positions.sol) it is an `immutable` assigned in the constructor, so it cannot change for this deployment. A different Positions address can have been deployed with a different value, which is why the script reads it from the manager it targets rather than hard-coding it.
Active liquidity is in the same units in both protocols, but one pool’s liquidity says little about execution. Ekubo has several ETH/USDC pools with different fees, tick spacings, and extensions, and the Quoter API splits across them. Compare routed quotes, not single pools, when deciding where to send a trade.
`config` packs three fields into one word: the extension address in the top 160 bits, the fee in the next 64, and a 32-bit pool-type field. Here that field is `0x8000137c`: the high bit marks a concentrated pool and `0x137c` is the tick spacing 4988. [`poolConfig.sol`](https://github.com/EkuboProtocol/evm-contracts/blob/v3.4.0/src/types/poolConfig.sol) defines the layout; `decodeEvmPoolConfig` decodes it.
## Get a quote
[Section titled “Get a quote”](#get-a-quote)
Uniswap integrations usually quote through `QuoterV2`. The closest Ekubo equivalent for off-chain routing is the [Quoter API](/api/#quoter), which needs no key:
```sh
curl -s https://prod-api-quoter.ekubo.org/1/1000000000000000000/0x0000000000000000000000000000000000000000/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
| jq '{block_number, total_calculated, estimated_gas_cost, splits: (.splits | length), first_split: (.splits[0] | {amount_specified, amount_calculated, pool_config: .route[0].swap.pool_key.config})}'
```
Observed at 15:49 UTC on 24 October 2026:
```json
{
"block_number": 26263508,
"total_calculated": "2282386247",
"estimated_gas_cost": 206609,
"splits": 6,
"first_split": {
"amount_specified": "750000000000000000",
"amount_calculated": "1712557071",
"pool_config": "0x00000000000000000000000000000000000000000020c49ba5e353f88000137c"
}
}
```
The path is `/{chainId}/{amount}/{specifiedToken}/{otherToken}` with amounts in base units. This request sells 1 ETH. In the observed response, 75% of it routed through the pool the script read; the split count and shares change from block to block. For exact output, make the amount negative and specify the output token: `/1/-2000000000/{USDC}/{ETH}` asks what it costs to receive 2,000 USDC, and `total_calculated` comes back negative because it is an input.
A quote is pinned to `block_number`. To execute it, encode its splits with `@ekubo/yul-router-sdk` as described in [Preparing a swap from the Quoter API](/integration-guides/yul-router/#preparing-a-swap-from-the-quoter-api). The same guide shows how to re-check a route on-chain with `quote(bytes)`.
## Liquidity positions do not migrate atomically
[Section titled “Liquidity positions do not migrate atomically”](#liquidity-positions-do-not-migrate-atomically)
There is no contract that converts a Uniswap v3 position into an Ekubo position. Moving liquidity is two independent operations: remove liquidity and collect fees from the Uniswap `NonfungiblePositionManager`, then deposit into an Ekubo pool through Positions. The price can move between them, the range has to be re-expressed in Ekubo ticks rounded to the new pool’s spacing, and the amounts deposited will depend on the price at deposit time.
Choose the target pool deliberately. A fee tier in Uniswap maps to more than one possible Ekubo pool, and a pool with an extension behaves differently from one without. Narrower ranges earn more fees per dollar while the price is inside them and suffer more divergence loss and time out of range when it is not; moving protocols does not change that trade-off.
## When to keep Uniswap v3
[Section titled “When to keep Uniswap v3”](#when-to-keep-uniswap-v3)
Adding Ekubo does not require removing Uniswap. Keep the existing integration, alone or alongside Ekubo, when any of these hold:
* The chain you need is not in Ekubo’s [supported chains](/reference/contracts/evm-v3/#supported-chains), or is listed as awaiting EIP-7939.
* Routed Ekubo quotes for your pairs and sizes are consistently worse than what you get today. Compare at the sizes you actually trade.
* Your product depends on Uniswap-specific surfaces: `observe()` on arbitrary pairs, Uniswap position NFTs accepted by other protocols, or tooling built on `@uniswap/v3-sdk` types.
* Your contracts would be derived from or incorporate Ekubo’s contracts or ABI, and the [license](https://github.com/EkuboProtocol/evm-contracts/blob/v3.4.0/LICENSE) terms do not suit you. This page does not interpret the license; read it with your own counsel.
For aggregators and solvers, adding Ekubo as an additional source is the usual pattern. The [Aggregators](/integration-guides/aggregators/) guide and its reference implementations show how others did it.
## Troubleshooting
[Section titled “Troubleshooting”](#troubleshooting)
| Symptom | Cause and fix |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Ekubo price and liquidity are all zero | The pool key does not match an initialized pool. Every field counts: WETH instead of `address(0)`, a fee off by one unit, a different tick spacing, or a different extension each name another pool. Find initialized keys with the Quoter API response or the MCP server’s `list_pool_keys`. |
| Revert with `TokensMustBeSorted()` | `token0` must be the numerically smaller address. Core rejects an unsorted key when a pool is initialized; a read with an unsorted key just returns zero state. With native ETH, `address(0)` is always `token0`. |
| The Ekubo price is off by a factor of `10^12` | The decimal adjustment was skipped or inverted. The sqrt ratio is for `token1` per `token0` in base units, so scale by `10^(decimals0 - decimals1)` for a human price. |
| The prices differ by much more than a fraction of a percent | The two reads used different blocks, or one pool has little active liquidity. Pin both reads to one block, as the script does. |
| HTTP `429` from the RPC | The public endpoint rate-limits. Set `RPC_URL` to your own provider. |
| Swaps revert on a chain that has Core deployed | Some chains still lack the `CLZ` opcode; see [Supported chains](/reference/contracts/evm-v3/#supported-chains). |
## Differences this guide does not cover
[Section titled “Differences this guide does not cover”](#differences-this-guide-does-not-cover)
This guide is the minimum needed to read state and get a quote. A full migration also has to deal with these, each of which is its own piece of work:
* Executing swaps from your own contract inside a Core lock, including settlement and native-ETH payment; see [Swapping](/integration-guides/swapping/).
* Stableswap and full-range pools, which have no tick spacing and use different liquidity math.
* Pools with extensions, including forwarded hops for MEVCapture and Ve33, and quoting their behavior off-chain; see [Aggregators](/integration-guides/aggregators/#note-on-extensions).
* Indexing: Ekubo emits events from Core rather than from per-pool contracts, so pool discovery and state tracking differ. The open-source [indexer](/products/indexer/) is a working reference.
* TWAMM orders, the Oracle extension’s interface, boosted fees, and signed exclusive swaps, none of which have a Uniswap v3 equivalent.
* Position management through Positions: minting, ID generation, fee collection, and the per-chain manager generations.
* Starknet, where prices and fees are encoded differently.
# Reading pool price
> How to interpret pool data from the on-chain methods
How to read a pool’s current price on-chain and convert it to a human-readable number. For how prices are encoded (fixed point on Starknet, the compact `SqrtRatio` float type on EVM), see [Price representation](/reference/price-representation/).
## EVM (V3)
[Section titled “EVM (V3)”](#evm-v3)
The simplest way to read a pool price on EVM chains is the `CoreDataFetcher` lens contract (deployed at the same address on every chain — see [EVM Contracts (V3)](/reference/contracts/evm-v3/)):
```solidity
// returns the sqrt ratio expanded to 64.128 fixed point, plus the current tick
(uint256 sqrtRatioFixed, int32 tick) = coreDataFetcher.poolPrice(poolKey);
```
The returned value is a 64.128 fixed-point number — identical semantics to Starknet’s `sqrt_ratio` below, so the conversion to a human-readable price is the same: divide by `2**128`, square, and adjust for token decimals. (If you read Core’s packed pool state directly instead, the price is a 96-bit `SqrtRatio`; call `toFixed()` on it or see [Price representation](/reference/price-representation/) for the bit layout.)
Note that on EVM, native ETH is represented as `address(0)`, so ETH is always `token0` of any pool it is in.
## Starknet
[Section titled “Starknet”](#starknet)
Let’s say you wanted to determine the human-readable price of the ETH-USDC pool on Starknet mainnet.
These two tokens are:
* **ETH**: `0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7` ([Voyager](https://voyager.online/contract/0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7))
* **USDC**: `0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8` ([Voyager](https://voyager.online/contract/0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8))
First, let’s determine which token is `token0` and which token is `token1`. We can do this by comparing the integer values of the addresses. In this case, `0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8 > 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7`, so USDC is `token1` and ETH is `token0`.
Next, we need to decide which pool to read. Let’s use the pool with the 0.05% fee and the 0.1% tick spacing, since it’s the most popular ETH/USDC pool. The easiest way to get the fee and tick spacing parameters is to [read the URL](https://ekubo.org/positions/new?baseCurrency=ETH\"eCurrency=USDC\&fee=170141183460469235273462165868118016\&tickSpacing=1000\&step=1\&tickLower=-20104000\&tickUpper=-20064000\&initialTick=-20083671) from the website when you add liquidity to this pool:
```plaintext
https://ekubo.org/positions/new?baseCurrency=ETH"eCurrency=USDC&fee=170141183460469235273462165868118016&tickSpacing=1000
```
The second easiest way is to compute the value. Fee is a 0.128 fixed point number, so to compute the fee, we can do [`floor(0.05% * 2**128)`](https://www.wolframalpha.com/input?i=floor%280.05%25*2**128%29). The result is `170141183460469235273462165868118016`. The tick spacing of `0.01%` is represented as an exponent of `1.000001`, so it can be computed as [`log base 1.000001 of 1.001`](https://www.wolframalpha.com/input?i=log+base+1.000001+of+1.001), which is roughly equal to `1000`. The extension is `0` because it is not used for this pool.
Input the values into the [Core](/reference/contracts/evm-v2/) contract on Voyager to read the pool price. If you’re following along, you’ll get a value that looks like this:
```json
{
"sqrt_ratio": "0x029895c9cbfca44f2c46e6e9b5459b",
"tick": {
"mag": "0x0135566d",
"sign": "0x01"
},
"call_points": {
"after_initialize_pool": "0x00",
"before_swap": "0x00",
"after_swap": "0x00",
"before_update_position": "0x00",
"after_update_position": "0x00"
}
}
```
Let’s compute the price from this result. The value `sqrt_ratio` is a `64.128` fixed point number. To convert it to a price, first divide it by `2**128`, then square it to get the price. Since USDC is `token1`, this value is the price of the pool in USDC/ETH. `(0x029895c9cbfca44f2c46e6e9b5459b / 2**128)**2 == 1.56914... ×10^-9`. To adjust for display, we have to account for the decimal difference between the USDC and ETH tokens. Because USDC has 6 decimals and ETH has 18 decimals, we need to scale it up by `10**(18-6)` to be human readable. `1.56914e-9 * 1e12 == 1.56914e3 == 1569.14 USDC/ETH`.
# SDKs
> Libraries for computing Ekubo quotes, working with pool math, and encoding swap routes
Ekubo publishes three libraries. All of them implement the same protocol math, so quotes computed off-chain match on-chain execution exactly.
| Package | Language | Use it for |
| ------------------------------------------------------------------------------ | ---------- | ------------------------------------------------------------ |
| [`ekubo_sdk`](https://crates.io/crates/ekubo_sdk) | Rust | Computing quotes and simulating swaps across every pool type |
| [`@ekubo/sdk`](https://www.npmjs.com/package/@ekubo/sdk) | TypeScript | Pool math, tick and price conversions, pool key encoding |
| [`@ekubo/yul-router-sdk`](https://www.npmjs.com/package/@ekubo/yul-router-sdk) | TypeScript | Encoding swap routes for execution on EVM chains |
All three cover both Starknet and EVM deployments where the underlying math is shared.
## Rust: `ekubo_sdk`
[Section titled “Rust: ekubo\_sdk”](#rust-ekubo_sdk)
The [Rust SDK](https://github.com/EkuboProtocol/rust-sdk) is the most complete implementation — it is what powers Ekubo’s own routing. It exists primarily to **compute quotes** from Ekubo pools, plus the supporting protocol math.
```toml
[dependencies]
ekubo_sdk = { version = "3.2", features = ["evm"] }
```
The crate is `no_std`-compatible, which makes it usable in constrained environments. Select what you need with feature flags:
| Feature | Effect |
| -------------------------- | --------------------------------------------------------------------------- |
| `std` (default) / `no_std` | Standard library or `libm`-backed math; exactly one must be enabled |
| `evm` | EVM types via `alloy-primitives` (v1 by default; `evm-alloy-0_6` pins v0.6) |
| `starknet` | Starknet felt types via `starknet-types-core` |
| `serde` | Serialization for the protocol types |
Three modules:
* **`math`** — tick and sqrt-ratio conversions, swap step computation, deltas, fixed-point helpers, and TWAMM math
* **`quoting`** — the quoting engine: a `Pool` trait with a `quote` method, `QuoteParams` / `Quote` / `TokenAmount` types, and `PoolKey` / `PoolConfig` / `Tick` definitions
* **`chain`** — the EVM and Starknet type mappings
The `quoting::pools` module implements every pool type and extension so that off-chain quotes account for extension behavior: `concentrated`, `full_range`, `stableswap`, `limit_order`, `mev_capture`, `twamm`, `oracle`, `ve33`, `spline`, and `boosted_fees` variants. This is the practical reason to prefer the Rust SDK for routing — replicating extension behavior by hand is where most integrations go wrong (see [Aggregators](/integration-guides/aggregators/)).
## TypeScript: `@ekubo/sdk`
[Section titled “TypeScript: @ekubo/sdk”](#typescript-ekubosdk)
```sh
npm install @ekubo/sdk
```
Shared math and protocol encoding utilities, with **no runtime dependencies**. It covers the pieces most integrations need in a browser or Node environment:
* **Tick and price math** — conversions between ticks, sqrt ratios, and prices for both chains (see [Price representation](/reference/price-representation/))
* **Liquidity math** — `maxLiquidityForTokenAmounts`, `liquidityToAmountBase` / `liquidityToAmountQuote`, `amountsFromSpecifiedAmount`, and the `amount0Delta` / `amount1Delta` primitives for sizing positions
* **Swap math** — `computeStep`, `computeFee`, `amountBeforeFee`, `nextSqrtRatioFromAmount0` / `nextSqrtRatioFromAmount1`
* **TWAMM math** — `calculateNextSqrtRatio` for [DCA](/user-guides/dollar-cost-average-orders/) pools
* **Pool key and protocol encoding** for EVM
* **Chain constants** — `EVM_MIN_TICK` / `EVM_MAX_TICK` (±88,722,835), `EVM_MAX_TICK_SPACING`, `EVM_MIN_SQRT_RATIO` / `EVM_MAX_SQRT_RATIO`, and the Starknet equivalents (±88,722,883)
Note
`@ekubo/sdk` is published as an alpha. Pin an exact version, and expect the surface to change before a stable release.
## TypeScript: `@ekubo/yul-router-sdk`
[Section titled “TypeScript: @ekubo/yul-router-sdk”](#typescript-ekuboyul-router-sdk)
```sh
npm install @ekubo/yul-router-sdk
```
Encodes calldata for the [Yul Router](/integration-guides/yul-router/), the router used for Ekubo swaps on EVM chains. Use it to turn a route — typically one returned by the [Quoter API](/api/#quoter) — into a transaction. See the [Yul Router guide](/integration-guides/yul-router/) for the full surface and worked examples.
# Signed exclusive swaps
> RFQ-style exclusive liquidity on Ekubo: an extension where a controller signs each swap off-chain with its own fee and bounds
`SignedExclusiveSwap` is an [extension](/concepts/extensions/) that makes a pool tradeable **only** through off-chain signed quotes. A designated *controller* signs each swap, setting a per-swap fee and bounds on the result. This gives market makers and aggregators an RFQ-style venue that still settles in Ekubo Core, reusing its liquidity math, routing, and [flash accounting](/concepts/architecture/).
The extension is forward-only: direct swaps against the pool revert. Every swap must arrive through `Core.forward(...)` carrying a valid signature.
Source: [`SignedExclusiveSwap.sol`](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/extensions/SignedExclusiveSwap.sol) · [integration guide](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/signed-exclusive-swap-extension.md) · [scoped review](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/audits/SignedExclusiveSwap-Extension-Audit.md)
## What the extension enforces
[Section titled “What the extension enforces”](#what-the-extension-enforces)
* Direct swaps are blocked; swaps must be forwarded with a signed payload
* Signatures are one-time-use, via a nonce bitmap — with one reserved exception (see [Replay protection](#replay-protection-and-nonce-management))
* A signature can optionally restrict which locker may use it
* The pool’s own fee must be zero — all fee logic lives in the signature
* Pools must be initialized through the extension’s owner-only `initializePool(...)`
* Signed fees are collected by the extension and donated to LPs on the next block touch
## The signed payload
[Section titled “The signed payload”](#the-signed-payload)
A forward call decodes five values: the `poolKey`, the swap `params`, a `meta` word, a `minBalanceUpdate` word, and the `signature`.
`SignedSwapMeta` packs four fields into one 256-bit word:
| Field | Width | Meaning |
| ------------------------ | -------- | ------------------------------------------------------------ |
| `deadline` | 32 bits | Signature expiry |
| `fee` | 32 bits | Q32 fee rate applied to this swap |
| `nonce` | 64 bits | One-time-use replay protection |
| `authorizedLockerLow128` | 128 bits | Lower 128 bits of the permitted locker; `0` means any locker |
The signature is EIP-712 over:
```plaintext
SignedSwap(bytes32 poolId,uint256 meta,bytes32 minBalanceUpdate)
```
where `poolId = keccak256(abi.encode(poolKey.token0, poolKey.token1, poolKey.config))`. The domain is `Ekubo SignedExclusiveSwap`, version `1`, bound to the chain ID and the extension address.
With [`@ekubo/yul-router-sdk`](/integration-guides/yul-router/), `encodeSignedSwapMeta({ deadline, fee, nonce, authorizedLocker })` packs the meta word and `encodePoolBalanceUpdate(delta0, delta1)` packs the bounds.
## Swap flow
[Section titled “Swap flow”](#swap-flow)
1. The caller holds a Core lock and forwards to the extension.
2. The extension validates the deadline (which must also be no further than 30 days out), the locker authorization, and the signature against the pool’s stored controller.
3. If this is the pool’s first touch at the current block timestamp, previously collected extension fees are donated to LPs.
4. The extension calls `Core.swap(...)`.
5. The balance update returned by Core is checked component-wise against `minBalanceUpdate`. **This check happens before the fee is applied**, so the bound constrains the raw swap result, not the amount the swapper finally receives.
6. Only now is the nonce consumed — deliberately after the bounds check, so a swap that fails its bounds costs less gas and does not burn the nonce.
7. The signed `fee` is applied — charged on the output for exact-in swaps, on the required input for exact-out — and the fee-adjusted balance update is returned to the caller.
8. The charged fee is credited to the **extension contract’s own** saved balance in Core, salted by the pool ID, from which it is later donated to that pool’s liquidity providers.
## Why `minBalanceUpdate` matters
[Section titled “Why minBalanceUpdate matters”](#why-minbalanceupdate-matters)
`minBalanceUpdate` is a signed lower bound on both components of the balance update Core returns for the swap — checked before the signed fee is applied, as in step 5 above — and it is part of the signed payload. One field gives the signer four protections at once:
* **Direction enforcement** — requiring the expected leg to be positive or negative prevents a fill that moves value the wrong way
* **Slippage tolerance** — the signer can allow a range (“at least X output”) rather than an exact result
* **Maximum magnitude** — bounds cap how large a trade can execute under one signature
* **Best-price cap** — because both components are bounded, the signer can also cap how *favorable* a fill may be, avoiding overfilling beyond inventory or risk limits
## Fee donation timing
[Section titled “Fee donation timing”](#fee-donation-timing)
Collected fees are not donated to LPs immediately. On a pool’s first touch at a new block *timestamp* — a swap, a position update, or a fee collection — the extension donates previously collected fees into LP accounting and records the timestamp. This prevents liquidity from being added purely to capture fees earned earlier. Note the gate is the block timestamp, not the block number, so on chains that can produce more than one block per second donation happens at most once per second.
## Replay protection and nonce management
[Section titled “Replay protection and nonce management”](#replay-protection-and-nonce-management)
Each signed quote carries a nonce that can be consumed only once, tracked in a bitmap. Managing the lifecycle is the controller’s responsibility off-chain: track which nonces were consumed on-chain, which were issued but expired unfilled, and only recycle a nonce when it is safe to do so. The owner can reset nonce state through admin bitmap management when controlled recycling is needed.
Caution
The nonce `type(uint64).max` is a **reserved, reusable sentinel**: it is never consumed, so a signature carrying it can be replayed without limit until its deadline passes. Issue it only when unlimited reuse within the deadline is exactly what you intend.
## Quote selection risk
[Section titled “Quote selection risk”](#quote-selection-risk)
Off-chain signed quotes carry selective execution risk: a counterparty can request many quote variants, wait for the market to move, and execute only the profitable ones while letting the rest expire. Common mitigations:
* Use short deadlines so stale quotes lose value quickly
* Bind quotes to an authorized locker or session
* Gate quote access so only qualified users receive exclusive-liquidity quotes
* Apply stricter issuance policies — rate limits, narrower bounds, per-user controls — to higher-risk flow
## Controller management
[Section titled “Controller management”](#controller-management)
The extension is `Ownable`. The owner initializes each pool with a controller address via `initializePool(poolKey, tick, controller)`; direct initialization through Core is blocked. The controller for an already-initialized pool can be updated by the owner. Controller signatures work for both EOAs and ERC-1271 contract wallets, and which path is used is determined by **bit 159 of the controller address itself**, not by a separate flag: addresses below `2^159` are verified as EOAs via ECDSA, addresses at or above it via ERC-1271. Initialization and controller updates enforce that the address’s code presence matches, so an EOA whose address happens to have bit 159 set cannot be used as a controller.
# Swapping
> Integrate Ekubo protocol to provide better prices to swappers or to perform arbitrage
## Routing
[Section titled “Routing”](#routing)
It is your responsibility to find the best list of pools for executing a trade. This is equivalent to finding the best route for arbitrage. The easiest way to get a route is the [Quoter API](/api/#quoter), which returns block-pinned split routes ready to execute.
## Executing swaps on-chain
[Section titled “Executing swaps on-chain”](#executing-swaps-on-chain)
Every interaction that moves tokens starts with `ICore#lock` (see the [“till” pattern](/concepts/architecture/)). Core then calls back into your contract: on Starknet via `ILocker#locked(id, data)`, and on EVM via the zero function selector `0x00000000`, which the reference implementations expose as `locked_6416899205(uint256)` — the numeric suffix is mined so that the selector is zero. Inside the callback you execute the swap(s), pay the input, and withdraw the output tokens, in any order. You typically call `swap` several times in a single `locked` callback so that you only settle the net differences.
## Swapping on EVM chains
[Section titled “Swapping on EVM chains”](#swapping-on-evm-chains)
Production swaps go through the [Yul Router](/integration-guides/yul-router/) — a gas-optimized router deployed deterministically at the same address on each chain it has been released to, with routes encoded by [`@ekubo/yul-router-sdk`](https://www.npmjs.com/package/@ekubo/yul-router-sdk). Release v0.8.0 is deployed on all 13 mainnets in the [EVM supported chains](/reference/contracts/evm-v3/#supported-chains) table and on 13 testnets; the [release manifest](https://github.com/EkuboProtocol/yul-router/blob/v0.8.0/broadcast/releases/v0.8.0/manifest.json) lists each of the 26 networks. See the [Yul Router guide](/integration-guides/yul-router/) for the calldata model, hop types, and SDK usage.
### Retired: the HuffRouter and `@ekubo/evm-hyper-router-sdk`
[Section titled “Retired: the HuffRouter and @ekubo/evm-hyper-router-sdk”](#retired-the-huffrouter-and-ekuboevm-hyper-router-sdk)
The earlier HuffRouter and its npm package `@ekubo/evm-hyper-router-sdk` are retired and must not be integrated. On May 5, 2026 a calldata parsing bug in the deployed routers was exploited to drain ERC-20 approvals from accounts that had approved them; the [post-mortem](https://github.com/EkuboProtocol/huff-router/blob/main/incident-report/README.md) documents the bug. Every published version of the package (`0.0.1` through `0.2.2`, all prereleases, and `0.2.3-alpha.0`) is deprecated on npm, and the routers it targeted are immutable contracts that still hold code, so a remaining approval is still drainable.
| Chain | Router | Exported by the SDK |
| -------- | -------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Ethereum | `0x8f52903d17e2d8d6c77d1a1de0cc975b6b5a0d15` | never (Ekubo V2 generation) |
| Ethereum | `0x8ccb1ffd5c2aa6bd926473425dea4c8c15de60fd` | `MAINNET_ADDRESS` in `0.1.0-alpha.1` |
| Ethereum | `0x4f168f17923435c999f5c8565acab52c2218edf2` | `DEPLOYMENTS` chain 1 in `0.2.0-alpha.5` through `0.2.3-alpha.0` |
| Arbitrum | `0xc93c4ad185ca48d66fefe80f906a67ef859fc47d` | `DEPLOYMENTS` chain 42161 in `0.2.0-alpha.2` and `0.2.0-alpha.6` through `0.2.3-alpha.0` |
The same package also pointed at that router generation on Sepolia (`0xf9bf8e1f79d8ee9983abf3be674b815d5d499a2e`), Arbitrum Sepolia, Robinhood testnet, and MegaETH (the last three at `0xc93c4ad185ca48d66fefe80f906a67ef859fc47d`).
If an account ever approved one of these routers, set the allowance back to zero on each approved token. To migrate, re-encode routes with `@ekubo/yul-router-sdk` and send them to the `YUL_ROUTER_ADDRESS` it exports: HuffRouter calldata is a different format that the Yul Router rejects with `InvalidRoute`, so changing only the destination address does not work. Three HuffRouter options have no equivalent: `integrationFee` (the Yul Router collects no fees), `delegatecall` execution (rejected by the Yul Router), and the implicit unbounded slippage default (`calculatedAmountThreshold` is required by the Yul SDK). Hop types are renamed from `swap` and `wrappedToken` to `core` (or `forwarded`) and `wrapper`.
## Reference routers
[Section titled “Reference routers”](#reference-routers)
To execute swaps from your own contract, see the reference Router implementations on [Starknet](https://github.com/EkuboProtocol/starknet-contracts/blob/v5.0.3/src/router.cairo) and [EVM](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/Router.sol). On EVM, a contract that already holds a Core lock can also run a Yul Router route through `Core.forward` and settle the net result itself; see [forwarded mode](/integration-guides/yul-router/#three-ways-to-execute-a-route).
# Yul Router
> The gas-optimized router used for Ekubo swaps on EVM chains, and the TypeScript SDK that encodes routes and quotes for it
The [Yul Router](https://github.com/EkuboProtocol/yul-router/tree/v0.8.0) is a gas-focused router written in Yul that executes Ekubo swaps on EVM chains. It is how swaps are executed in production today: the [interface](https://ekubo.org) encodes routes from [Quoter API](/api/#quoter) results and sends them to the router.
This page describes release **v0.8.0** of the router and of `@ekubo/yul-router-sdk`. The repository’s `main` branch can contain unreleased changes, so read the source and README at the [`v0.8.0` tag](https://github.com/EkuboProtocol/yul-router/tree/v0.8.0) rather than at `main`.
The router is deployed deterministically on every supported network. Always use `YUL_ROUTER_ADDRESS` exported by the same installed version of `@ekubo/yul-router-sdk` that you use to encode calldata. This keeps the router destination compatible with that version’s encoding; do not copy or hard-code an address from documentation.
## Design
[Section titled “Design”](#design)
The router deliberately carries everything it needs — token addresses, pool configs, extension forwardee addresses, and token wrapper addresses — **in calldata**. There are no token or extension jump tables and no stored routes.
A single route can contain many multi-hop paths. The router executes all of them under **one Core lock**, aggregates the specified and calculated amounts, applies **one slippage check** against the aggregate, and settles token transfers once.
### Three ways to execute a route
[Section titled “Three ways to execute a route”](#three-ways-to-execute-a-route)
The same packed route bytes can be executed in three modes. All three run the identical route, apply the same slippage check, and return the same four-word result `(address specifiedToken, address calculatedToken, int256 specifiedAmount, int256 calculatedAmount)`, so for the same route and starting state their return data is byte-for-byte identical.
| Mode | How it is called | Settles? |
| --------- | -------------------------------------------------------------- | ----------------------------------------------------------- |
| Direct | Send the packed route bytes to the router as calldata | Yes: pulls the input from the sender and pays the recipient |
| Forwarded | `Core.forward(router, routeData)` from inside an existing lock | No: the debt stays on the caller’s lock |
| Quote | `quote(bytes routeData)` on the router | No: all state changes are rolled back |
**Direct.** There is no public swap selector: any call that does not come from Ekubo Core and does not use the `quote(bytes)` selector is interpreted as packed route data. The router takes a Core lock, executes the route, and settles. ERC20 input is pulled from the caller with `transferFrom`, so approve `YUL_ROUTER_ADDRESS` for the input token first. The output goes to `recipient` if the route encodes one, otherwise to the caller.
**Forwarded.** A contract that already holds a Core lock can pass the same route data through `Core.forward(router, routeData)`. The router executes the route and applies its slippage check but deliberately does **not** settle. It leaves endpoint debt changes of `specifiedAmount` in `specifiedToken` and `-calculatedAmount` in `calculatedToken` on the caller’s lock. This lets a locker combine a routed swap with another operation in the same lock, such as adding liquidity, and settle only the net result. Any `recipient` encoded in the route is ignored in this mode, because the original locker owns settlement.
**Quote.** `quote(bytes routeData)` runs the route inside a Core lock, then reverts from the lock callback with a recognized result payload. The public entrypoint catches only that payload and returns the four-word result. Pool and extension state changes are rolled back, and no token balance or approval is needed. It must be called without ETH value. Unrelated route or extension errors are bubbled unchanged, including `SlippageCheckFailed(int256)` if the route’s threshold is not met.
Calls that come from Core are reserved for the lock callback (selector `0x00000000`) and the forward callback (selector `0x00000001`). Any other selector from Core reverts.
Amount signs follow one convention in every mode: exact-input amounts are positive and exact-output amounts are negative. `calculatedAmount` is the output for an exact-input route (positive) and the input for an exact-output route (negative).
### Hop types
[Section titled “Hop types”](#hop-types)
| Hop type | Executes |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `core` | A direct `Core.swap` against the pool key; Core dispatches the pool’s configured hooks |
| `forwarded` | `Core.forward(forwardee, abi.encode(poolKey, params))` for forward-only swap extensions such as MEVCapture and [Ve33](/products/ve33/) |
| `signedExclusiveSwap` | A controller-signed swap on a SignedExclusiveSwap pool (pool key, params, signed meta, minimum balance update, and signature) |
| `wrapper` | `Core.forward(wrapper, abi.encode(int256 amount))` to wrap or unwrap through an Ekubo token wrapper |
For `forwarded` and `signedExclusiveSwap` hops, the SDK defaults `forwardee` to the extension encoded in `poolKey.config`; set it explicitly only to route through an adapter.
`core` and `forwarded` hops accept an optional `allowPartial` flag. With it, the router accounts for the amount actually swapped instead of requiring the specified amount to be filled in full. It is only valid on a single-hop path with a nonzero `specifiedAmount`, so a partial fill cannot strand debt in an intermediate token. To move a pool to a target price, use a partial exact-output swap with `specifiedAmount` set to the `int128` minimum and the target as its `sqrtRatioLimit`.
### Excluded by design
[Section titled “Excluded by design”](#excluded-by-design)
* No delegatecall routing (the router checks an immutable self address and rejects delegatecall execution)
* No protocol or integration fee collection and no fee claiming
### Review status
[Section titled “Review status”](#review-status)
The router’s [Codex audit](https://github.com/EkuboProtocol/yul-router/blob/v0.8.0/audits/codex-audit-2026-07-06.md) covered commit `04d29c4` (July 6, 2026). That commit predates the `signedExclusiveSwap` hop (added July 8), forwarded mode (July 25), and `quote(bytes)` and partial routes (July 27); at the audited commit, `Core.forward(router, ...)` was rejected. The Cantina [AI audit scan](/reference/audits/) is dated July 24, 2026, also before forwarded mode, `quote(bytes)`, and partial routes were added. v0.7.1 was additionally reviewed by the CSO (ACCEPT, pinned to `c5a0dc4`, 2026-09-29); the Codex and Cantina reviews covered earlier revisions. v0.8.0 is additionally covered by CSO review (2026-10-01): ACCEPT of the `c5a0dc4..2f849ed` refactor range, pinned to `2f849ed`, which together with the reviewed `2f849ed..262b793` deadline delta and an empty `262b793..v0.8.0` source diff completes CSO coverage of the v0.8.0 router source.
CI continuously checks the router against production: it requests live mainnet quotes from the Quoter API, converts them to calldata with the SDK, and executes that calldata against canonical Core on a mainnet fork at each quote’s block. The cases cover ETH to ERC20, ERC20 to ETH, ERC20 to ERC20, and exact-output swaps.
## The SDK: `@ekubo/yul-router-sdk`
[Section titled “The SDK: @ekubo/yul-router-sdk”](#the-sdk-ekuboyul-router-sdk)
Routes are encoded with [`@ekubo/yul-router-sdk`](https://www.npmjs.com/package/@ekubo/yul-router-sdk), which is published with npm provenance from the repository’s release workflow:
```sh
npm install @ekubo/yul-router-sdk@0.8.0
```
`viem` is a peer dependency.
### Encoding a swap
[Section titled “Encoding a swap”](#encoding-a-swap)
`encodeRoutes(...)` is the primary surface. Each entry in `multiHops` is an independent path from `specifiedToken` to `calculatedToken` with its own `specifiedAmount`; the router aggregates them all under one lock and one slippage check. Splitting a trade across multiple multi-hops is how split routes execute atomically.
```typescript
import { encodeRoutes, YUL_ROUTER_ADDRESS } from "@ekubo/yul-router-sdk";
const calldata = encodeRoutes({
// positive specifiedAmount = exact-in; negative = exact-out
specifiedToken: WETH,
calculatedToken: USDC,
// slippage protection: minimum output (exact-in) or maximum input
// (exact-out). Required. Pass `false` only to explicitly opt into an
// unbounded threshold.
calculatedAmountThreshold: minUsdcOut,
recipient, // optional; defaults to the sender
// last unix second the route may execute; see Deadlines below
deadline: Math.floor(Date.now() / 1000) + 30 * 60,
multiHops: [
{ specifiedAmount: 10n ** 18n, hops: [{ type: "core", poolKey }] },
// e.g. a second split through a MEVCapture pool:
// { specifiedAmount: ..., hops: [{ type: "forwarded", poolKey: otherPoolKey }] },
],
});
// send directly to the router: the calldata IS the route
await wallet.sendTransaction({ to: YUL_ROUTER_ADDRESS, data: calldata });
```
Notes:
* Native ETH is `address(0)` (always `token0`). When the input is native ETH, attach the input amount as `value`, or the maximum input for an exact-output route. Unused ETH is refunded to the sender.
* ERC20 input requires an approval of `YUL_ROUTER_ADDRESS` for at least the input amount.
* All multi-hops in one call must agree on direction; mixing exact-in and exact-out throws.
* `encodeRoute(...)` is a convenience wrapper for a single path; `generateCalldata(...)` is an alias of `encodeRoutes(...)`.
* Limits: up to 256 multi-hops per call and 256 hops per multi-hop (`MAX_MULTIHOP_LENGTH`, `MAX_HOP_LENGTH`).
### Deadlines
[Section titled “Deadlines”](#deadlines)
`encodeRoutes(...)` and `encodeRoute(...)` accept an optional `deadline`: the last Unix timestamp, in seconds, at which the route may execute. It must fit in a `uint32`, and the SDK throws otherwise. The router compares it with the block timestamp before executing any hop: the route may still execute during the deadline second itself, and from the next second it reverts with `DeadlineExpired()` (selector `0x1ab7da6b`). The check applies in all three modes: direct, forwarded, and `quote(bytes)`.
Omitting `deadline` encodes a route that never expires; such a route encodes exactly as it did before deadlines were added. Swaps submitted on behalf of users should always set a deadline. `calculatedAmountThreshold` bounds only the amounts, while a deadline also bounds how long a pool’s fee or state can change before the route lands. The [interface](https://ekubo.org) sets a deadline when the user signs, 30 minutes ahead by default, counted from the latest block’s timestamp rather than the device clock.
In the encoded route, the deadline is a 4-byte value appended to the header after the optional recipient and signalled by header flag bit 1. Header flag bits above bit 1 are reserved, and the router rejects them with `InvalidRoute()`.
### Quoting a route on-chain
[Section titled “Quoting a route on-chain”](#quoting-a-route-on-chain)
`generateQuoteCalldata(params)` takes the same parameters as `encodeRoutes(...)` and returns calldata for `quote(bytes)`. `encodeQuoteCalldata(routeData)` wraps route bytes you have already encoded. Call the result with `eth_call` and decode it with the exported `YUL_ROUTER_ABI`:
```typescript
import { decodeFunctionResult } from "viem";
import {
generateQuoteCalldata,
YUL_ROUTER_ABI,
YUL_ROUTER_ADDRESS,
} from "@ekubo/yul-router-sdk";
const data = generateQuoteCalldata({
specifiedToken: WETH,
calculatedToken: USDC,
// still required; `false` quotes without a slippage bound
calculatedAmountThreshold: false,
multiHops,
});
const { data: result } = await publicClient.call({
to: YUL_ROUTER_ADDRESS,
data,
});
const [specifiedToken, calculatedToken, specifiedAmount, calculatedAmount] =
decodeFunctionResult({
abi: YUL_ROUTER_ABI,
functionName: "quote",
data: result!,
});
```
`calculatedAmountThreshold` is required here too. The SDK throws if it is omitted, and the router enforces it during a quote exactly as it does during a swap. Pass `false` to read the unbounded result, or pass your real threshold to check that the swap would clear it at the current state.
### Signed exclusive swaps
[Section titled “Signed exclusive swaps”](#signed-exclusive-swaps)
For `signedExclusiveSwap` hops, `encodeSignedSwapMeta({ deadline, fee, nonce, authorizedLocker })` packs the signed metadata word. `deadline` and `fee` are `uint32` numbers; `nonce` must be a `bigint` (a JavaScript `number` is rejected so `uint64` nonces cannot lose precision). `encodePoolBalanceUpdate(delta0, delta1)` packs the signed minimum balance update.
### Other exports
[Section titled “Other exports”](#other-exports)
* `YUL_ROUTER_ADDRESS`: the router compatible with this SDK version’s encoding
* `YUL_ROUTER_ABI`: the ABI of the `quote(bytes)` entrypoint
* `MIN_SQRT_RATIO` / `MAX_SQRT_RATIO`: bounds for `sqrtRatioLimit` on hops (see [Price representation](/reference/price-representation/))
* `MIN_CALCULATED_AMOUNT_THRESHOLD` / `MAX_CALCULATED_AMOUNT_THRESHOLD`: the `int128` bounds for `calculatedAmountThreshold`
* `PoolKey`, `Hop`, `MultiHop`, and parameter types for TypeScript consumers
* `calldataSize(data)`: helper for estimating calldata cost
## Preparing a swap from the Quoter API
[Section titled “Preparing a swap from the Quoter API”](#preparing-a-swap-from-the-quoter-api)
The [Quoter API](/api/#quoter) returns block-pinned split routes, but **not** in the shape the SDK takes. The Quoter uses snake\_case field names and encodes amounts and sqrt ratios as strings; the SDK takes camelCase fields and `bigint` values. Convert each field as follows:
| Quoter response field | SDK parameter | Conversion |
| --------------------------------------------- | ------------------------------------------ | ------------------------------------------------- |
| `splits[]` | `multiHops[]` | One multi-hop per split |
| `splits[].amount_specified` (string) | `multiHops[].specifiedAmount` | `BigInt(...)` |
| `splits[].route[]` | `multiHops[].hops[]` | One hop per route node |
| `route[].swap.type` (`core` / `forwarded`) | `hop.type` | Unchanged |
| `route[].swap.pool_key` | `hop.poolKey` | Same `token0` / `token1` / `config` fields |
| `route[].swap.sqrt_ratio_limit` (hex string) | `hop.sqrtRatioLimit` | `BigInt(...)` |
| `route[].swap.skip_ahead` (integer) | `hop.skipAhead` | Unchanged |
| `route[].wrapped_token.{underlying, wrapped}` | `{ type: "wrapper", underlying, wrapped }` | Unchanged addresses |
| `total_calculated` (string) | basis for `calculatedAmountThreshold` | `BigInt(...)`, then apply your slippage tolerance |
The Quoter does not return the endpoint tokens: `specifiedToken` and `calculatedToken` are the tokens you requested the quote for. For an exact-output request (negative amount), `specifiedToken` is the output token and `calculatedToken` is the input token. `forwarded` hops need no `forwardee`, because the SDK derives it from the extension in `poolKey.config`.
```typescript
import {
encodeRoutes,
YUL_ROUTER_ADDRESS,
type Hop,
} from "@ekubo/yul-router-sdk";
function toHop(node): Hop {
if (node.wrapped_token) {
const { underlying, wrapped } = node.wrapped_token;
return { type: "wrapper", underlying, wrapped };
}
const { type, pool_key, sqrt_ratio_limit, skip_ahead } = node.swap;
return {
type, // "core" or "forwarded"
poolKey: pool_key,
sqrtRatioLimit: BigInt(sqrt_ratio_limit),
skipAhead: skip_ahead,
};
}
// quote = await fetch(`https://prod-api-quoter.ekubo.org/${chainId}/${amount}/${specifiedToken}/${calculatedToken}`)
const totalCalculated = BigInt(quote.total_calculated);
const slippageBps = 50n;
const calculatedAmountThreshold =
totalCalculated < 0n
? (totalCalculated * (10_000n + slippageBps)) / 10_000n // exact-out: maximum input
: (totalCalculated * (10_000n - slippageBps)) / 10_000n; // exact-in: minimum output
const calldata = encodeRoutes({
specifiedToken,
calculatedToken,
calculatedAmountThreshold,
deadline: Math.floor(Date.now() / 1000) + 30 * 60,
multiHops: quote.splits.map((split) => ({
specifiedAmount: BigInt(split.amount_specified),
hops: split.route.map(toHop),
})),
});
```
Send the calldata to `YUL_ROUTER_ADDRESS` promptly, since quotes are pinned to a block; the `deadline` bounds how late it can still land. To confirm the route still clears your threshold at the current state, pass the same parameters to `generateQuoteCalldata(...)` first.
## Deploying to a new chain
[Section titled “Deploying to a new chain”](#deploying-to-a-new-chain)
Use the repository’s Foundry deploy script. It deploys through the canonical deterministic deployer against the canonical Core address, so the router lands at the same address everywhere.
# Products
> What you can do with Ekubo, and the infrastructure behind it
Ekubo is more than an AMM contract. It is a set of products built around one shared liquidity layer — for traders, liquidity providers, ecosystems bootstrapping their own markets, and developers who need the underlying data.
## [Wallet](/wallet/)
[Section titled “Wallet”](#wallet)
A native desktop EVM wallet for people, local AI agents, and WalletConnect dapps. Agents can discover accounts and networks, inspect balances, simulate exact actions, and propose transactions while private keys, security settings, and owner approvals remain inside the wallet.
## [Trading](/products/trading/)
[Section titled “Trading”](#trading)
Swap across every Ekubo pool with routing that accounts for extension behavior. Quotes come from a public routing service that returns block-pinned split routes, which execute through a gas-optimized router in a single transaction.
## [Providing liquidity](/products/liquidity/)
[Section titled “Providing liquidity”](#providing-liquidity)
Supply concentrated, stableswap, or full-range liquidity in the same Core contract. Extensions add behavior on top — automatic order execution, MEV capture, externally funded fee boosts — without fragmenting liquidity into separate protocols.
## [Ve33 and STONX](/products/ve33/)
[Section titled “Ve33 and STONX”](#ve33-and-stonx)
A token-governed liquidity marketplace anyone can deploy around their own token. Holders lock the stake token, direct emissions to pools, set those pools’ fees, and earn the fees of the pools they support. **STONX** on Robinhood Chain is the first ecosystem deployment, coordinating liquidity across stock-token markets.
## [Rewards and incentives](/products/rewards/)
[Section titled “Rewards and incentives”](#rewards-and-incentives)
Liquidity incentive campaigns that measure real, useful liquidity and distribute rewards through periodic on-chain drops. Claims are merkle-based and permissionless, and anyone can fund a drop.
## [Indexer](/products/indexer/)
[Section titled “Indexer”](#indexer)
The open source indexer that turns Ekubo’s on-chain events into a queryable Postgres database — the same code behind the public API. Nightly database dumps let you bootstrap a node in minutes instead of days.
## [MCP server](/products/mcp-server/)
[Section titled “MCP server”](#mcp-server)
A public Model Context Protocol server at `mcp.ekubo.org` that lets AI agents quote swaps, read pools and positions, and prepare unsigned execution plans — non-custodially, with signing left to the user’s wallet.
## [Governance](/products/governance/)
[Section titled “Governance”](#governance)
The EKUBO token, the Staker and Governor contracts that control the protocol’s upgradeable deployments, how protocol revenue flows back to the DAO, and Ekubo, Inc.’s defined role within it.
# Governance
> How the Ekubo DAO is structured, what it controls, how protocol revenue flows back to it, and Ekubo, Inc.'s role within it
Ekubo Protocol is governed by holders of the [EKUBO token](/user-guides/ekubo-token/). Governance is deliberately narrow in scope: the EVM V3 Core contracts are **ownerless and immutable**, so there is no privileged actor who can change how the AMM works or seize funds. What governance does control is the protocol’s upgradeable deployments, its treasury, and the parameters of the periphery.
The contracts are open source in the [governance repository](https://github.com/EkuboProtocol/governance) and are themselves ownerless and non-upgradeable, apart from the Governor’s ability to upgrade itself by proposal. Deployed addresses are listed in the [governance contracts reference](/reference/contracts/governance/).
## The three contracts
[Section titled “The three contracts”](#the-three-contracts)
**EKUBO** is an ERC-20 on Ethereum, bridged to Starknet. It is the unit of voting weight.
**Staker** holds staked tokens and tracks delegation. Staking is not vote-escrow: there is no lockup, no decay, and no penalty for withdrawing. You stake to a delegate — often yourself — and can withdraw at any time.
Voting weight is not simply your staked balance. The Staker records delegation over time, and weight is the **average amount delegated to you** over a smoothing window ending when voting opens. This makes weight expensive to manufacture immediately before a vote.
**Governor** runs the proposal lifecycle and executes approved calls itself, so a passed proposal can make arbitrary calls — including `send_message_to_l1`, which is how it drives the owner proxies on other chains. (It also implements the account interface, but only so that proposals can be simulated off-chain.)
## Proposal lifecycle
[Section titled “Proposal lifecycle”](#proposal-lifecycle)
```plaintext
propose → voting delay → voting period → execution delay → execution window → executed
```
A proposal commits to a set of calls by hash; those exact calls must be supplied again at execution. It passes only if it reaches quorum **and** receives strictly more `yea` than `nay` votes — a tie fails.
Current configuration:
| Parameter | Value |
| -------------------------------- | --------------- |
| Voting start delay | 1 hour |
| Voting period | 4 days |
| Voting weight smoothing duration | 1 day |
| Quorum | 3,250,000 EKUBO |
| Proposal creation threshold | 100,000 EKUBO |
| Execution delay | 1 hour |
| Execution window | 30 days |
These are themselves governance-configurable, and each proposal is versioned against the configuration in effect when it was created — so a proposal created before a reconfiguration still runs under the old parameters. Read the current values directly from the Governor’s `get_config` entrypoint.
Additional rules worth knowing: a proposer may have only one active proposal at a time, and a proposal can be cancelled only by its proposer and only before voting opens — the delay period exists so mistakes can be corrected. Execution is atomic and happens once; if a call reverts, the whole proposal can be retried within the execution window.
For the practical steps, see [Participate in governance](/user-guides/governance/).
## What governance controls
[Section titled “What governance controls”](#what-governance-controls)
* **Starknet contracts** — Core, Positions, and the extensions are upgradeable in place. The extensions are owned directly by the Governor; Core and Positions are held by the RevenueBuybacks contracts, which the Governor owns and can reclaim from by proposal
* **The treasury** — assets held by the DAO, disbursed by proposal (including streamed payments)
* **Cross-chain deployments** — owner proxies on Ethereum, Base, Optimism, Arbitrum One, Robinhood Chain, Unichain, World Chain, Ink, MegaETH, Gnosis, and Polygon are configured so that a Starknet proposal can control contracts on other chains. See the [peripheral-contract ownership update](#peripheral-contract-ownership-update-9-october-2026) below for which contracts they own and for the status of production cross-chain message delivery
* **Periphery ownership** — the owner role on Positions and other periphery contracts, which can withdraw accumulated protocol fees to a recipient the owner chooses. It is not held by the DAO on every chain: on BNB Smart Chain and Monad, Ekubo, Inc. still owns the Positions and Orders contracts (see the [update](#peripheral-contract-ownership-update-9-october-2026) below). The fee *rate* itself is not a governance parameter: it is immutable on EVM and a compile-time constant on Starknet, so changing it requires a new deployment
Notably *not* controlled: the EVM V3 Core contract, which has no owner at all.
## Peripheral-contract ownership update (9 October 2026)
[Section titled “Peripheral-contract ownership update (9 October 2026)”](#peripheral-contract-ownership-update-9-october-2026)
**Peripheral-contract ownership update — 9 October 2026.** Ekubo, Inc. has completed 33 direct ownership transfers of Positions, Orders and Auctions peripheral contracts to designated DAO owner proxies across 11 EVM chains: Ethereum, Base, Optimism, Arbitrum One, Robinhood Chain, Unichain, World Chain, Ink, MegaETH, Gnosis and Polygon. Six new owner proxies were deployed as part of this migration. The transfers were company-signed, board-approved `transferOwnership` transactions, not transactions executed under a DAO vote. No Governor proposal was created for the prepared P1, R1 or R2 drafts. P1 was withdrawn before proposal; R1 was superseded before proposal; the board stood down R2 on 9 October 2026. No ownership handover request or completion step remains pending.
**Six company-controlled contracts remain.** The Ekubo, Inc. company key `0x00000c771f6176268d5a9846e0956c3ef58597a1` still owns Positions and Orders v3.2.0 on BNB Smart Chain (two contracts), and Positions and Orders v3.1.1 and v3.2.0 on Monad (four contracts). Their DAO-control route remains subject to a separate board decision. Positions ownership permits withdrawal of accrued protocol fees to an owner-selected recipient and metadata/ownership administration; Orders ownership permits metadata/ownership administration, not funds withdrawal. These six contracts are not DAO-controlled.
**Production cross-chain delivery remains unproven.** Ownership and proxy configuration have been independently verified by Ekubo’s security reviewer, and messaging paths have been tested on live-chain forks. However, no production DAO message has yet been delivered through any of the ten L2/sidechain owner proxies. The board accepted this residual and decided not to create the R2 reachability-proof proposal. Configured DAO ownership is not a demonstration or guarantee of production cross-chain message delivery. If a messaging route fails to deliver, affected owner-only functions, including fee withdrawal and metadata administration where applicable, could be unreachable until the route is restored. Such a delivery failure does not itself transfer or give access to user positions or funds, or change the immutable, ownerless EVM Core contracts.
The ownership migration did not withdraw fees, transfer user tokens or position NFTs, change protocol fee rates or fee recipients, create automatic fee remittances or buybacks, or change STONX economic rights. The accompanying chain-, version- and address-specific control matrix and transaction record identify the migrated contracts, new proxies and remaining company-controlled contracts. This is a dated ownership snapshot, not a claim that all deployments are DAO-controlled or that every cross-chain route has been exercised in production.
The per-contract transaction record, the owner proxies and the six contracts that remain company-owned are listed, with snapshot blocks, in [Peripheral-contract ownership](/reference/contracts/peripheral-ownership/).
## Revenue buybacks
[Section titled “Revenue buybacks”](#revenue-buybacks)
Protocol fees are collected at the periphery — a share of the swap fees liquidity providers collect — and flow back to the DAO through the RevenueBuybacks contract.
The mechanism is permissionless: anyone can trigger it. It withdraws accumulated protocol fees and places a [TWAMM order](/user-guides/dollar-cost-average-orders/) selling them for EKUBO gradually over a configured window, rather than in a single market-moving trade. Proceeds are collected to the Governor. Order timing and duration bounds, and the pool fee used, are set by governance per token.
## Ekubo, Inc.’s role
[Section titled “Ekubo, Inc.’s role”](#ekubo-incs-role)
Ekubo, Inc. is the Delaware C corporation that built the initial version of Ekubo Protocol, along with the [indexer](/products/indexer/), the interface, the governance contracts, and the API. It was founded by [Moody Salem](https://x.com/sendmoodz), and bootstrapped the Ekubo DAO in May 2024, distributing two thirds of total supply — one third by airdrop and one third sold by the DAO (see [EKUBO token](/user-guides/ekubo-token/)) — and governing actively from the start. The DAO received the largest [Starknet Catalyst Program grant](https://www.starknet.io/blog/announcing-the-catalyst-program-igniting-transformative-change/) in recognition of that work.
In July 2024 the DAO approved a proposal defining the company’s role in exchange for a one-time grant of roughly $1.5M — intended to be the only grant the company ever requests. Under it, Ekubo, Inc. committed to:
* Develop the core contracts for the benefit of the DAO, and make source code available at the DAO’s direction
* Design and implement a framework for returning protocol revenue to stakers — delivered as the revenue buybacks above
* Develop and host the interface, free to swap on, with a public feature prioritization process
* Maintain this documentation, provide developer support in the [Discord](https://discord.ekubo.org), and help delegates create proposals
* Operate the public API and open source the governance tooling
The company holds one third of the total EKUBO supply and has committed to **never sell** those tokens for as long as it exists, keeping it permanently aligned with the protocol.
### Where the ecosystem can contribute
[Section titled “Where the ecosystem can contribute”](#where-the-ecosystem-can-contribute)
Ekubo, Inc. deliberately does not cover everything. Areas that benefit from independent teams include liquidity provider tooling and automated liquidity management, advanced delegate and governance tooling, market analytics, aggregator and routing integrations, marketing and community management, and exchange listings. If you want to build in one of these areas, start a conversation in the [Discord](https://discord.ekubo.org).
# Indexer
> The open source service that turns Ekubo's on-chain events into a queryable Postgres database — and how to bootstrap one from a nightly dump
The [indexer](https://github.com/EkuboProtocol/indexer) is the open-source service that ingests Ekubo events into a Postgres database. It is the same code that powers the public [Ekubo API](/api/), so anything the API can answer, your own instance can answer too.
Its design goal is an **always-consistent realtime view**: events are cataloged rather than transformed, and the schema is reorg-safe. Analysis happens on top, in materialized views and queries, so the raw record stays faithful to the chain.
Run your own instance when you need lower latency than the public API, higher request volume than its rate limits allow, or direct SQL access for analytics.
## Bootstrapping from a database dump
[Section titled “Bootstrapping from a database dump”](#bootstrapping-from-a-database-dump)
Syncing a fresh database across all networks takes **days**. You almost never want to do that. Instead, start from a published dump.
A nightly workflow (`.github/workflows/pg-dump.yaml`) runs `pg_dump -Fc` against the production database and uploads the result as a GitHub Actions artifact:
1. Open the [Actions tab](https://github.com/EkuboProtocol/indexer/actions) of the indexer repository and select the most recent **pg-dump** run.
2. Download the artifact — named `db-backup-`, containing `db-backup-.dump`.
3. Restore it into your Postgres instance:
```bash
pg_restore --clean --if-exists --no-owner \
--dbname postgres://user:pass@host:5432/dbname \
db-backup-20240101T000000Z.dump
```
Note
Artifacts are retained for **7 days**, so grab a recent run. During restore you may see warnings about the DigitalOcean `doadmin` role or the `pg_cron` extension — those are expected and safe to ignore if your target database lacks those privileges or extensions.
Once restored, start the indexer and it will catch up from the dump’s head to the current chain tip.
## Running it
[Section titled “Running it”](#running-it)
A prebuilt image is published to the GitHub Container Registry for every commit to `main`:
```bash
docker pull ghcr.io/ekuboprotocol/indexer:
```
Bun executes the TypeScript sources directly, so there is no build step. Run the entrypoint for the chain family you want, with `NETWORK` selecting the specific network:
```bash
docker run --rm -e NETWORK=mainnet ekubo-indexer bun src/starknet.ts # Starknet
docker run --rm -e NETWORK=mainnet ekubo-indexer bun src/evm.ts # EVM chains
```
Point it at Postgres with `PG_CONNECTION_STRING`, and apply the schema first:
```bash
docker run --rm --env-file .env ekubo-indexer bun scripts/migrate.ts
```
Migrations live under `migrations/` and run in order. Apply them **before** rolling out new workers.
The repository also includes a DigitalOcean App Platform spec (`.do/app.yaml`) describing the full production stack — a worker per network, managed Postgres, a pre-deploy migration job, and the price-sync process. Use it as a template for reproducing the setup elsewhere.
## What’s in the database
[Section titled “What’s in the database”](#whats-in-the-database)
Tables mirror Ekubo’s on-chain events and derived state: pool initializations and swaps, position updates, per-tick liquidity, pool TVL, TWAMM sale rates and orders, oracle snapshots, Ve33 staking and voting, and ERC-20 token metadata with USD price history. Aggregations such as hourly volume and 24-hour pool statistics are maintained as views.
The repository keeps a **breaking changelog** documenting every schema change and any deployment that needs manual intervention. Read it before upgrading a running instance — some entries change columns that downstream consumers depend on.
Note
Token metadata is not written by the indexer. It is owned by the [`default-tokens`](https://github.com/EkuboProtocol/default-tokens) repository, which syncs metadata into the database separately.
Need help running one? Ask in the `#devs` channel of the [Discord](https://discord.ekubo.org).
# Providing liquidity
> Pool types, fees, and the extensions that change what providing liquidity on Ekubo looks like
Liquidity on Ekubo lives in one Core contract, but the shape it takes is configurable. A pool’s type, its fee, and its [extension](/concepts/extensions/) are all part of its identity, chosen when the pool is created.
For the step-by-step version in the app, see [Add liquidity](/user-guides/add-liquidity/).
## Pool types
[Section titled “Pool types”](#pool-types)
| Type | Liquidity shape | Suits |
| ---------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------- |
| **Concentrated** | Provider-chosen price range, with tick spacing controlling how narrow ranges can be | Most pairs; precise control over where capital sits |
| **Stableswap** | Concentrated around a configurable center price, with an amplification factor | Correlated assets that trade near a fixed ratio |
| **Full range** | The entire price range | Long-tail pairs and the simplest possible position |
All three settle in the same Core contract and are quoted and routed together, so choosing one does not fragment your liquidity away from the others.
Ekubo’s ticks are 1/100th of a basis point — 100x finer than most concentrated-liquidity AMMs — so a range can be drawn exactly where you want it. See [Pool math](/reference/pool-math/) for how positions and prices are represented.
## Fees
[Section titled “Fees”](#fees)
The pool fee is what swappers pay to trade against your liquidity, and it is chosen when the pool is created rather than from a fixed set of tiers. A protocol fee is applied to the swap fees you collect (10% on EVM, 20% on Starknet, both applied by the Positions contract) and funds the DAO through [revenue buybacks](/products/governance/#revenue-buybacks). The canonical Positions deployment charges **no fee on your principal** when you withdraw — the contract supports a withdrawal fee, but it is configured to zero.
Because any licensee can deploy their own Positions contract with its own fee settings, always check the deployment you are actually using.
Core itself charges nothing — see [Protocol architecture](/concepts/architecture/).
## Extensions that change the LP experience
[Section titled “Extensions that change the LP experience”](#extensions-that-change-the-lp-experience)
Extensions attach behavior to a pool without moving liquidity out of Core:
* **TWAMM** — pools that back [DCA orders](/user-guides/dollar-cost-average-orders/). Providing liquidity here earns fees from order flow that executes against the pool when buy and sell orders are imbalanced, in addition to ordinary swap volume.
* **MEV capture** — charges extra on price-moving swaps and returns that value to LPs instead of searchers. MEV capture fees are credited at the pool’s next interaction in a later block timestamp; see [How MEV capture fees are credited](/products/trading/#how-mev-capture-fees-are-credited).
* **Boosted fees** — lets anyone fund additional fee rewards for a pool’s LPs, on top of swap fees. Used for targeted liquidity campaigns.
* **[Ve33](/products/ve33/)** — replaces swap-fee income with emissions of a stake token, with voters directing where those emissions go and setting each pool’s fee. LPs earn emissions; voters earn the swap fees.
* **Oracle** — records price history for a token against native ETH on EVM (a configured quote token on Starknet). Oracle pools are full-range and fee-free by design.
## Positions are NFTs
[Section titled “Positions are NFTs”](#positions-are-nfts)
A liquidity position is an ERC-721 token minted by the Positions contract, so it can be held, transferred, or used as collateral like any other NFT. Its `tokenURI` resolves to the [Ekubo API](/api/), which serves the position’s metadata and a rendered SVG image.
Caution
Never sell the NFT representing a position or an order — it *is* the claim on the underlying capital.
## Rewards beyond swap fees
[Section titled “Rewards beyond swap fees”](#rewards-beyond-swap-fees)
Liquidity can also earn from [incentive campaigns](/products/rewards/), which measure the depth your position provides near the market price and distribute reward tokens through periodic drops.
## Risk
[Section titled “Risk”](#risk)
Providing liquidity is not a yield product. A position earns fees from the volume that trades against it, and simultaneously accrues divergence loss as the price moves — ending up holding more of whichever asset fell in relative value. Narrowing a range concentrates both effects at once: more fees per dollar while the price is in range, and faster conversion into the losing asset when it moves. Whether a position is profitable depends on the volume it captures relative to that divergence, which is a property of the market, not of the protocol.
# MCP server
> Connect an AI agent to Ekubo: quotes, pool and position data, and unsigned execution plans for Ekubo, Aave, Morpho, Sky, Lido, Merkl, and Aerodrome over the Model Context Protocol
Ekubo runs a public [Model Context Protocol](https://modelcontextprotocol.io/) server at **`https://mcp.ekubo.org/mcp`**. It gives AI agents first-class access to Ekubo — resolving tokens, quoting swaps, reading pools and positions, and building transactions — without scraping a web interface.
Because an execution plan is signer-neutral calldata rather than an Ekubo-specific document, the server’s coverage does not stop at Ekubo: it also prepares Aave V3, Morpho Vault V2, Sky Savings, Lido, Merkl, and Aerodrome actions, and quotes swaps from 0x and bridges from Across, LayerZero, and LI.FI alongside Ekubo’s own.
The server is **non-custodial and read-only with respect to keys**. It never holds funds, never signs, and never submits. Tools that produce a transaction return a reference to an unsigned *execution plan*; signing and submission happen in the user’s own wallet tooling.
## Connecting
[Section titled “Connecting”](#connecting)
The server speaks Streamable HTTP and requires no authentication.
### Claude Desktop
[Section titled “Claude Desktop”](#claude-desktop)
Claude Desktop manages remote MCP servers as account-level custom connectors. Do not add the hosted Ekubo server to `claude_desktop_config.json`; that file is for local stdio servers.
1. In Claude Desktop, open **Customize → Connectors**.
2. Click **+**, then select **Add custom connector**.
3. Name the connector **Ekubo**.
4. Enter **`https://mcp.ekubo.org/mcp`** as its remote MCP server URL.
5. Click **Add**, then enable Ekubo for the conversations where you want to use it.
On Team and Enterprise plans, an Owner or Primary Owner must first add the custom connector under **Organization settings → Connectors**. Members can then connect and enable it from **Customize → Connectors**. See [Anthropic’s custom connector guide](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp) for the current account and organization flows.
### Grok Build
[Section titled “Grok Build”](#grok-build)
Ekubo Wallet detects Grok Build and can install both its local wallet bridge and this hosted companion into `~/.grok/config.toml`. To configure only the hosted server manually, use Grok Build’s native TOML shape:
```toml
[mcp_servers.ekubo]
url = "https://mcp.ekubo.org/mcp"
```
### Other MCP clients
[Section titled “Other MCP clients”](#other-mcp-clients)
For clients that accept remote MCP servers in a local configuration file, add:
```json
{
"mcpServers": {
"ekubo": {
"type": "http",
"url": "https://mcp.ekubo.org/mcp"
}
}
}
```
Most clients also accept it from the command line. For example, Claude Code uses `claude mcp add --transport http ekubo https://mcp.ekubo.org/mcp`.
## What it can do
[Section titled “What it can do”](#what-it-can-do)
More than seventy tools, grouped by what you’re trying to accomplish. The catalog grows on its own schedule, so `https://mcp.ekubo.org/tools` — which returns the live list along with the server version and catalog revision it came from — is authoritative wherever this page has drifted.
| Area | Capabilities |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Tokens** | Search the canonical token list by symbol, or look up exact chain/address pairs in batches of up to 1,000. Ordered by visibility priority so the preferred token wins ambiguous symbol matches. |
| **Swaps** | One call returns every available quote — Ekubo and 0x for same-chain swaps, Across, LayerZero, and LI.FI for cross-chain — each option already carrying the execution plan that executes it, so there is no separate preparation step. |
| **Pools** | Read pool state and liquidity, list pool keys, derive pool IDs, decode pool configs, initialize pools, and correct a mispriced pool. |
| **Liquidity positions** | List positions by owner, inspect a position, find candidate pools for a position, and prepare deposits, withdrawals, earnings claims, and transfers. |
| **DCA / TWAMM** | Place, collect, and stop orders, and execute virtual orders. |
| **[Ve33](/products/ve33/)** | Stake, vote, reallocate, extend, split, merge, increase, withdraw, reinvest, and claim fees — plus current allocations and a STONX allocation recommendation. |
| **[Auctions](/reference/contracts/evm-v3/)** | Create an auction, complete it, and collect creator proceeds. |
| **Rewards** | List claimable Ekubo [rewards](/products/rewards/) for an owner and prepare claims, including recovery fund claims; surface boosted-fee, incentive, and projected ve(3,3) opportunities ranked by APR. |
| **Other protocols** | Aave V3, Morpho Vault V2, Sky Savings, Lido, Merkl, and Aerodrome — see [Protocols beyond Ekubo](#protocols-beyond-ekubo). |
| **Transfers** | Prepare one ordered plan of 1 to 4,096 native, ERC-20, ERC-721, and ERC-1155 transfers on a chain, mixed freely. |
| **Utilities** | Revoke approvals, wrap and unwrap tokens, boost a pool manually, expand oracle capacity, unwrap old gEKUBO, trigger revenue buybacks, and export the token list to a wallet by reference. |
It also publishes **resources** that document its own conventions — the canonical agent workflow, the LP position workflow, the Ve33 workflow, quote semantics across providers, the execution-plan handoff, the data API’s OpenAPI spec, and a chain-indexed directory of deployed contract addresses. Agents can read these directly rather than guessing at usage.
Alongside those it publishes **skills** — `ekubo://skills/use-morpho`, `ekubo://skills/use-sky`, `ekubo://skills/use-lido`, `ekubo://skills/use-merkl`, and `ekubo://skills/use-aerodrome`, each with a `references/discovery.md` child naming the official endpoints and reads. They are also plain files over HTTPS, at `https://mcp.ekubo.org/skills/use-merkl/SKILL.md` and its siblings, so a client that does not speak MCP resources can still load them.
## Protocols beyond Ekubo
[Section titled “Protocols beyond Ekubo”](#protocols-beyond-ekubo)
For Aave V3, Morpho Vault V2, Sky Savings, Lido, Merkl, and Aerodrome the server prepares transactions but is deliberately **not in the data path**. It holds a fixed, locally maintained deployment catalog — the addresses and the chains they were verified on — and returns unsigned calls against it. Live market, vault, queue, reward, and balance state is read by the agent from each protocol’s own public API or through your wallet’s RPC, never proxied, cached, or replayed by this server. What decides whether an action succeeds is the wallet’s simulation of the exact calls.
[Supported protocols](/wallet/protocols/) lists the actions and chains for each.
### Merkl reward claims
[Section titled “Merkl reward claims”](#merkl-reward-claims)
[Merkl](https://merkl.xyz/) distributes incentive campaigns for hundreds of protocols. Rewards accrue off chain, are published as a Merkle root to a Distributor contract, and are claimed against a proof — so a claim’s inputs necessarily come from Merkl’s own API rather than from a chain.
The agent fetches them directly, from `https://api.merkl.xyz/v4/users/{address}/rewards/summary`, which is public and needs no key. What makes that safe is not trust in the response. `prepare_merkl_claim` folds every supplied proof into the Merkle root it implies, refuses a batch whose proofs fold to more than one root, and returns a read bundle asking the wallet for the root the chain is currently enforcing along with each already-claimed total and any claim-recipient override. A proof for a rotated tree, or for one still inside its dispute period, fails before anything is signed. Neither the server nor the wallet has to believe Merkl.
Three properties of Merkl’s data are worth stating, because getting them wrong misreports what a user is owed:
* **`amount` is cumulative, not a delta.** The Distributor transfers `amount` minus what that address already claimed for that token, so the claimable figure is `amount - claimed`. Presenting `amount` overstates it, sometimes by everything already collected.
* **`pending` is not claimable.** It is earned but not yet in any published root, and adding it to `amount` double-counts.
* **Claiming for yourself needs no authorization.** Anyone may call `claim()` for a user and the tokens still go to that user, so enabling Merkl’s autoclaim operator delegates gas and timing rather than custody.
`get_merkl_deployment` returns the Distributor address and the chains preparation is allowed on. Merkl lists 67 chains; the server pins the subset that answered `getMerkleRoot()` with a live root at that address, so an unverified chain is refused rather than served a plan nobody checked — ZKsync Era, which has no code at the address the other chains share, is the reason that check exists. One claim covers up to 32 reward tokens on a single chain.
This is distinct from `prepare_rewards_claim`, which claims Ekubo’s own [incentive drops](/products/rewards/).
### Aerodrome
[Section titled “Aerodrome”](#aerodrome)
[Aerodrome](https://aerodrome.finance/) is the ve(3,3) exchange on Base, and it is the case where the no-proxy boundary costs nothing at all — because Aerodrome publishes no data API to be kept out of. Its data pipeline is **Sugar**: lens contracts that fold what would otherwise be dozens of calls into a single `eth_call` returning whole structs of pools, positions, veNFTs, epochs, and rewards.
So discovery here is an on-chain read, not an API call. `prepare_aerodrome_sugar_reads` returns the exact call bundle for one dataset, the wallet runs it against your own RPC, and the pool, gauge, fee, and bribe addresses it decodes are the required inputs to everything else. Those reward contracts are per pool and cannot be derived — a claim naming the wrong one succeeds while transferring nothing, which is why the server asks for them rather than guessing.
The prepared actions cover v2 liquidity through the Router, gauge staking and emissions claims, veAERO locks, gauge votes, and voter fee, bribe, and rebase claims. Three protocol rules shape what is possible, and each is surfaced in the plan rather than discovered by paying gas: a veNFT votes once per weekly epoch and reverts on a second attempt, a vote replaces the entire allocation rather than adding to it, and staking an LP token into a gauge trades that position’s trading fees for AERO emissions rather than adding to them.
`get_aerodrome_deployment` returns the contracts, every one derived on chain from the Voter outward rather than copied from Velodrome’s published SDKs. That distinction matters more than it sounds: those SDKs carry Optimism addresses, and their position struct has drifted from the deployed Base lens by two fields, so decoding a live response with it silently misreads every later field instead of failing. Aerodrome is Base-only; Velodrome is the same codebase elsewhere and is not prepared here. Swaps stay with `get_quotes_with_plans`, which compares sources before anything executes, and concentrated Slipstream positions are readable today but not yet mintable.
## The execution plan boundary
[Section titled “The execution plan boundary”](#the-execution-plan-boundary)
This is the part worth understanding before building on it.
Preparation tools never return “a transaction to send” loosely — and they do not return the plan inline either. Every executable preparation returns an `execution_plan_reference`: an artifact-reference envelope naming where the server stored the plan body, an integrity block (the keccak256 of the exact stored bytes plus their byte count), and a summary carrying the plan’s `chain_id`, sender, and step count for sanity checks. The stored body — a signer-neutral, ordered sequence of unsigned calls — is the canonical boundary between the MCP server and whatever signs.
The rules that keep the boundary safe:
* **Relay the reference, not the body.** The agent passes the envelope unchanged to the wallet, which fetches the body over HTTPS, recomputes the integrity digest, checks the byte count, and refuses a mismatch. The agent in between never fetches, restates, or reconstructs the plan itself.
* **No timestamps travel in the envelope.** A plan’s validity is expressed by the deadline inside its calldata and enforced by the wallet’s simulation against current chain state. An expired reference simply 404s on fetch, and the fix is to re-run the preparation for fresh state and calldata.
* **Bind the sender first.** Choose the signing account before preparing, and pass that exact address. After preparation, the envelope summary’s `chain_id` and sender must match the wallet’s observed chain and account — a mismatch invalidates the plan rather than being silently rewritten.
* **Let the wallet own execution.** Before signing, the wallet freshly validates the exact plan, simulates current chain state, prepares the transaction, and evaluates its current policy. It then presents any required review and submits — preserving step order (a plan may require atomic batching), following the plan’s simulation-failure policy on reverts, and applying each step’s bundled custom-error ABI when decoding failures. Simulation IDs and previews are short-lived handles, not durable authorization. An agent should not add a second confirmation on top.
Prepared on-chain reads travel the same way, as `read_calls_reference` envelopes whose stored body is an exact batch-call argument object. The full contract is published as the server’s `ekubo://docs/execution-plan` resource.
## A typical swap
[Section titled “A typical swap”](#a-typical-swap)
1. Resolve the tokens by symbol or address, and show the user the chains and addresses chosen.
2. Convert the user’s amount to base units without floating-point arithmetic.
3. Once the user has decided to swap, request quotes in a single call with the sender and slippage tolerance. Every available source is returned — the server does not pick one — and each option already carries the execution plan that executes it, so the quote the user compared is the quote that executes. Individual provider failures are reported separately without invalidating the quotes that succeeded.
4. Choose an option and hand its `execution_plan_reference` to the wallet unchanged. The wallet fetches and verifies the plan body, then freshly simulates, prepares, and checks current policy before any signature; it presents required review and submits after authorization.
5. Re-quote only after an expiry, a revert, or a change to the request — never to “refresh” a plan already in hand, which would replace the quote the user approved.
For “swap my entire balance” requests there is an extra step: read the exact on-chain balance first, rather than trusting a displayed number. And for a purely indicative “what would I get” comparison, omit the sender and slippage — quotes come back with no calldata attached.
## Cross-chain transfers
[Section titled “Cross-chain transfers”](#cross-chain-transfers)
When a request’s destination chain differs from its origin, the same `get_quotes_with_plans` call goes out to three bridges instead of two same-chain aggregators: [Across](https://across.to/), [LayerZero](https://layerzero.network/)’s Value Transfer API, and [LI.FI](https://li.fi/). They come back as ordinary quote options, compared exactly the way an Ekubo quote is compared against a 0x one, and the server picks none of them. A provider that cannot serve the request says so in `unavailable_sources` without costing the others their quote.
They do not all price the same question. LayerZero prices an exact source amount only, so an exact-output bridge — “leave me with exactly 1,000 USDC on Arbitrum” — reports `unsupported_quote_type` for LayerZero and is served by Across and LI.FI, which price both directions.
The part worth building around is what happens after the plan is signed. A bridge is the one execution plan whose successful origin receipt does not mean the user has their funds, so a LayerZero or LI.FI option is not finished when the origin transaction confirms. `get_value_transfer_status` reports where the transfer has reached, keyed by that option’s `source`:
| Source | Lookup key |
| ----------- | -------------------------------------------------------- |
| `layerzero` | The option’s `provider_quote_id`, plus the origin hash |
| `lifi` | The origin transaction hash — a quote id is not accepted |
Poll it every fifteen to thirty seconds while `settled` is false and stop as soon as it is true; transfers settle in minutes, and the call draws on the same metered budget as a quote. A status of `UNKNOWN` or `NOT_FOUND` in the first moments after submission means the transfer has not been indexed yet, not that it was lost. On a settled LI.FI transfer, read `substatus` before reporting delivery: `REFUNDED` and `PARTIAL` are both filed under status `DONE`, and only one of those means the money arrived.
Across transfers execute the same way but are not tracked by this tool.
## Jurisdiction restrictions
[Section titled “Jurisdiction restrictions”](#jurisdiction-restrictions)
Some assets may not be traded by some investors. Today the restriction policy covers Robinhood Chain, where Robinhood Stock Tokens are tokenised debt securities subject to issuer restrictions and applicable law. The server only informs: it does not know, infer or check the user’s jurisdiction, never refuses a request on jurisdiction grounds, and does not read the connecting IP. Establishing the user’s eligibility is the agent’s job, and a wallet’s signing policy is not a substitute for it.
Results that move a covered asset carry a `jurisdiction` object (policy `ekubo-token-jurisdictions-v2`, with its `policy_digest`): swap quotes and their plans, the swap phase of a ve(3,3) reinvestment, `prepare_twamm_order`, `prepare_auction_create`, `prepare_lp_position_deposit`, `prepare_fix_pool_price`, `prepare_oracle_capacity_expansion`, and `prepare_transfers` when it sends a Robinhood Stock Token or an unclassified ERC-20. Every asset is listed with its side, classification and provenance, so an empty `restricted_jurisdictions` list is explicit rather than missing. An asset on a covered chain that has not been classified is labeled `coverage: "unknown"` and `execution_hold: true` with null restriction lists; the metadata is not authoritative for it, and it must not be executed until it is classified.
This metadata describes assets, not the user. Before requesting signatures or submitting approvals or trades, the agent establishes the user’s relevant jurisdiction and eligibility, including location, residence or entity incorporation, US-person status under Regulation S, any person for whose account or benefit the transaction is made, and issuer Prohibited Investor restrictions. It asks the user when these are not already known and never infers them from an IP address, the environment or the wallet. If required facts or asset-policy coverage are unknown, it does not proceed, and it does not proceed for a user in a listed jurisdiction.
Restrictions apply to buying and selling alike, and no blanket disposal exemption applies. A quote, execution plan, simulation or wallet approval is not permission to trade. Only the user or an authorized representative of the actual investor may give a factual attestation; an agent must not attest on their behalf, and a known prohibited fact cannot be overridden by one. Keep the attestation client-side: it is never sent to the quote API or to this server.
## Rate limits
[Section titled “Rate limits”](#rate-limits)
The server is public and unauthenticated, so there is no API key to raise a quota against. Limits apply per caller, where a caller is one IPv4 address or one IPv6 /64. No fixed quota is guaranteed and the thresholds are not published, so an agent should react to what the server tells it rather than pace itself against a constant it has memorized.
The part worth designing around is that requests are not all counted the same. Four budgets run at once, and a rejection names the one you hit:
| Scope | What it counts |
| ------------------- | -------------------------------------------------------------------------------------- |
| `burst` | Requests over a few seconds, across every endpoint |
| `sustained` | Requests over a minute, across every endpoint |
| `tool_units` | The weighted cost of tool calls over a minute |
| `metered_providers` | Calls over a minute to the tools that buy quotes or recommendations from a third party |
`tool_units` is the one that catches integrators out. A tool call is charged by what it costs the server to answer, not as one request: `derive_pool_id` hashes a struct locally and costs nothing at all, an ordinary token or pool read is the unit of measure, and a call that fans out across several upstream requests or buys a firm quote costs several times that. Two clients making an identical number of calls per minute can get very different answers, and the cheapest way to stay inside the budget is usually to ask for more per call rather than to call more often.
`metered_providers` is deliberately a separate and much smaller budget rather than a share of the same one, because those calls spend real money with third parties. Sitting comfortably inside `tool_units` does not buy you headroom for quotes.
### When you are limited
[Section titled “When you are limited”](#when-you-are-limited)
A rejection is an HTTP `429` carrying a `Retry-After` header in seconds. Honor that header rather than retrying on a fixed interval.
When the rejected request was a single identified JSON-RPC call, the body is a JSON-RPC error as well, so a client can attach the refusal to the call that caused it rather than to the connection:
```json
{
"jsonrpc": "2.0",
"id": "your-request-id",
"error": {
"code": -32029,
"message": "Tool budget for this minute is exhausted...",
"data": {
"reason": "rate_limited",
"scope": "tool_units",
"retry_after_seconds": 60
}
}
}
```
`data.scope` is worth branching on, because the four scopes do not all mean the same thing. `burst` and `sustained` say you are asking too fast, and waiting is a complete fix. `tool_units` and `metered_providers` say the work you asked for was expensive, and waiting alone will not help if you resume the same pattern afterwards.
Enforcement is approximate, and it is measured close to where your request lands rather than globally. A batch of simultaneous requests can slip through where the same requests sent one after another would not, so treat a `200` as permission for that call rather than as evidence that you are inside the budget.
### Staying inside the budget
[Section titled “Staying inside the budget”](#staying-inside-the-budget)
Batch instead of iterating. `get_tokens` resolves up to 1,000 chain and address pairs in a single call and is charged once; the same thousand lookups issued individually are charged a thousand times. Where a tool offers a search or a filter, narrowing is far cheaper than paging the whole catalog and filtering client-side.
Reuse the quote you already hold. `get_quotes_with_plans` buys firm quotes from providers, and each option it returns already carries the execution plan that executes it. Re-quoting to refresh a plan you already have spends the metered budget a second time and replaces the quote the user approved. Re-quote after an expiry, a revert, or a change to the request, not on a timer.
Respect the freshness windows. Protocol data is cached upstream, and pool state, pool keys, and tick liquidity do not change on every request. The server publishes the specific intervals as `polling_guidance` alongside the rate limit contract; polling faster than those windows spends budget without producing fresher data.
### Request size
[Section titled “Request size”](#request-size)
Independently of the budgets, a single request may not exceed the limits below, as published by server version 0.53.2. The live values are under `operational_semantics.rate_limit_contract.request_limits` at `GET https://mcp.ekubo.org/` and are authoritative if this table drifts.
| Limit | Value | Rejection |
| ------------------------------ | --------------- | --------- |
| Body size | 2,097,152 bytes | `413` |
| JSON-RPC messages per request | 20 | `400` |
| Combined tool cost per request | 40 units | `400` |
The cost ceiling refuses rather than trims: an over-budget request is rejected outright instead of being quietly served at a discount. No single tool call reaches the ceiling on its own, so in practice it only ever asks you to split a batch — and JSON-RPC batching was removed from the protocol in revision 2025-06-18 in any case.
### Reading the contract at runtime
[Section titled “Reading the contract at runtime”](#reading-the-contract-at-runtime)
Rather than hard-coding any of this, a client can read it. `GET https://mcp.ekubo.org/` returns the current contract under `operational_semantics.rate_limit_contract`, covering every scope name, the response format, and the request-size limits, and `https://mcp.ekubo.org/llms.txt` carries the same summary in prose. Those are authoritative if this page ever drifts from them.
## Notes
[Section titled “Notes”](#notes)
* Supported on EVM chains, including Robinhood Chain (chain ID **4663** — the L2, not the brokerage).
* The server is versioned and advertises `listChanged` for both tools and resources, so clients are notified when its surface changes.
* Underlying protocol data comes from the same public [Ekubo API](/api/) and [Quoter API](/api/#quoter) documented here, so results agree with the interface and with your own integrations.
Questions or problems? Ask in the [Discord](https://discord.ekubo.org).
# Rewards and incentives
> Liquidity incentive campaigns, how rewards are measured, and the on-chain drop infrastructure used to distribute them
Ekubo runs liquidity incentive campaigns that pay reward tokens to liquidity providers. Rather than paying for deposited value alone, campaigns measure the **depth a position actually provides near the market price**, so rewards track useful liquidity rather than parked capital.
Rewards accrue continuously and are distributed through periodic on-chain drops that you claim yourself.
## Campaigns
[Section titled “Campaigns”](#campaigns)
A campaign defines what is being rewarded and for how long: a chain, a reward token, a start and end time, the set of pairs included, and which lockers and extensions qualify. Campaigns are listed publicly:
```plaintext
GET https://prod-api.ekubo.org/campaigns?chainId={chainId}
```
Each campaign reports, per pair, the depth currently measured and the amounts `scheduled` and already `distributed`, along with a campaign-level `nextDropTime` — when the next drop is scheduled to land.
To see what a specific position has earned over a period:
```plaintext
GET https://prod-api.ekubo.org/rewards/{chainId}/{locker}/{salt}
```
Rewards are computed from the same on-chain event data served by the open source [indexer](/products/indexer/), so the measurement is reproducible from public data.
## Drops and claiming
[Section titled “Drops and claiming”](#drops-and-claiming)
Accrued rewards are published periodically as a **merkle drop**. Each drop commits to a merkle root covering every recipient and amount; claiming means presenting a proof against that root.
To find what an address can claim right now:
```plaintext
GET https://prod-api.ekubo.org/claims/{address}?chainId={chainId}
```
The [app](https://ekubo.org/rewards) turns these into one-click claims, but the claim is an ordinary on-chain call — nothing gates it.
### The Incentives contract
[Section titled “The Incentives contract”](#the-incentives-contract)
On EVM chains, drops are handled by a single [`Incentives`](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/Incentives.sol) contract — a singleton that hosts many independent airdrops at the deterministic address `0xC52D2656cb8C634263E6A15469588beB9C3Bb738` on every chain.
A drop is identified by its **drop key**: the owner, the reward token, and the merkle root. That means a drop is fully described by what it pays and who it pays — no registry, no per-drop deployment.
| Function | Who can call it | What it does |
| ----------------------------- | ------------------------- | --------------------------------------------------------------------------- |
| `fund(key, minimum)` | Anyone | Tops the drop up to a minimum funded amount, pulling tokens from the caller |
| `claim(key, claimKey, proof)` | Anyone with a valid proof | Claims a recipient’s allocation; each claim can only be made once |
| `refund(key)` | The drop owner | Returns whatever remains unclaimed |
Funding is permissionless: anyone can fund a drop, including third parties who want to add to a campaign. Claims are tracked in a bitmap so a claim cannot be replayed, and the contract is multicallable, so several claims can be batched into one transaction.
On Starknet, the equivalent role is played by the [`Airdrop`](https://github.com/EkuboProtocol/governance) contract in the governance repository, which uses the same merkle-proof approach and supports batched claims. It is what distributed the original [EKUBO token](/user-guides/ekubo-token/) airdrop, and it has no claim deadline.
## Running your own campaign
[Section titled “Running your own campaign”](#running-your-own-campaign)
Because both the campaign measurement data and the drop contract are public, incentive programs are not limited to the DAO:
* Compute allocations from indexed liquidity data — your own instance of the [indexer](/products/indexer/) is enough
* Publish a merkle root and fund the drop through `Incentives.fund(...)`
* Recipients claim against it permissionlessly; you can reclaim whatever goes unclaimed
For incentives that pay continuously rather than in periodic drops, the **boosted fees** extension streams externally funded rewards directly to a pool’s LPs — see [Providing liquidity](/products/liquidity/), or [Ve33](/products/ve33/) if you want token holders to decide where those rewards go.
# Trading
> How trading works on Ekubo: routing across every pool, quotes that account for extensions, and single-transaction execution
A trade on Ekubo goes through three stages: finding a route, quoting it, and executing it. Each stage is available as public infrastructure you can use directly.
## Routing and quoting
[Section titled “Routing and quoting”](#routing-and-quoting)
Ekubo’s liquidity is spread across pool types and [extensions](/concepts/extensions/) — concentrated, stableswap, full-range, TWAMM, MEV capture, Ve33 — all inside the same Core contract. A good route often splits a trade across several of them.
The [Quoter API](/api/#quoter) does this for you. Given a chain, an amount, and a token pair, it returns **block-pinned split routes** for exact-input or exact-output swaps:
```plaintext
GET https://prod-api-quoter.ekubo.org/{chainId}/{amount}/{specifiedToken}/{otherToken}
```
A negative amount requests an exact-output quote. Token addresses and decimals resolve through the [Ekubo API](/api/) token list.
Because quotes are pinned to a block, they reflect exact pool state at that block — including extension behavior, which is the part hand-rolled integrations most often get wrong.
## Simulation
[Section titled “Simulation”](#simulation)
If you need to compute quotes yourself — running your own solver, backtesting, or simulating without a network round-trip — the [SDKs](/integration-guides/sdks/) implement the same math the contracts do.
The Rust SDK ([`ekubo_sdk`](https://crates.io/crates/ekubo_sdk)) is the most complete: its quoting module implements every pool type and extension, so simulated quotes match on-chain execution rather than approximating it. It is `no_std`-compatible, so it runs in constrained environments as well as on a server.
For pool math in TypeScript — tick and price conversions, liquidity sizing, swap steps — use [`@ekubo/sdk`](https://www.npmjs.com/package/@ekubo/sdk).
## Execution
[Section titled “Execution”](#execution)
On EVM chains, swaps execute through the [Yul Router](/integration-guides/yul-router/), deployed at the same address on every supported chain. Routes are encoded with [`@ekubo/yul-router-sdk`](https://www.npmjs.com/package/@ekubo/yul-router-sdk) and sent as raw calldata — there is no swap selector, because the calldata *is* the route. The same route can be priced without executing it through the router’s `quote(bytes)` entrypoint.
The router executes every split of a trade under a **single Core lock**, aggregates the amounts, applies **one slippage check** against the total, and settles token transfers once. This is what makes multi-pool routing on Ekubo cheap: [flash accounting](/concepts/architecture/) means intermediate hops never touch the token contracts.
Trading from your own contract on either chain follows the same lock-and-callback flow — see [Swapping](/integration-guides/swapping/).
## Trade types beyond spot
[Section titled “Trade types beyond spot”](#trade-types-beyond-spot)
* **[DCA orders](/user-guides/dollar-cost-average-orders/)** — sell a token gradually over a period, netted against opposing orders, using the TWAMM extension
* **Limit orders** (Starknet) — narrow positions one tick spacing wide (128 ticks) that execute at a chosen price and are pulled automatically once filled
* **[Signed exclusive swaps](/integration-guides/signed-exclusive-swaps/)** — RFQ-style pools where a market maker signs each quote off-chain with its own fee and bounds
## MEV capture
[Section titled “MEV capture”](#mev-capture)
The MEV capture extension charges an additional fee on swaps that move a pool’s price significantly, and directs that value back to the pool’s liquidity providers rather than to searchers. Pools using it are routed and quoted like any other pool.
### How MEV capture fees are credited
[Section titled “How MEV capture fees are credited”](#how-mev-capture-fees-are-credited)
MEV capture holds its additional fee in the pool and credits it to liquidity providers at the first interaction with the pool in a later block timestamp. On chains with sub-second blocks, several blocks can share one timestamp. The credit goes to the liquidity that is active at the pool price at that moment, not to the liquidity the surcharged swap traded through. Positions added in the same block timestamp after a surcharged swap share in that swap’s additional fee, and positions removed in that timestamp do not. If the price was left in a range with no liquidity, that amount is not credited to anyone. A position near the edge of the active range can therefore receive more or less than its share of a given swap’s additional fee, in some cases all of it or none of it. Ekubo estimates that from June 2025 to October 2026 this affected about 0.15% of the additional fees paid out, with no sign of deliberate exploitation. That is a historical estimate, not a limit. Deployed MEV capture extensions cannot be changed, so this applies to every existing MEV capture pool.
# Ve33 and STONX
> A token-governed liquidity marketplace anyone can deploy on Ekubo Core, and STONX, the first ecosystem deployment
Ecosystems — chains, protocols, launchpads — often need liquidity across a set of related markets. **Ve33** turns that into a marketplace: holders lock a stake token, direct emissions to the pools they want liquidity in, set those pools’ fees, and earn the trading fees the pools generate. Because voters are paid from the fees of the pools they support, they have a direct financial stake in allocating well.
Ve33 is an [extension](/concepts/extensions/) on Ekubo Core. Anyone can deploy an instance around a token of their choice, including an existing token. It is permissionless and ownerless: no admin, no treasury cut, no weekly epoch, and emission policy lives outside the base extension.
The design descends from Curve’s veCRV and Solidly’s ve(3,3) — later refined by Velodrome and Aerodrome — with one significant addition: **voters set the pool fee**, not just the emission share. Background and economics: [Ve33: a token-governed liquidity marketplace](https://blog.ekubo.org/ve33-liquidity-marketplace/).
## Tokenomics
[Section titled “Tokenomics”](#tokenomics)
**Locking.** Holders lock the stake token for up to four years. Voting power is linear in both size and remaining time:
```plaintext
voting power = stake amount × (unlock time − now) / 4 years
```
A lock is represented by a transferable ERC-721 through the `VeToken` wrapper, whose metadata and SVG image are rendered entirely on-chain. Stakes can be increased, extended, split into several positions, or merged.
**Voting.** A stake assigns its full voting power to one pool — split a stake to support several. Each vote carries two decisions:
1. **Emissions** — the pool’s share of the global emission stream
2. **Fee** — the swap fee that pool charges
The active pool fee is the voting-power-weighted average of all fee votes on that pool. A pool with no active vote weight charges a zero extension fee.
**Who earns what.** This is the part that differs most from an ordinary AMM:
| Participant | Earns |
| ----------------------- | ----------------------------------------------------------------------------------------------------- |
| **Voters** | The full swap fees of the pools they vote for, proportional to vote weight — no treasury share |
| **Liquidity providers** | Emissions of the stake token, by pool vote weight and their active liquidity — **not** Core swap fees |
LP rewards are range-aware: concentrated positions earn while in range, using the same inside/outside accounting Core uses for fees. Stableswap positions use global pool reward growth.
**Continuous accounting.** Unlike weekly-epoch ve(3,3) systems, fees and emissions accrue through on-chain growth indices updated whenever a pool or position is touched. There is no weekly distribution transaction. Vote weights, by contrast, are *sampled* — stored pool weights change only when a vote is cast or refreshed, so stake owners should renew votes as locks approach expiry.
**Emissions are modular.** Anyone can fund an emission schedule over a chosen interval by calling `scheduleEmissions(startTime, endTime, rate)` through the Ve33 periphery. Emission policy therefore lives outside the extension: a community can place the funding source — a mint authority, a treasury, or anything else — under whatever governance it prefers.
Because Ve33 pools use Ekubo Core underneath, they inherit singleton settlement, flash accounting, high-precision concentrated liquidity, stableswap support, and the shared routing surface. Swaps must be forwarded through a Ve33-aware router — direct Core swaps revert, since the voter-selected fee is applied by the extension.
## STONX on Robinhood Chain
[Section titled “STONX on Robinhood Chain”](#stonx-on-robinhood-chain)
**STONX** is the first ecosystem deployment of Ve33: a stake and reward token for bootstrapping capital-efficient **stock token liquidity** on [Robinhood Chain](https://robinhood.com/us/en/newsroom/robinhood-chain-launches-public-testnet).
Note
STONX is a [DAO proposal](https://blog.ekubo.org/stonx-on-robinhood-chain/) under discussion. Details are subject to change and it will not proceed without Ekubo DAO approval on a specific version.
As proposed, the launch mints **999,999 STONX** in three equal parts:
| Allocation | Amount | Purpose |
| ------------- | ------------- | --------------------------------------------------------------------- |
| Ekubo, Inc. | 333,333 STONX | Max-staked to direct initial emissions and fund continued development |
| DAO liquidity | 333,333 STONX | Paired with 333,333 USDG as DAO-owned STONX/USDG liquidity |
| Emissions | 333,333 STONX | 100 days of bootstrap emissions (about 3,333 STONX/day) |
The DAO would pair its allocation with 333,333 USDG in a STONX/USDG Ve33 pool at an initial price of $1.00 per STONX, with the DAO’s position spanning the full usable tick range, funded by selling existing DAO liquidity positions on Ethereum.
**Emission policy.** The initial 333,333 STONX is scheduled directly through the Ve33 periphery over **100 days**, an average of about **3,333 STONX/day**, to bootstrap liquidity. Because a funded schedule is committed on-chain, emissions already scheduled cannot be withdrawn or reduced; further emissions require funding a new schedule.
**What the DAO controls.** STONX mint authority is transferred to governance through the owner proxy on Robinhood Chain. This is the DAO’s *only* form of ownership over the system: it can approve additional mints — to fund further emission schedules, partnerships, or distributions — by proposal. It cannot control votes, fees, or existing liquidity, and cannot claw back emissions already scheduled.
Ekubo, Inc. would max-stake its allocation and direct initial votes toward stock-token markets, with the resulting fee income funding development and maintenance.
The open questions are the ones voters answer continuously: which stock-token markets deserve emissions, what fee gives traders competitive execution while producing useful voter revenue, and whether the volume that follows each unit of emissions justifies the issuance.
## For developers
[Section titled “For developers”](#for-developers)
* [Ve33 user guide](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/docs/ve33-user-guide.md) — operational detail, including vote clearing, reward claiming, and expired stakes
* [Architecture notes](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/docs/ve-integrated-extension.md)
* [`Ve33.sol`](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/extensions/Ve33.sol), [`VeToken.sol`](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/VeToken.sol), [`Ve33Periphery.sol`](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/Ve33Periphery.sol)
The Ve33 accounting is covered by unit, fuzz, and stateful invariant tests for stake backing, voter-fee solvency, emission solvency, vote consistency, and range-aware LP rewards — see [Audits](/reference/audits/).
# Audits
> Audit reports for the Ekubo Protocol contracts
## Ekubo V3 (EVM)
[Section titled “Ekubo V3 (EVM)”](#ekubo-v3-evm)
The current EVM contracts are immutable and have been reviewed by multiple independent auditors and a public competitive audit. All reports live in the [audits directory](https://github.com/EkuboProtocol/evm-contracts/tree/v3.2.0/audits) of the source repository.
| Report | Scope | Date |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| [Code4rena competitive audit](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/audits/Code4rena%20x%20Ekubo%20audit%20report%202025-11.pdf) | The V3 protocol, reviewed publicly by a competitive audit field | November 19 – December 10, 2025 |
| [Riley Holterhus audit](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/audits/Ekubo-Riley-Holterhus-Audit.pdf) | Core protocol | November 18, 2025 |
| [Riley Holterhus update](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/audits/Ekubo-Feb-2026-Update-Riley-Holterhus-Audit.pdf) | Changes since the original review | February 2, 2026 |
| [Auctions audit](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/audits/Ekubo-Auctions-Riley-Holterhus-Audit.pdf) — Riley Holterhus | The Auctions contract | February 18, 2026 |
| [SignedExclusiveSwap review](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/audits/SignedExclusiveSwap-Extension-Audit.md) | The [signed exclusive swap](/integration-guides/signed-exclusive-swaps/) extension | June 30, 2026 |
| [Ve33 invariants](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/audits/ve33-audit-invariants.md) and [invariant verification](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/audits/ve33-audit-invariant-verification.md) | [Ve33](/products/ve33/) stake backing, voter-fee and emission solvency, vote consistency, and range-aware LP rewards | July 2, 2026 |
| [AI audit scan](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/audits/EkuboProtocol%20Audit%20Scan%20-%20AI%20Scan.pdf) — Apex Security Review by Cantina | Automated review pass across the EVM contracts, Starknet contracts, governance, and the Yul router | July 24, 2026 |
## Starknet
[Section titled “Starknet”](#starknet)
The Starknet contracts have been reviewed by Plainshift and, across several separate engagements, by Nethermind Security.
| Report | Scope | Date |
| -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| [Plainshift](/assets/plainshift%20ekubo.pdf) | All Starknet contracts | February 14, 2025 |
| [Nethermind Security — core contracts](/assets/NM0123_EKUBO_FINAL_PUBLIC.pdf) (partially redacted) | Core, router, positions and NFT contracts — a fifteen engineer-week review of 4,044 lines | March 22, 2024 |
| [Nethermind Security — TWAMM extension](/assets/NM0205_EKUBO_TWAMM.pdf) | The TWAMM extension, which powers [DCA-enabled pools and DCA orders](/user-guides/dollar-cost-average-orders/) — a six engineer-week review of 1,681 lines | April 4, 2024 |
| [Nethermind Security — revenue buybacks](/assets/NM_0282_FINAL_Ekubo_Revenue_Buybacks.pdf) | The first version of the revenue buybacks contract | September 2, 2024 |
| [Nethermind Security — limit orders extension](/assets/NM0369_EKUBO_LIMIT_ORDERS.pdf) | The limit orders extension, for which order placement is now deprecated — a six engineer-week review of 568 lines | January 6, 2025 |
## Governance
[Section titled “Governance”](#governance)
| Report | Scope | Date |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------- |
| [Cairo Security Clan](/assets/Ekubo_Governance_L1_Proxy.pdf) | The Starknet L1 proxy, which lets Starknet governance control contracts on Ethereum | January 22, 2025 |
| [AI audit scan](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/audits/EkuboProtocol%20Audit%20Scan%20-%20AI%20Scan.pdf) — Apex Security Review by Cantina | Automated review pass; its scope included the governance contracts | July 24, 2026 |
## Legacy deployments
[Section titled “Legacy deployments”](#legacy-deployments)
Audits of the deprecated EVM V2 deployment
These reports cover the [EVM V2 contracts](/reference/contracts/evm-v2/), which are deprecated and superseded by V3. They are retained for reference only.
| Report | Scope | Date |
| ----------------------------------------------------------------------- | ----------------------------------------------------------- | ---------------------------- |
| [Plainshift](/assets/Ekubo%20EVM%20Deployment%20Plainshift%20Audit.pdf) | The V2 deployment: core, periphery and the Oracle extension | February 24 – March 17, 2025 |
| [ABDK](/assets/ABDK_Ekubo_EkuboProtocol_v_1_0.pdf) | V2 core | April 1, 2025 |
| [ABDK](/assets/ABDK_Ekubo_TWAMM_v_1_0.pdf) | V2 TWAMM | April 27, 2025 |
| [ABDK](/assets/ABDK_Ekubo_InvariantAnalysis_v_1_0.pdf) | V2 TWAMM invariant analysis | April 12, 2025 |
# Contract addresses
> Addresses of all deployed Ekubo Protocol contracts
Deployed contract addresses by deployment:
* [EVM (V3)](/reference/contracts/evm-v3/) — the current EVM deployment: same deterministic addresses on every supported chain
* [Peripheral ownership](/reference/contracts/peripheral-ownership/) — dated per-chain record of who owns the EVM Positions, Orders and Auctions contracts
* [Starknet](/reference/contracts/starknet/) — the original Ekubo deployment on Starknet
* [Starknet Sepolia](/reference/contracts/starknet-sepolia/) — the Starknet testnet deployment
* [Governance](/reference/contracts/governance/) — the EKUBO token, Staker, and Governor contracts on Ethereum and Starknet mainnet
* [Governance Sepolia](/reference/contracts/governance-sepolia/) — the governance testnet deployments
* [EVM (V2, deprecated)](/reference/contracts/evm-v2/) — the deprecated Ethereum-only V2 deployment
# EVM Contracts (V2)
> Addresses of the deployed contracts on Ethereum (V2)
Note
The EVM contracts are now deprecated.
All smart contracts on Ethereum are **immutable**. The `owner` role in the contract can *only* withdraw protocol fees, and is set to the L1 StarknetOwnerProxy ([Etherscan](https://etherscan.io/address/0x1e0ef4162e42c9bf820c307218c4e41ccca6e9cc)) contract, representing the governor on Starknet.
There are two kinds of contracts in Ekubo Protocol, stateful and stateless. Stateless contracts may be redeployed at any time for the use in our backend and interface. Stateful contracts are *never redeployed*, i.e. their addresses will never change.
The addresses of all Ethereum contracts are the same on Sepolia and Mainnet. The contracts are not deployed to any other EVM chain.
| Contract | Address |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Core | `0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444` [Mainnet](https://etherscan.io/address/0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444#readContract), [Sepolia](https://sepolia.etherscan.io/address/0xe0e0e08A6A4b9Dc7bD67BCB7aadE5cF48157d444#readContract) |
| Positions | `0xA37cc341634AFD9E0919D334606E676dbAb63E17` [Mainnet](https://etherscan.io/address/0xA37cc341634AFD9E0919D334606E676dbAb63E17#readContract), [Sepolia](https://sepolia.etherscan.io/address/0xA37cc341634AFD9E0919D334606E676dbAb63E17#readContract) |
| Oracle | `0x51d02A5948496a67827242EaBc5725531342527C` [Mainnet](https://etherscan.io/address/0x51d02A5948496a67827242EaBc5725531342527C#readContract), [Sepolia](https://sepolia.etherscan.io/address/0x51d02A5948496a67827242EaBc5725531342527C#readContract) |
| TWAMM | `0xd4279c050da1f5c5b2830558c7a08e57e12b54ec` [Mainnet](https://etherscan.io/address/0xD4279c050DA1F5c5B2830558C7A08E57e12b54eC), [Sepolia](https://sepolia.etherscan.io/address/0xd4279c050da1f5c5b2830558c7a08e57e12b54ec) |
| Orders | `0xae1430e3e089794beacba260657fcd0f0967c18a` [Mainnet](https://etherscan.io/address/0xae1430e3e089794beacba260657fcd0f0967c18a), [Sepolia](https://sepolia.etherscan.io/address/0xae1430e3e089794beacba260657fcd0f0967c18a) |
| Router (Stateless) | `0x9995855C00494d039aB6792f18e368e530DFf931` [Mainnet](https://etherscan.io/address/0x9995855C00494d039aB6792f18e368e530DFf931#readContract), [Sepolia](https://sepolia.etherscan.io/address/0x9995855C00494d039aB6792f18e368e530DFf931#readContract) |
| MEVResistRouter (Stateless) | `0x0c95eA31e4501B3b879Cae2232087E478D44aEAB` [Mainnet](https://etherscan.io/address/0x0c95eA31e4501B3b879Cae2232087E478D44aEAB#code), [Sepolia](https://sepolia.etherscan.io/address/0x0c95eA31e4501B3b879Cae2232087E478D44aEAB#code) |
| MEVResist | `0x553a2EFc570c9e104942cEC6aC1c18118e54C091` [Mainnet](https://etherscan.io/address/0x553a2efc570c9e104942cec6ac1c18118e54c091#internaltx), [Sepolia](https://sepolia.etherscan.io/address/0x553a2efc570c9e104942cec6ac1c18118e54c091#internaltx) |
| PriceFetcher (Stateless) | `0x5a9093673cdc9f7cae8b0d177797040b758205be` |
| CoreDataFetcher (Stateless) | `0x208bb00c6b142351e4a431f6dd323691ebb7c285` |
| QuoteDataFetcher (Stateless) | `0x91cB8a896cAF5e60b1F7C4818730543f849B408c` |
| TokenWrapperFactory (Stateless) | `0x2b8d80d891C1E20aca70fF8a85714aa1900Ab120` [Mainnet](https://etherscan.io/address/0x2b8d80d891c1e20aca70ff8a85714aa1900ab120), [Sepolia](https://sepolia.etherscan.io/address/0x2b8d80d891c1e20aca70ff8a85714aa1900ab120) |
| TokenWrapperPeriphery (Stateless) | `0x2992e49d73Cc97FbC69c5498F332984C9a89f5d6` [Mainnet](https://etherscan.io/address/0x2992e49d73cc97fbc69c5498f332984c9a89f5d6), [Sepolia](https://sepolia.etherscan.io/address/0x2992e49d73cc97fbc69c5498f332984c9a89f5d6) |
| RevenueBuybacks | TBD |
# EVM Contracts (V3)
> Architecture and deployed contracts for Ekubo Protocol V3 on EVM chains
Ekubo Protocol V3 is the open-source EVM deployment of Ekubo. The source code lives at [EkuboProtocol/evm-contracts](https://github.com/EkuboProtocol/evm-contracts) and is licensed under the [Ekubo DAO Shared Revenue License](https://ekubo-license-v1.eth.link/). The authoritative deployment table and ABIs are published with each [GitHub release](https://github.com/EkuboProtocol/evm-contracts/releases/latest) — the tables below reflect release `v3.2.0`.
## Architecture
[Section titled “Architecture”](#architecture)
* **Core** is an ownerless, permissionless singleton holding all pools and tokens, using [flash accounting](/concepts/key-concepts/#flash-accounting) and the [“till” pattern](/concepts/architecture/). All interactions start with `lock()`; Core calls back into your contract, which performs swaps and position updates and settles net balances at the end. Native ETH is supported directly as `address(0)` (so ETH is always `token0`).
* **Pool types** — a pool’s configuration is packed into a single word (`PoolConfig`: extension, fee, and pool-type parameters):
* **Concentrated liquidity** — tick-spacing-parameterized, as on Starknet
* **Stableswap** — liquidity concentrated around a configurable center tick with an amplification factor
* **Full range** — a zero-amplification stableswap pool; the cheapest option
* **Fees** are a `uint64` binary fraction of `2^64` (e.g. 0.3% = `55340232221128654`). See [Price representation](/reference/price-representation/) for fee and price encodings.
* **Extensions** customize pool behavior at eight lifecycle call points. On EVM, an extension’s call points are **encoded in the top byte of its own address** (extensions are deployed by mining an address with the right bits).
* **Periphery** (Positions, Orders, Router, Incentives, …) is where protocol fees are applied — Core itself takes no fees. The canonical Positions deployment applies a 10% protocol fee to collected swap fees (for the Ekubo DAO) and no withdrawal fee.
For the design rationale, read the [V3 whitepaper](/about-ekubo/v3-whitepaper/).
## Supported chains
[Section titled “Supported chains”](#supported-chains)
Ekubo V3 is deployed on the chains below. This page is the authoritative address reference; the [GitHub releases](https://github.com/EkuboProtocol/evm-contracts/releases/latest) carry the source and ABIs.
A chain needs two EVM features for the contracts to work: [EIP-1153](https://eips.ethereum.org/EIPS/eip-1153) (`TSTORE`/`TLOAD`, used for flash accounting) and [EIP-7939](https://eips.ethereum.org/EIPS/eip-7939) (`CLZ`, used in the pool math). Where `CLZ` is missing, the contracts deploy at the usual addresses but every swap and position update reverts. Such a chain becomes usable the moment it enables the opcode — no redeployment is needed.
| Chain | Chain ID | Status | Position and order managers |
| --------------- | -------- | ----------------- | --------------------------- |
| Ethereum | 1 | Live | Original |
| Optimism | 10 | Live | 192-bit IDs |
| BNB Smart Chain | 56 | Live | 192-bit IDs |
| Gnosis | 100 | Live | 192-bit IDs |
| Unichain | 130 | Live | 192-bit IDs |
| Polygon | 137 | Live | 192-bit IDs |
| Monad | 143 | Live | Original |
| World Chain | 480 | Awaiting EIP-7939 | 192-bit IDs |
| MegaETH | 4326 | Awaiting EIP-7939 | Original |
| Robinhood Chain | 4663 | Live | Original |
| Base | 8453 | Live | Original |
| Arbitrum | 42161 | Live | Original |
| Ink | 57073 | Live | 192-bit IDs |
**Live** means the contracts are deployed, both required opcodes are active, and the chain is selectable in the [Ekubo interface](https://ekubo.org). Deployments also exist on several testnets, which this page does not enumerate.
Which manager generation applies per chain is explained under [Position and order managers](#position-and-order-managers).
## Shared deterministic deployments
[Section titled “Shared deterministic deployments”](#shared-deterministic-deployments)
These contracts are deployed at the **same address on every chain listed above**. Anyone can deploy them to another compatible network with the [DeployAll script](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/script/DeployAll.s.sol).
### Core and extensions
[Section titled “Core and extensions”](#core-and-extensions)
| Contract | Address (every chain) | Description |
| ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------- |
| [Core](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/Core.sol) | `0x00000000000014aA86C5d3c41765bb24e11bd701` | The singleton AMM: all pools, positions, and token custody |
| [MEVCapture](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/extensions/MEVCapture.sol) | `0x5555fF9Ff2757500BF4EE020DcfD0210CFfa41Be` | Extension charging extra fees on price-moving swaps, directing that value to LPs |
| [Oracle](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/extensions/Oracle.sol) | `0x517E506700271AEa091b02f42756F5E174Af5230` | Extension recording on-chain price history for token pairs (vs native ETH) |
| [TWAMM](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/extensions/TWAMM.sol) | `0xd47f1B1eDCfEaBb08F6eBd8FC337c27E636C75BA` | Time-weighted AMM extension powering [DCA orders](/user-guides/dollar-cost-average-orders/) |
| [BoostedFees (concentrated)](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/extensions/BoostedFees.sol) | `0xd4B54d0ca6979Da05F25895E6e269E678ba00f9e` | Extension streaming boosted fee rewards to concentrated-liquidity pools |
| [BoostedFees (stableswap)](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/extensions/BoostedFees.sol) | `0x948b9C2C99718034954110cB61a6e08e107745f9` | Same, for stableswap pools |
| [ManualPoolBooster](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/ManualPoolBooster.sol) | `0xddb1758118F65e13a91497015B8cB26801402761` | Permissionless funding of boosted-fee campaigns for specific pools |
| [Incentives](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/Incentives.sol) | `0xC52D2656cb8C634263E6A15469588beB9C3Bb738` | Distributes liquidity incentive campaigns |
| [TokenWrapperFactory](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/TokenWrapperFactory.sol) | `0xAA166592922C4020cEfA23448054AD070211790a` | Wraps non-standard tokens for safe use with Ekubo |
| [MEVCaptureRouter (legacy)](https://github.com/EkuboProtocol/evm-contracts/blob/v3.1.1/src/MEVCaptureRouter.sol) | `0xd26f20001a72a18C002b00e6710000d68700ce00` | Legacy `v3.1.1` router for MEVCapture pools (superseded by the configurable Router) |
### Lens (read-only data fetchers)
[Section titled “Lens (read-only data fetchers)”](#lens-read-only-data-fetchers)
| Contract | Address (every chain) | Description |
| ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- | ---------------------------------------------------------------------------------------- |
| [CoreDataFetcher](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/lens/CoreDataFetcher.sol) | `0xF68F25CA6C817733b7B15a42191AE72A34d56a2B` | Pool state, [prices](/integration-guides/reading-pool-price/), positions, saved balances |
| [QuoteDataFetcher](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/lens/QuoteDataFetcher.sol) | `0x5a3F0F1dA4Ac0c4b937d5685f330704c8e8303f1` | Batched data for computing swap quotes |
| [TWAMMDataFetcher](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/lens/TWAMMDataFetcher.sol) | `0xDEFe25E56a7891CC4c0E1401879f3dC81F1Cc4A6` | DCA order and TWAMM state |
| [IncentivesDataFetcher](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/lens/IncentivesDataFetcher.sol) | `0x69F9eCfa84CF0C41bE9F68b557b07b6b89d71eD0` | Incentive campaign state |
| [PriceFetcher](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/lens/PriceFetcher.sol) | `0xFE0Aa09c1CC2bA299b3AaFA52716bE00f40F1D6d` | Oracle price reads for many tokens at once |
| [TokenDataFetcher](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/lens/TokenDataFetcher.sol) | `0x305Cf9A34dCb265522780D1D64544d3f7C450407` | Token metadata and balances |
| [BoostedFeesDataFetcher](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/lens/BoostedFeesDataFetcher.sol) | `0x50DabB063ab0B62a33abf49f1357f0981cec241F` | Boosted-fees campaign state |
### Position and order managers
[Section titled “Position and order managers”](#position-and-order-managers)
Unlike the contracts above, the managers do **not** share one address across all chains, because `v3.2.0` changed their source.
[`BasePositions`](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/base/BasePositions.sol) now overrides `saltToId` to truncate a generated NFT ID to `uint192`, so the whole token ID can be used directly as the Core position salt rather than a hashed-down version of it. Enabling that override meant marking `BaseNonfungibleToken.saltToId` as `virtual`, which changed the compiled `Orders` bytecode as well — so the Orders manager moved to a new deterministic address even though `Orders.sol` itself is unchanged. Both releases compile with solc 0.8.33; this is a source change, not a recompilation.
There is no migration. Chains that already had the original managers kept them, and chains onboarded afterwards received the new ones.
Both generations are live, and a position minted through either remains valid. To build a **new** transaction, use whichever generation the target chain has, per the [Supported chains](#supported-chains) table:
| Generation | Positions | Orders | Canonical on |
| -------------------------------------------------------------------------------------------- | -------------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------- |
| **Original** ([`v3.1.1`](https://github.com/EkuboProtocol/evm-contracts/tree/v3.1.1/src)) | `0x02D9876A21AF7545f8632C3af76eC90b5ad4b66D` | `0x3325428adB409c239E88ca472F50b0efe00E98B4` | Ethereum, Monad, MegaETH, Robinhood Chain, Base, Arbitrum |
| **192-bit IDs** ([`v3.2.0`](https://github.com/EkuboProtocol/evm-contracts/tree/v3.2.0/src)) | `0xA2971E0C37cFdb13aE8440A0C94Ef1A1af39e326` | `0x9bB520B6192F71ec3D015C8a74F914f9c94bF794` | Optimism, BNB Smart Chain, Gnosis, Unichain, Polygon, World Chain, Ink |
Most chains in the first row carry **both** generations, because the `v3.2.0` managers were deployed everywhere afterwards; only Robinhood Chain has the original alone. Where both exist the original is canonical, which is what the Ekubo interface builds against. [Positions](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/Positions.sol) is the ERC-721 manager for [liquidity positions](/user-guides/add-liquidity/) and applies a 10% swap protocol fee for the Ekubo DAO (and no withdrawal fee); [Orders](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/Orders.sol) is the ERC-721 manager for [DCA orders](/user-guides/dollar-cost-average-orders/). A few chains also carry earlier manager deployments that are not canonical and should not be used for new transactions.
Each manager has an `owner` role: on Positions it can withdraw accrued protocol fees to a recipient the owner chooses and administer metadata and ownership; on Orders it administers metadata and ownership only. Who holds that role differs by chain. It is not held by the DAO on every chain: on BNB Smart Chain and Monad, Ekubo, Inc. still owns the managers. See [Peripheral-contract ownership](/reference/contracts/peripheral-ownership/) for the dated, per-chain record and the [ownership update](/products/governance/#peripheral-contract-ownership-update-9-october-2026) for what it does and does not establish.
### Deployment-specific contracts
[Section titled “Deployment-specific contracts”](#deployment-specific-contracts)
Contracts added or reworked in `v3.2.0` are configuration-specific and have no universal address — their addresses are per deployment:
* [Ve33](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/extensions/Ve33.sol), [VeToken](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/VeToken.sol), [Ve33Positions](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/Ve33Positions.sol), Ve33Periphery, Ve33EmissionRateScheduler, and [Ve33DataFetcher](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/lens/Ve33DataFetcher.sol) — the [Ve33](/products/ve33/) token-governed liquidity system (each instance is deployed around its own stake token, e.g. STONX on Robinhood Chain)
* [SignedExclusiveSwap](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/extensions/SignedExclusiveSwap.sol) — extension for controller-signed EIP-712 swaps ([integration guide](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/signed-exclusive-swap-extension.md))
* [Router](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/Router.sol) — configurable router executing Core swaps and forwarding to MEVCapture / Ve33 pools
* [PoolKeyIndex](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/PoolKeyIndex.sol) — optional registry for discovering initialized pool keys by pool ID, token, or extension
* [Auctions](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/Auctions.sol) — on-chain auctions ([whitepaper](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/auctions-whitepaper.md))
* [MintableERC20](https://github.com/EkuboProtocol/evm-contracts/blob/v3.2.0/src/MintableERC20.sol) — owner-mintable token used by deployments that need one (e.g. a Ve33 stake token)
Audit reports for the EVM contracts are linked on the [Audits](/reference/audits/) page.
# Governance Contracts
> Addresses of the governance contracts on Starknet and Ethereum mainnet
## Governance contracts
[Section titled “Governance contracts”](#governance-contracts)
These contracts are responsible for the management of the core contracts. The source code is [here](https://github.com/EkuboProtocol/governance). Note the EKUBO token used in governance is deployed to Ethereum and bridged to Starknet via Starkgate. The code for the EKUBO token is [here](https://github.com/EkuboProtocol/token).
This page lists the **mainnet** deployments. Testnet addresses are on the [Governance Sepolia](/reference/contracts/governance-sepolia/) page. For how the system is designed — what governance controls and how proposals pass — see [Governance](/products/governance/); for the practical steps, see [Participate in governance](/user-guides/governance/).
| Contract / Network | Address |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| EKUBO (Ethereum) | `0x04C46E830Bb56ce22735d5d8Fc9CB90309317d0f` [Etherscan](https://etherscan.io/address/0x04c46e830bb56ce22735d5d8fc9cb90309317d0f) |
| EKUBO | `0x075afe6402ad5a5c20dd25e10ec3b3986acaa647b77e4ae24b0cbc9a54a27a87` [Voyager](https://voyager.online/contract/0x075afe6402ad5a5c20dd25e10ec3b3986acaa647b77e4ae24b0cbc9a54a27a87) [Starkscan](https://starkscan.co/contract/0x075afe6402ad5a5c20dd25e10ec3b3986acaa647b77e4ae24b0cbc9a54a27a87) |
| Staker | `0x02a3ed03046e1042e193651e3da6d3c973e3d45c624442be936a374380a78bb5` [Voyager](https://voyager.online/contract/0x02a3ed03046e1042e193651e3da6d3c973e3d45c624442be936a374380a78bb5) [Starkscan](https://starkscan.co/contract/0x02a3ed03046e1042e193651e3da6d3c973e3d45c624442be936a374380a78bb5) |
| Governor | `0x053499f7aa2706395060fe72d00388803fb2dcc111429891ad7b2d9dcea29acd` [Voyager](https://voyager.online/contract/0x053499f7aa2706395060fe72d00388803fb2dcc111429891ad7b2d9dcea29acd) [Starkscan](https://starkscan.co/contract/0x053499f7aa2706395060fe72d00388803fb2dcc111429891ad7b2d9dcea29acd) |
| Streamed Payments | `0x006ce8b1f418f7e28e85ea355a069e71ad33a6da791fe3550a94a84f693755a4` [Voyager](https://voyager.online/contract/0x006ce8b1f418f7e28e85ea355a069e71ad33a6da791fe3550a94a84f693755a4) [Starkscan](https://starkscan.co/contract/0x006ce8b1f418f7e28e85ea355a069e71ad33a6da791fe3550a94a84f693755a4) |
| L1 Starknet Proxy | `0x1E0EF4162e42C9bF820c307218c4E41cCcA6E9CC` [Etherscan](https://etherscan.io/address/0x1e0ef4162e42c9bf820c307218c4e41ccca6e9cc#readContract) |
# Governance Sepolia Contracts
> Addresses of the governance contracts on Starknet and Ethereum Sepolia testnets
This page lists the **Sepolia testnet** deployments. Production addresses and source code links are on the [Governance](/reference/contracts/governance/) page.
## Governance contracts
[Section titled “Governance contracts”](#governance-contracts)
| Contract / Network | Address |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| EKUBO (Ethereum) | `0x618C25b11a5e9B5Ad60B04bb64FcBdfBad7621d1` [Etherscan](https://sepolia.etherscan.io/address/0x618C25b11a5e9B5Ad60B04bb64FcBdfBad7621d1) |
| EKUBO | `0x01fad7c03b2ea7fbef306764e20977f8d4eae6191b3a54e4514cc5fc9d19e569` [Voyager](https://sepolia.voyager.online/contract/0x01fad7c03b2ea7fbef306764e20977f8d4eae6191b3a54e4514cc5fc9d19e569) [Starkscan](https://sepolia.starkscan.co/contract/0x01fad7c03b2ea7fbef306764e20977f8d4eae6191b3a54e4514cc5fc9d19e569) |
| Staker | `0x0359c9a32c758a162a6c59a9cff93f09a2eb2dbe82cba9ecbdd64aa4314f8687` [Voyager](https://sepolia.voyager.online/contract/0x0359c9a32c758a162a6c59a9cff93f09a2eb2dbe82cba9ecbdd64aa4314f8687) [Starkscan](https://sepolia.starkscan.co/contract/0x0359c9a32c758a162a6c59a9cff93f09a2eb2dbe82cba9ecbdd64aa4314f8687) |
| Governor | `0x048bb83134ce6a312d1b41b0b3deccc4ce9a9d280e6c68c0eb1c517259c89d74` [Voyager](https://sepolia.voyager.online/contract/0x048bb83134ce6a312d1b41b0b3deccc4ce9a9d280e6c68c0eb1c517259c89d74) [Starkscan](https://sepolia.starkscan.co/contract/0x048bb83134ce6a312d1b41b0b3deccc4ce9a9d280e6c68c0eb1c517259c89d74) |
| Streamed Payments | `0x038898fc956075394eaecefe7c51e35de9e4d43f7f7fd8c7ea8ad6316663c33e` [Voyager](https://sepolia.voyager.online/contract/0x038898fc956075394eaecefe7c51e35de9e4d43f7f7fd8c7ea8ad6316663c33e) [Starkscan](https://sepolia.starkscan.co/contract/0x038898fc956075394eaecefe7c51e35de9e4d43f7f7fd8c7ea8ad6316663c33e) |
| L1 Starknet Proxy | `0x36e3FDC259A4a8b0775D25b3f9396e0Ea6E110a5` [Etherscan](https://sepolia.etherscan.io/address/0x36e3fdc259a4a8b0775d25b3f9396e0ea6e110a5#readContract) |
# Peripheral-contract ownership (snapshot 2026-10-09)
> Dated snapshot of who owns the Ekubo EVM Positions, Orders and Auctions peripheral contracts on each chain, with the ownership-transfer transactions and DAO owner proxies
Companion to the [ownership update of 9 October 2026](/products/governance/#peripheral-contract-ownership-update-9-october-2026). Every row is verifiable on the chain’s public explorer: the transaction emits `OwnershipTransferred(previousOwner, newOwner)` from the contract and the contract’s `owner()` now returns the new owner. Owner proxies are controlled from Ethereum by the `StarknetOwnerProxy` [`0x1e0ef4162e42c9bf820c307218c4e41ccca6e9cc`](https://etherscan.io/address/0x1e0ef4162e42c9bf820c307218c4e41ccca6e9cc), which only the Ekubo DAO Governor on Starknet ([`0x053499f7aa2706395060fe72d00388803fb2dcc111429891ad7b2d9dcea29acd`](https://voyager.online/contract/0x053499f7aa2706395060fe72d00388803fb2dcc111429891ad7b2d9dcea29acd)) can drive. Previous owner of every transferred contract: the Ekubo, Inc. company key `0x00000C771F6176268D5A9846E0956C3eF58597A1`.
## 1. Ownership transfers (33)
[Section titled “1. Ownership transfers (33)”](#1-ownership-transfers-33)
| # | Chain (ID) | Contract | Address | New owner (DAO owner proxy) | Transaction | Block |
| -- | ---------------------- | ----------------------------------- | -------------------------------------------- | -------------------------------------------- | -------------------------------------------------------------------- | --------- |
| 1 | Ethereum (1) | Orders v3.1.1 | `0x3325428adB409c239E88ca472F50b0efe00E98B4` | `0x1e0ef4162e42c9bf820c307218c4e41ccca6e9cc` | `0xc00a14226876f8e0b73926ba540889167df02e857b12325c0b5c5818ef644bac` | 26150761 |
| 2 | Ethereum (1) | Positions v3.2.0 | `0xA2971E0C37cFdb13aE8440A0C94Ef1A1af39e326` | `0x1e0ef4162e42c9bf820c307218c4e41ccca6e9cc` | `0x4cf2c2e5dc7346fd1db1f03c740cad384f52d5c07ade36adabc899ba3e118ede` | 26150762 |
| 3 | Ethereum (1) | Orders v3.2.0 | `0x9bB520B6192F71ec3D015C8a74F914f9c94bF794` | `0x1e0ef4162e42c9bf820c307218c4e41ccca6e9cc` | `0x350604113d67aea5e56999e1c250b2f849e04aa47652d772c9ec4469f7e4dba4` | 26150763 |
| 4 | Ethereum (1) | Auctions | `0xcb4e1b5fb7b120db0815afa63453c969136c0ec9` | `0x1e0ef4162e42c9bf820c307218c4e41ccca6e9cc` | `0x9fd9e1135bcf79f50003270f6028bff0164c469891d7b1b164c261bd32b10c8b` | 26150764 |
| 5 | Ethereum (1) | Orders (pre-recompile, oldOrdersV3) | `0xfF6cF0Ca6d7a30a60539AcD4bB20B3df84EA0644` | `0x1e0ef4162e42c9bf820c307218c4e41ccca6e9cc` | `0x81554e3fed59c25f5a341bd4115b180c7437dd3fcc2e6346278badefca1d8775` | 26150765 |
| 6 | Base (8453) | Positions v3.1.1 | `0x02d9876a21af7545f8632c3af76ec90b5ad4b66d` | `0xd46197c8c5cba977c4c287f04ddc77f2dc105067` | `0x1bd1cead88b05ee5570217552d27a5e9d5cd929d6d8824166296893e0fc6d84b` | 52355194 |
| 7 | Base (8453) | Orders v3.1.1 | `0x3325428adB409c239E88ca472F50b0efe00E98B4` | `0xd46197c8c5cba977c4c287f04ddc77f2dc105067` | `0x51eb03c0863ae551ae394543d524f4754bf3730cfe7bd47b894ad66ad5175f43` | 52355326 |
| 8 | Base (8453) | Positions v3.2.0 | `0xA2971E0C37cFdb13aE8440A0C94Ef1A1af39e326` | `0xd46197c8c5cba977c4c287f04ddc77f2dc105067` | `0x1d93e2cb8d3b6309c81daa8d47300d60d3fbfa83796c9c253f8834fb6ea0fee3` | 52355372 |
| 9 | Base (8453) | Orders v3.2.0 | `0x9bB520B6192F71ec3D015C8a74F914f9c94bF794` | `0xd46197c8c5cba977c4c287f04ddc77f2dc105067` | `0x55c09d263f3f640de543f0a7fd4272c50d874f5e4fd47a7995f8c79b560723ab` | 52355378 |
| 10 | OP Mainnet (10) | Positions v3.2.0 | `0xa2971e0c37cfdb13ae8440a0c94ef1a1af39e326` | `0xcad6e08e0532d523c10825821d10e3f7c845b0c0` | `0x0a05e1b03f9c315dbf0c7d8868c4e57033dd7e550ed6e055561a951dea04a61b` | 157950480 |
| 11 | OP Mainnet (10) | Orders v3.2.0 | `0x9bb520b6192f71ec3d015c8a74f914f9c94bf794` | `0xcad6e08e0532d523c10825821d10e3f7c845b0c0` | `0x15707d5c173bc5eb7db62fe1ebc28b3b631f0e394cd43827ab6d465302783886` | 157950495 |
| 12 | Arbitrum One (42161) | Positions v3.1.1 | `0x02D9876A21AF7545f8632C3af76eC90b5ad4b66D` | `0x2ed25dec49800edb237de812ab9a4c37b74d3282` | `0x048689ea932ba831db2932dbec5f52b90a801ff3be69393548ad52835845baa9` | 513021097 |
| 13 | Arbitrum One (42161) | Orders v3.1.1 | `0x3325428adB409c239E88ca472F50b0efe00E98B4` | `0x2ed25dec49800edb237de812ab9a4c37b74d3282` | `0x45478972fd80055908cc3321749c55d89fe473c7e44cd5eaf37861706c8a2459` | 513021109 |
| 14 | Arbitrum One (42161) | Positions v3.2.0 | `0xA2971E0C37cFdb13aE8440A0C94Ef1A1af39e326` | `0x2ed25dec49800edb237de812ab9a4c37b74d3282` | `0xac4c1015c70b06e58c70c9b8e67a5942ae0e2b50a22dd4ad0d4866283cfa7040` | 513021122 |
| 15 | Arbitrum One (42161) | Orders v3.2.0 | `0x9bB520B6192F71ec3D015C8a74F914f9c94bF794` | `0x2ed25dec49800edb237de812ab9a4c37b74d3282` | `0x57b6ba1062d08080f77d8a350ccae7183295d96fe771b5c42585a41dd59b99f9` | 513021135 |
| 16 | Arbitrum One (42161) | Auctions | `0xcb4e1b5fb7b120db0815afa63453c969136c0ec9` | `0x2ed25dec49800edb237de812ab9a4c37b74d3282` | `0x4f2e0e8cf987ff995e25c2fc4c7b4db2215fe47be89a78e3131d2afa8ad9c473` | 513021144 |
| 17 | Arbitrum One (42161) | Orders (pre-recompile, oldOrdersV3) | `0xfF6cF0Ca6d7a30a60539AcD4bB20B3df84EA0644` | `0x2ed25dec49800edb237de812ab9a4c37b74d3282` | `0x8fd841293c5d8142259b03df8b2d8891be18d026aa5b7411982e2de83b7a7a03` | 513021158 |
| 18 | Robinhood Chain (4663) | Positions v3.1.1 | `0x02D9876A21AF7545f8632C3af76eC90b5ad4b66D` | `0xcd87828f4f279d3c5fd7af531370298964b5eaab` | `0xb97097c8e1687749267c2f8481077034b5b0200cd90736b91939e97127ae6ffa` | 83681935 |
| 19 | Robinhood Chain (4663) | Orders v3.1.1 | `0x3325428adB409c239E88ca472F50b0efe00E98B4` | `0xcd87828f4f279d3c5fd7af531370298964b5eaab` | `0x2d651858af9b3e8a744f99ab2d1c6d7574e1a042da2a1099573632cf60ead19e` | 83681969 |
| 20 | Unichain (130) | Positions v3.2.0 | `0xa2971e0c37cfdb13ae8440a0c94ef1a1af39e326` | `0x6ed0533539b76d160f7a2c7630c1c85313d78dcc` | `0xa3514cf24b664ed24b558d26d72ec2dce04d7301d3492002d016ac0d1acb7aa1` | 60752003 |
| 21 | Unichain (130) | Orders v3.2.0 | `0x9bB520B6192F71ec3D015C8a74F914f9c94bF794` | `0x6ed0533539b76d160f7a2c7630c1c85313d78dcc` | `0x9436d1fe0c59fda8b71706355a32485b76ba34a4f6e27d9407c7c296563ae324` | 60752064 |
| 22 | World Chain (480) | Positions v3.2.0 | `0xA2971E0C37cFdb13aE8440A0C94Ef1A1af39e326` | `0xdaa3293aab3a1da4f63ffdf5d015bb761f84a065` | `0xcb46875c86080959bc3a357d9fe042dd452954868f163c37fbbdef9647ab3a8b` | 36082365 |
| 23 | World Chain (480) | Orders v3.2.0 | `0x9bB520B6192F71ec3D015C8a74F914f9c94bF794` | `0xdaa3293aab3a1da4f63ffdf5d015bb761f84a065` | `0x3177a9f774c21bc835b5f58b533aab4642d569c4ebecb723e229304a5af06a76` | 36082366 |
| 24 | Ink (57073) | Orders v3.2.0 | `0x9bB520B6192F71ec3D015C8a74F914f9c94bF794` | `0x6ed0533539b76d160f7a2c7630c1c85313d78dcc` | `0x6920fbc3d11ef5357df22e50e3c8ebdfb91c2ca89e9447b91ae406af57b9ec70` | 58002029 |
| 25 | Ink (57073) | Positions v3.2.0 | `0xa2971e0c37cfdb13ae8440a0c94ef1a1af39e326` | `0x6ed0533539b76d160f7a2c7630c1c85313d78dcc` | `0x7327d1a92a3871c898f10f7d47035d5e44c305d4442d043aedb73bb50e7b8b89` | 58001971 |
| 26 | MegaETH (4326) | Positions v3.1.1 | `0x02D9876A21AF7545f8632C3af76eC90b5ad4b66D` | `0x112ae742db22d6a579cf47b225f17c716d27c535` | `0x6ef1f56022d4af7e9f2ff6073d161572181c803b713c212f1b16f7469b7c8b61` | 28703374 |
| 27 | MegaETH (4326) | Orders v3.1.1 | `0x3325428adB409c239E88ca472F50b0efe00E98B4` | `0x112ae742db22d6a579cf47b225f17c716d27c535` | `0xd27a0a9bac830fd7dde0f716e4aca9ad247828c9b8350ee9854621e5b821164d` | 28703377 |
| 28 | MegaETH (4326) | Positions v3.2.0 | `0xA2971E0C37cFdb13aE8440A0C94Ef1A1af39e326` | `0x112ae742db22d6a579cf47b225f17c716d27c535` | `0x0aa173494fa7634d22d34f5db802aa6872861c406bf21deba706bef6db725871` | 28703380 |
| 29 | MegaETH (4326) | Orders v3.2.0 | `0x9bB520B6192F71ec3D015C8a74F914f9c94bF794` | `0x112ae742db22d6a579cf47b225f17c716d27c535` | `0x4d590cdcf390a6c1b4c5b9330e544f73a7cf6bb8b9daeb1f1f7cb2d4c67308f1` | 28703382 |
| 30 | Gnosis (100) | Positions v3.2.0 | `0xA2971E0C37cFdb13aE8440A0C94Ef1A1af39e326` | `0xdaa3293aab3a1da4f63ffdf5d015bb761f84a065` | `0x71df3a10468a9353a23449a4aa9d9af7f3fc11cf86824f27f766ba0dccc17791` | 48659496 |
| 31 | Gnosis (100) | Orders v3.2.0 | `0x9bB520B6192F71ec3D015C8a74F914f9c94bF794` | `0xdaa3293aab3a1da4f63ffdf5d015bb761f84a065` | `0x2733571831a082ca0e5332f866e6496eeabc7bb01af3047df61db7881fc40e4d` | 48659499 |
| 32 | Polygon PoS (137) | Positions v3.2.0 | `0xA2971E0C37cFdb13aE8440A0C94Ef1A1af39e326` | `0x6ed0533539b76d160f7a2c7630c1c85313d78dcc` | `0x84a0973eebe3cadb78dfcac96dbbcf1d18b4e44026c2ea0cc0d5ef1f68777747` | 95197659 |
| 33 | Polygon PoS (137) | Orders v3.2.0 | `0x9bB520B6192F71ec3D015C8a74F914f9c94bF794` | `0x6ed0533539b76d160f7a2c7630c1c85313d78dcc` | `0xff2d65392180628b3cc514489b2153757ba208e0ed15f5ba8e1e1c49a254abcb` | 95197669 |
## 2. DAO owner proxies (11; 6 deployed in this migration)
[Section titled “2. DAO owner proxies (11; 6 deployed in this migration)”](#2-dao-owner-proxies-11-6-deployed-in-this-migration)
| Chain (ID) | Proxy type | Address | Deployed | Deployment transaction |
| ---------------------- | ------------------------------------------------------------------------ | -------------------------------------------- | ---------------------- | -------------------------------------------------------------------- |
| Ethereum (1) | StarknetOwnerProxy (Ethereum root, driven only by the Starknet Governor) | `0x1e0ef4162e42c9bf820c307218c4e41ccca6e9cc` | earlier (pre-existing) | — |
| Base (8453) | OPStackOwnerProxy | `0xd46197c8c5cba977c4c287f04ddc77f2dc105067` | earlier (pre-existing) | — |
| OP Mainnet (10) | OPStackOwnerProxy | `0xcad6e08e0532d523c10825821d10e3f7c845b0c0` | earlier (pre-existing) | — |
| Arbitrum One (42161) | ArbitrumOwnerProxy | `0x2ed25dec49800edb237de812ab9a4c37b74d3282` | earlier (pre-existing) | — |
| Robinhood Chain (4663) | ArbitrumOwnerProxy | `0xcd87828f4f279d3c5fd7af531370298964b5eaab` | earlier (pre-existing) | — |
| Unichain (130) | OPStackOwnerProxy | `0x6ed0533539b76d160f7a2c7630c1c85313d78dcc` | 2026-10-08 | `0x8cbe6bfd1c8c8168cda3fd17d4099ca338767bbd0851ce5bd8ef3e99d184b260` |
| World Chain (480) | OPStackOwnerProxy | `0xdaa3293aab3a1da4f63ffdf5d015bb761f84a065` | 2026-10-08 | `0x60b27139898f7e5ee392ee12953d904537bac8802e242d4587b6a1c7d6822be1` |
| Ink (57073) | OPStackOwnerProxy | `0x6ed0533539b76d160f7a2c7630c1c85313d78dcc` | 2026-10-08 | `0xa96398d2cdc140b40b0be4b257c5983414dad51eb1db64faeed2f63d37d282fd` |
| MegaETH (4326) | OPStackOwnerProxy | `0x112ae742db22d6a579cf47b225f17c716d27c535` | 2026-10-08 | `0x3d8bf877445c634312d8125c043184bd711201b984ec42b152437231a495c36d` |
| Gnosis (100) | AMBOwnerProxy | `0xdaa3293aab3a1da4f63ffdf5d015bb761f84a065` | 2026-10-08 | `0xc04ea38ac53d84e6a1c0d1701e817aae8b6a6746f146985b64acc1cfacf333c7` |
| Polygon PoS (137) | FxPortalOwnerProxy | `0x6ed0533539b76d160f7a2c7630c1c85313d78dcc` | 2026-10-08 | `0xb569f85ed1431c3c9a6a3a41f78e71f590f05aa5530360d42e00d0657ebc48d9` |
Source code for all proxies is published and verified on the chains’ explorers (Sourcify/Blockscout/Etherscan-family). The `OPStackOwnerProxy` and `ArbitrumOwnerProxy` sources are in the public [`EkuboProtocol/governance`](https://github.com/EkuboProtocol/governance) repository (`l1_proxy/`); the `AMBOwnerProxy` and `FxPortalOwnerProxy` sources were merged into the same repository on 9 October 2026 ([pull request #83](https://github.com/EkuboProtocol/governance/pull/83)).
## 3. Contracts that remain owned by the Ekubo, Inc. company key (6)
[Section titled “3. Contracts that remain owned by the Ekubo, Inc. company key (6)”](#3-contracts-that-remain-owned-by-the-ekubo-inc-company-key-6)
| Chain (ID) | Contract | Address | Owner powers |
| -------------------- | ------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------- |
| BNB Smart Chain (56) | Positions 192-bit v3.2.0 | `0xA2971E0C37cFdb13aE8440A0C94Ef1A1af39e326` | withdraw accrued protocol fees to an owner-chosen recipient; set metadata; transfer ownership |
| BNB Smart Chain (56) | Orders 192-bit v3.2.0 | `0x9bB520B6192F71ec3D015C8a74F914f9c94bF794` | set metadata; transfer ownership (no funds) |
| Monad (143) | Positions original v3.1.1 | `0x02D9876A21AF7545f8632C3af76eC90b5ad4b66D` | withdraw accrued protocol fees to an owner-chosen recipient; set metadata; transfer ownership |
| Monad (143) | Orders original v3.1.1 | `0x3325428adB409c239E88ca472F50b0efe00E98B4` | set metadata; transfer ownership (no funds) |
| Monad (143) | Positions 192-bit v3.2.0 | `0xA2971E0C37cFdb13aE8440A0C94Ef1A1af39e326` | withdraw accrued protocol fees to an owner-chosen recipient; set metadata; transfer ownership |
| Monad (143) | Orders 192-bit v3.2.0 | `0x9bB520B6192F71ec3D015C8a74F914f9c94bF794` | set metadata; transfer ownership (no funds) |
Reason: BNB Smart Chain and Monad have no canonical Ethereum message bridge, so no DAO owner proxy exists there yet. Their route is a separate pending company decision.
## 4. Snapshot evidence
[Section titled “4. Snapshot evidence”](#4-snapshot-evidence)
Read-only state snapshot taken 2026-10-09 (per-chain block numbers below); all `owner()` values above re-read at these blocks; no ownership handover request is pending on any contract (`ownershipHandoverExpiresAt` = 0 for the company key on all 33 transferred contracts and for the DAO proxies).
| Chain (ID) | Block | Block hash | Time (UTC) |
| ---------------------- | --------- | -------------------------------------------------------------------- | ------------------------- |
| Ethereum (1) | 26150933 | `0xdeae404e96e65dbaf483164f75c4f7897fc6fbc86d4226dad3f0f0098f224095` | 2026-10-08T23:22:11+00:00 |
| Base (8453) | 52356211 | `0xe0f588a2997990c5058469c9e72156d2f6165108f76ea08696e9e9185f4e019e` | 2026-10-08T23:22:49+00:00 |
| OP Mainnet (10) | 157951481 | `0x4e639995626605ff1e8f248d377425e2a9c967e887f3a5a4c07ca2c35b3959c7` | 2026-10-08T23:22:19+00:00 |
| Arbitrum One (42161) | 513026621 | `0x8299020c78b281cf39f97541488084a474ff9cb3de8034858c379df7e079dcf9` | 2026-10-08T23:22:51+00:00 |
| Robinhood Chain (4663) | 83697491 | `0xca9dd481ada20baaeecf638005c88831297dc429e3eeddc9ea61a992db83af23` | 2026-10-08T23:22:45+00:00 |
| Unichain (130) | 60753388 | `0x2d9126d0f81649a5bbf898b8f2e418c325f5707ba1b20830f8e5637bf2e7856a` | 2026-10-08T23:22:27+00:00 |
| World Chain (480) | 36083059 | `0x58bf090aa18e10a91204e4b446951bb33251a5e98af0f64d87fe542219f2cc3c` | 2026-10-08T23:22:37+00:00 |
| Ink (57073) | 58003363 | `0xc6d705f87c83f925afe5794eef2f1793625c1ccfd775684412799a225fcea3ad` | 2026-10-08T23:22:54+00:00 |
| MegaETH (4326) | 28704750 | `0xa799291b51c217103d944abb4d267f03ddfc7bd2de670a61220ff109f4d70aeb` | 2026-10-08T23:22:41+00:00 |
| Gnosis (100) | 48659510 | `0x41f96b74ad598cba690d495d84cbad9b9f681396f104363d1e152820e2006c1d` | 2026-10-08T23:22:20+00:00 |
| Polygon PoS (137) | 95197694 | `0x874fc87ff6610ba93475fc5a3abae14a502797c627fd418500faa22578c6ad6a` | 2026-10-08T23:22:30+00:00 |
| BNB Smart Chain (56) | 126531070 | `0xa3098929db1acd8b15a9390a6fe79f2e54f817bf4d28e09bd24514b993e72102` | 2026-10-08T23:22:21+00:00 |
| Monad (143) | 111740017 | `0xd7df9e0ac37f4fdf95088f2106e0f1cc3f93a3bded57f040dd7d82d9ef21ace6` | 2026-10-08T23:22:32+00:00 |
Immutable, ownerless contracts (Core, extensions, routers, lenses) are unaffected by this migration and have no owner on any chain.
# Starknet Contracts
> Addresses of the deployed contracts on Starknet mainnet
The source code for the smart contracts is available [here](https://github.com/EkuboProtocol/starknet-contracts) and it can be used as a scarb package.
This page lists the **Starknet mainnet** deployment. Testnet addresses are on the [Starknet Sepolia](/reference/contracts/starknet-sepolia/) page.
## Upgradeable contracts
[Section titled “Upgradeable contracts”](#upgradeable-contracts)
The addresses of these contracts do not change. The contracts are updated to new versions in-place. These contracts are owned by the [governance contracts](/reference/contracts/governance/).
| Contract | Address |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Core | `0x00000005dd3D2F4429AF886cD1a3b08289DBcEa99A294197E9eB43b0e0325b4b` [Voyager](https://voyager.online/contract/0x00000005dd3d2f4429af886cd1a3b08289dbcea99a294197e9eb43b0e0325b4b) [Starkscan](https://starkscan.co/contract/0x00000005dd3D2F4429AF886cD1a3b08289DBcEa99A294197E9eB43b0e0325b4b) |
| Positions | `0x02e0af29598b407c8716b17f6d2795eca1b471413fa03fb145a5e33722184067` [Voyager](https://voyager.online/contract/0x02e0af29598b407c8716b17f6d2795eca1b471413fa03fb145a5e33722184067) [Starkscan](https://starkscan.co/contract/0x02e0af29598b407c8716b17f6d2795eca1b471413fa03fb145a5e33722184067) |
| Positions NFT | `0x07b696af58c967c1b14c9dde0ace001720635a660a8e90c565ea459345318b30` [Voyager](https://voyager.online/contract/0x07b696af58c967c1b14c9dde0ace001720635a660a8e90c565ea459345318b30) [Starkscan](https://starkscan.co/contract/0x07b696af58c967c1b14c9dde0ace001720635a660a8e90c565ea459345318b30) |
| TWAMM Extension | `0x043e4f09c32d13d43a880e85f69f7de93ceda62d6cf2581a582c6db635548fdc` [Voyager](https://voyager.online/contract/0x043e4f09c32d13d43a880e85f69f7de93ceda62d6cf2581a582c6db635548fdc) [Starkscan](https://starkscan.co/contract/0x043e4f09c32d13d43a880e85f69f7de93ceda62d6cf2581a582c6db635548fdc) |
| Oracle Extension | `0x005e470ff654d834983a46b8f29dfa99963d5044b993cb7b9c92243a69dab38f` [Voyager](https://voyager.online/contract/0x005e470ff654d834983a46b8f29dfa99963d5044b993cb7b9c92243a69dab38f) [Starkscan](https://starkscan.co/contract/0x005e470ff654d834983a46b8f29dfa99963d5044b993cb7b9c92243a69dab38f) |
| Limit Orders Extension | `0x050ed6ab03aef492cd062e25facf40ceef63294c53d12b514226f8fb4753266e` [Voyager](https://voyager.online/contract/0x050ed6ab03aef492cd062e25facf40ceef63294c53d12b514226f8fb4753266e) [Starkscan](https://starkscan.co/contract/0x050ed6ab03aef492cd062e25facf40ceef63294c53d12b514226f8fb4753266e) |
## Immutable contracts
[Section titled “Immutable contracts”](#immutable-contracts)
These are the addresses of the latest versions of the contracts. They may change, but the old versions will remain. We do not upgrade these contracts because it is often the case that old versions still work fine with core. Prefer to use the latest version whenever possible.
| Contract | Address |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Router V3.0.13 | `0x0199741822c2dc722f6f605204f35e56dbc23bceed54818168c4c49e4fb8737e` [Voyager](https://voyager.online/contract/0x0199741822c2dc722f6f605204f35e56dbc23bceed54818168c4c49e4fb8737e) [Starkscan](https://starkscan.co/contract/0x0199741822c2dc722f6f605204f35e56dbc23bceed54818168c4c49e4fb8737e) |
| Router V3.0.3 | `0x04505a9f06f2bd639b6601f37a4dc0908bb70e8e0e0c34b1220827d64f4fc066` [Voyager](https://voyager.online/contract/0x04505a9f06f2bd639b6601f37a4dc0908bb70e8e0e0c34b1220827d64f4fc066) [Starkscan](https://starkscan.co/contract/0x04505a9f06f2bd639b6601f37a4dc0908bb70e8e0e0c34b1220827d64f4fc066) |
| Router V2.0.1 | `0x03266fe47923e1500aec0fa973df8093b5850bbce8dcd0666d3f47298b4b806e` [Voyager](https://voyager.online/contract/0x03266fe47923e1500aec0fa973df8093b5850bbce8dcd0666d3f47298b4b806e) [Starkscan](https://starkscan.co/contract/0x03266fe47923e1500aec0fa973df8093b5850bbce8dcd0666d3f47298b4b806e) |
| Router V2 | `0x010c7eb57cbfeb18bde525912c1b6e9a7ebb4f692e0576af1ba7be8b3b9a70f6` [Voyager](https://voyager.online/contract/0x010c7eb57cbfeb18bde525912c1b6e9a7ebb4f692e0576af1ba7be8b3b9a70f6#code) [Starkscan](https://starkscan.co/contract/0x010c7eb57cbfeb18bde525912c1b6e9a7ebb4f692e0576af1ba7be8b3b9a70f6) |
| Router | `0x01b6f560def289b32e2a7b0920909615531a4d9d5636ca509045843559dc23d5` [Voyager](https://voyager.online/contract/0x01b6f560def289b32e2a7b0920909615531a4d9d5636ca509045843559dc23d5#writeContract) [Starkscan](https://starkscan.co/contract/0x01b6f560def289b32e2a7b0920909615531a4d9d5636ca509045843559dc23d5) |
| Token Registry V3 (supports ByteArray) | `0x064bdb4094881140bc39340146c5fcc5a187a98aec5a53f448ac702e5de5067e` [Voyager](https://voyager.online/contract/0x064bdb4094881140bc39340146c5fcc5a187a98aec5a53f448ac702e5de5067e) [Starkscan](https://starkscan.co/contract/0x064bdb4094881140bc39340146c5fcc5a187a98aec5a53f448ac702e5de5067e) |
| Token Registry V2 | `0x0013e25867b6eef62703735aa4cfa7754e72f4e94a56c9d3d9ad8ebe86cee4aa` [Voyager](https://voyager.online/contract/0x0013e25867b6eef62703735aa4cfa7754e72f4e94a56c9d3d9ad8ebe86cee4aa) [Starkscan](https://starkscan.co/contract/0x0013e25867b6eef62703735aa4cfa7754e72f4e94a56c9d3d9ad8ebe86cee4aa) |
| Token Registry V1 (Legacy) | `0x006f55e718ae592b22117c3e3b557b6b2b5f827ddcd7e6fdebd1a4ce7462c93e` [Voyager](https://voyager.online/contract/0x006f55e718ae592b22117c3e3b557b6b2b5f827ddcd7e6fdebd1a4ce7462c93e) [Starkscan](https://starkscan.co/contract/0x006f55e718ae592b22117c3e3b557b6b2b5f827ddcd7e6fdebd1a4ce7462c93e) |
| Revenue buybacks (v5.0.3, current) | `0x03d921193ccdc888785892d0677cf0f8eb98f4a87abcb60eee61e2874e687541` [Voyager](https://voyager.online/contract/0x03d921193ccdc888785892d0677cf0f8eb98f4a87abcb60eee61e2874e687541) [Starkscan](https://starkscan.co/contract/0x03d921193ccdc888785892d0677cf0f8eb98f4a87abcb60eee61e2874e687541) Sources protocol fees from Positions (fixed 20% of collected position fees) and sells them for EKUBO via TWAMM. |
| Revenue buybacks v1.0.0 (legacy) | `0x00f2e9a400ba65b13255ef2792612b45d5a20a7a7cf211ffb3f485445022ef72` [Voyager](https://voyager.online/contract/0x00f2e9a400ba65b13255ef2792612b45d5a20a7a7cf211ffb3f485445022ef72) [Starkscan](https://starkscan.co/contract/0x00f2e9a400ba65b13255ef2792612b45d5a20a7a7cf211ffb3f485445022ef72) |
| STRK20 swap anonymizer | `0x029c55514b13a1198ff004ff4e52d282bf98daf4c5c0dfc0303c390ec6615c15` [Voyager](https://voyager.online/contract/0x029c55514b13a1198ff004ff4e52d282bf98daf4c5c0dfc0303c390ec6615c15) [Starkscan](https://starkscan.co/contract/0x029c55514b13a1198ff004ff4e52d282bf98daf4c5c0dfc0303c390ec6615c15) Stateless helper that executes exact-input Ekubo swaps for STRK20 privacy pools. Immutable with no owner or storage. |
# Starknet Sepolia Contracts
> Addresses of the deployed contracts on Starknet Sepolia testnet
This page lists the **Starknet Sepolia testnet** deployment. Production addresses are on the [Starknet](/reference/contracts/starknet/) page, and the source code is available [here](https://github.com/EkuboProtocol/starknet-contracts).
Contracts that were never deployed to Sepolia are omitted.
## Upgradeable contracts
[Section titled “Upgradeable contracts”](#upgradeable-contracts)
The addresses of these contracts do not change. The contracts are updated to new versions in-place.
| Contract | Address |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Core | `0x0444a09d96389aa7148f1aada508e30b71299ffe650d9c97fdaae38cb9a23384` [Voyager](https://sepolia.voyager.online/contract/0x0444a09d96389aa7148f1aada508e30b71299ffe650d9c97fdaae38cb9a23384) [Starkscan](https://sepolia.starkscan.co/contract/0x0444a09d96389aa7148f1aada508e30b71299ffe650d9c97fdaae38cb9a23384) |
| Positions | `0x06a2aee84bb0ed5dded4384ddd0e40e9c1372b818668375ab8e3ec08807417e5` [Voyager](https://sepolia.voyager.online/contract/0x06a2aee84bb0ed5dded4384ddd0e40e9c1372b818668375ab8e3ec08807417e5) [Starkscan](https://sepolia.starkscan.co/contract/0x06a2aee84bb0ed5dded4384ddd0e40e9c1372b818668375ab8e3ec08807417e5) |
| Positions NFT | `0x04afc78d6fec3b122fc1f60276f074e557749df1a77a93416451be72c435120f` [Voyager](https://sepolia.voyager.online/contract/0x04afc78d6fec3b122fc1f60276f074e557749df1a77a93416451be72c435120f) [Starkscan](https://sepolia.starkscan.co/contract/0x04afc78d6fec3b122fc1f60276f074e557749df1a77a93416451be72c435120f) |
| TWAMM Extension | `0x073ec792c33b52d5f96940c2860d512b3884f2127d25e023eb9d44a678e4b971` [Voyager](https://sepolia.voyager.online/contract/0x073ec792c33b52d5f96940c2860d512b3884f2127d25e023eb9d44a678e4b971) [Starkscan](https://sepolia.starkscan.co/contract/0x073ec792c33b52d5f96940c2860d512b3884f2127d25e023eb9d44a678e4b971) |
| Oracle Extension | `0x003ccf3ee24638dd5f1a51ceb783e120695f53893f6fd947cc2dcabb3f86dc65` [Voyager](https://sepolia.voyager.online/contract/0x003ccf3ee24638dd5f1a51ceb783e120695f53893f6fd947cc2dcabb3f86dc65) [Starkscan](https://sepolia.starkscan.co/contract/0x003ccf3ee24638dd5f1a51ceb783e120695f53893f6fd947cc2dcabb3f86dc65) |
| Limit Orders Extension | `0x00c4c863f6de467b91ce974be48cc17ad7209d0d600926e82845a43a7848b822` [Voyager](https://sepolia.voyager.online/contract/0x00c4c863f6de467b91ce974be48cc17ad7209d0d600926e82845a43a7848b822) [Starkscan](https://sepolia.starkscan.co/contract/0x00c4c863f6de467b91ce974be48cc17ad7209d0d600926e82845a43a7848b822) |
## Immutable contracts
[Section titled “Immutable contracts”](#immutable-contracts)
These are the addresses of the latest versions of the contracts. They may change, but the old versions will remain. Prefer to use the latest version whenever possible.
| Contract | Address |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Router V3.0.13 | `0x0045f933adf0607292468ad1c1dedaa74d5ad166392590e72676a34d01d7b763` [Voyager](https://sepolia.voyager.online/contract/0x0045f933adf0607292468ad1c1dedaa74d5ad166392590e72676a34d01d7b763) [Starkscan](https://sepolia.starkscan.co/contract/0x0045f933adf0607292468ad1c1dedaa74d5ad166392590e72676a34d01d7b763) |
| Router V3.0.3 | `0x050d4da9f66589eadaa1d5e31cf73b08ac1a67c8b4dcd88e6fd4fe501c628af2` [Voyager](https://sepolia.voyager.online/contract/0x050d4da9f66589eadaa1d5e31cf73b08ac1a67c8b4dcd88e6fd4fe501c628af2) [Starkscan](https://sepolia.starkscan.co/contract/0x050d4da9f66589eadaa1d5e31cf73b08ac1a67c8b4dcd88e6fd4fe501c628af2) |
| Router V2.0.1 | `0x02bcf6ff92070beb7ed9a022744a659666e87830b591a8b375af4370eeadaa5b` [Voyager](https://sepolia.voyager.online/contract/0x02bcf6ff92070beb7ed9a022744a659666e87830b591a8b375af4370eeadaa5b) [Starkscan](https://sepolia.starkscan.co/contract/0x02bcf6ff92070beb7ed9a022744a659666e87830b591a8b375af4370eeadaa5b) |
| Router V2 | `0x04576498a328e5b44f88da12204b6138028643ee4c8c0146cfe3c3de7a22f783` [Voyager](https://sepolia.voyager.online/contract/0x04576498a328e5b44f88da12204b6138028643ee4c8c0146cfe3c3de7a22f783?lang=en-US\&theme=light) [Starkscan](https://sepolia.starkscan.co/contract/0x04576498a328e5b44f88da12204b6138028643ee4c8c0146cfe3c3de7a22f783) |
| Router | `0x02a3328b7f0f83c3ca23149623b6cee9e7fddb5591d36d52c77d2a9302d221d0` [Voyager](https://sepolia.voyager.online/contract/0x02a3328b7f0f83c3ca23149623b6cee9e7fddb5591d36d52c77d2a9302d221d0) [Starkscan](https://sepolia.starkscan.co/contract/0x02a3328b7f0f83c3ca23149623b6cee9e7fddb5591d36d52c77d2a9302d221d0) |
| Token Registry V3 (supports ByteArray) | `0x04484f91f0d2482bad844471ca8dc8e846d3a0211792322e72f21f0f44be63e5` [Voyager](https://sepolia.voyager.online/contract/0x04484f91f0d2482bad844471ca8dc8e846d3a0211792322e72f21f0f44be63e5) [Starkscan](https://sepolia.starkscan.co/contract/0x04484f91f0d2482bad844471ca8dc8e846d3a0211792322e72f21f0f44be63e5) |
| Token Registry V2 | `0x0293c0d738eff5aa65619069d437eba2bcb320fb8003f37c9708afcdbe8739c8` [Voyager](https://sepolia.voyager.online/contract/0x0293c0d738eff5aa65619069d437eba2bcb320fb8003f37c9708afcdbe8739c8) [Starkscan](https://sepolia.starkscan.co/contract/0x0293c0d738eff5aa65619069d437eba2bcb320fb8003f37c9708afcdbe8739c8) |
| Revenue buybacks v1.0.0 (legacy) | `0x03e65ebb45d47e0874fba9a41af0ba5df625bb7e67952ce7e567f40da52c509c` [Voyager](https://sepolia.voyager.online/contract/0x03e65ebb45d47e0874fba9a41af0ba5df625bb7e67952ce7e567f40da52c509c) [Starkscan](https://sepolia.starkscan.co/contract/0x03e65ebb45d47e0874fba9a41af0ba5df625bb7e67952ce7e567f40da52c509c) |
| Price Fetcher | `0x04613bee55d8a37adfa249b24c6b13451dedf7cf4f02d01de859579119de3add` [Voyager](https://sepolia.voyager.online/contract/0x04613bee55d8a37adfa249b24c6b13451dedf7cf4f02d01de859579119de3add) [Starkscan](https://sepolia.starkscan.co/contract/0x04613bee55d8a37adfa249b24c6b13451dedf7cf4f02d01de859579119de3add) |
# Gas efficiency
> Measured gas costs for Ekubo swaps against Uniswap v4 and v3, and what they mean for chain throughput
Ekubo is designed to make trading cheap: one singleton contract holds every pool, and flash accounting settles only the net difference at the end of a transaction. This page measures what that means in practice — what a swap costs, how many swaps fit in a block, and what it would mean chain-wide — against Uniswap v4 and Uniswap v3. Every figure runs the deployed production contracts of all three protocols on a fork of the latest mainnet block, with fresh plain ERC20 tokens so token quirks do not blur the comparison. Harnesses, raw snapshots, exact calldata and the full method live in `benchmarks/` in the docs repository.
## Bottom line
[Section titled “Bottom line”](#bottom-line)
One swap of 1 token through a single 0.3% pool of a fresh ERC20 pair, warmed, moving the price by at most one tick, at block 25999935 (September 17, 2026). Transaction gas is execution plus the 21,000 base plus the measured calldata:
| Transaction, 1 pool | Ekubo | Uniswap v4 | Uniswap v3 |
| ------------------------------------------------------------------------- | ----------- | ---------- | ---------- |
| Production router (Yul router; Universal Router with Permit2; SwapRouter) | **110,875** | 144,390 | 123,587 |
| Minimal single-lock router on every side (favors Uniswap) | **112,997** | 131,284 | 117,006 |
| Native ETH in (Yul router; minimal locker; SwapRouter wrapping in flight) | **96,496** | 115,892 | 126,792 |
Through the production routers, Ekubo’s transaction costs 23.2% less than v4’s and 10.3% less than v3’s. With every router stripped to the same minimal lock-swap-settle shape the gap is 13.9% to v4 and 3.4% to v3: at one pool v3’s pool is nearly as cheap, and the difference is in the router and beyond the first pool. With native ETH in, Ekubo is 16.7% below v4 and 23.9% below v3, which must wrap. All legs receive the same output within 0.005%, so the gap is overhead, not price. At the expensive end, 1000 USDT to USDC on the live mainnet pools (proxy tokens, blocklists) costs 133,750 on Ekubo against 155,960 on v4 and 151,515 on v3, 14.2% and 11.7% less: costly tokens compress the gap but do not close it.
## Throughput per block
[Section titled “Throughput per block”](#throughput-per-block)
Mainnet’s block gas limit is 60 million. Real swap flow is routes, not single pools: every extra pool costs Ekubo about 24k of transaction gas against v4’s 36k and v3’s 59k, so the gap widens with route length. Dividing the limit by measured route costs gives each protocol’s ceiling on routes per block if a block held nothing else (minimal routers on every side, which favors Uniswap):
| Routes per 60M block, if a block held nothing else | Ekubo | Uniswap v4 | Uniswap v3 |
| -------------------------------------------------- | ----------------- | ------------- | ------------- |
| 1 pool, production routers | 110,875 → **541** | 144,390 → 415 | 123,587 → 485 |
| 1 pool, minimal routers | 112,997 → **530** | 131,284 → 457 | 117,006 → 512 |
| 2 pools, minimal routers | 137,091 → **437** | 167,027 → 359 | 176,438 → 340 |
| 3 pools, minimal routers | 161,184 → **372** | 202,770 → 295 | 235,870 → 254 |
At three pools an Ekubo block holds 26% more routes than a v4 block and 46% more than a v3 block.
## What this means for the chain
[Section titled “What this means for the chain”](#what-this-means-for-the-chain)
**Pools per swap, measured over a month.** Every 10th mainnet block from August 18, 2026, 18:36 UTC to September 17, 2026, 21:08 UTC (21,600 blocks, 25783810 to 25999800) was read through public JSON-RPC, and every successful transaction that emitted a pool-level swap event was counted by the number of such events. The event set covers Uniswap v2 and its forks, v3, v4, PancakeSwap v3, Curve (all generations), Balancer v2 and v3, Maverick v1 and v2, Fluid, DODO, Bancor v2.1, v3 and Carbon, Solidly-style pairs, and Ekubo:
| Pools touched in the transaction | Share of swap transactions |
| -------------------------------- | -------------------------- |
| 1 | 69.1% |
| 2 | 16.0% |
| 3 | 7.4% |
| 4 or more | 7.5% |
That is 664,726 swap transactions at an average of **1.75 pools per swap** (daily averages 1.64 to 1.87; a 1,000-block window on September 17 gave 1.68). Those transactions used 32.7% of all gas in the sampled blocks (daily 20% to 41%). Transactions sent straight to the Uniswap and 1inch routers average 1.22 pools; the rest is aggregators, MEV and other contracts. Failed transactions emit no logs and are excluded by construction. Both figures are lower bounds: the count only sees swap events in the catalogued families, so any AMM outside the catalogue adds uncounted transactions and uncounted pools, and the swap-transaction gas share can only be higher. The chain-wide capacity gains below inherit that bias and are underestimates.
A swap’s transaction gas at *p* pools is the one-pool minimal-router transaction plus (*p* − 1) times the measured marginal per extra pool:
| Average pools per swap | Ekubo | Uniswap v4 | Uniswap v3 | Ekubo saving vs v4 / v3 |
| ---------------------- | ----------- | ---------- | ---------- | ----------------------- |
| **1.75 (measured)** | **131,019** | 158,020 | 161,461 | **17.1% / 18.9%** |
| 1.0 | 112,997 | 131,284 | 117,006 | 13.9% / 3.4% |
| 1.5 | 125,044 | 149,156 | 146,722 | 16.2% / 14.8% |
| 2.0 | 137,091 | 167,027 | 176,438 | 17.9% / 22.3% |
Block-space shares: over the 30 days ending September 17, 2026, mainnet burned 1,255.7 ETH of base fees (ultrasound.money), and transactions to known DEX routers were about 4.2% of it, a lower bound that misses aggregators, MEV and direct pool calls; ultrasound’s coarser “defi” category was 19.4%. The 32.7% measured above counts every transaction that touched a pool, including MEV and aggregator flow whose gas is mostly not swapping. The 70% share was stipulated for this page as an “all AMM trading” upside case; it is an assumption, not a measurement. If a swap’s share of the burn equals its share of gas and the saving applies to all of it, moving that slice to Ekubo frees the share times the saving (each cell spans the saving against v3 and against v4):
| Block-space share given to swaps | Measured 1.75 pools (v3 18.9%, v4 17.1%) | 1 pool, production routers (v3 10.3%, v4 23.2%) | 2.0 pools (v3 22.3%, v4 17.9%) |
| ------------------------------------------------- | ---------------------------------------- | ----------------------------------------------- | ------------------------------ |
| 4.2%, measured DEX-router burn, lower bound | 0.7–0.8% | 0.4–1.0% | 0.8–0.9% |
| 19.4%, “defi” category bracket | 3.3–3.7% | 2.0–4.5% | 3.5–4.3% |
| 32.7%, swap-event transactions, measured | 5.6–6.2% | 3.4–7.6% | 5.9–7.3% |
| 70%, stipulated AMM-trading upside (not measured) | 12.0–13.2% | 7.2–16.2% | 12.5–15.6% |
Induced demand would fill freed space. Read these as an order of magnitude, not a forecast.
## On Base and Arbitrum
[Section titled “On Base and Arbitrum”](#on-base-and-arbitrum)
L2 fees are cheap L2 execution plus a charge for the L1 data the sequencer posts. Both were measured on Base (block 51439900) and Arbitrum One (block 506178800) for 100 USDC to WETH through each protocol’s deployed contracts, warmed, on the deepest live Uniswap WETH/USDC pool of each chain; no live Ekubo pool there could absorb the swap, so the Ekubo leg runs on a pool created on the fork with the v3 pool’s price, fee and liquidity. The L1 data component is what each chain’s own fee oracle charges for the exact transaction, in gas at the block’s L2 base fee:
| Chain | Leg (100 USDC to WETH) | Execution gas | Calldata bytes | L1 data (gas-equivalent) | Total, incl. 21,000 base and calldata gas |
| -------- | -------------------------- | ------------- | -------------- | ------------------------ | ----------------------------------------- |
| Base | Ekubo, Yul router | **105,084** | 184 | 331 | **128,171** |
| Base | Uniswap v3, SwapRouter02 | 121,803 | 228 | 319 | 144,646 |
| Base | Uniswap v4, minimal locker | 126,959 | 292 | 367 | 150,694 |
| Arbitrum | Ekubo, Yul router | **113,121** | 184 | 561 | **136,870** |
| Arbitrum | Uniswap v3, SwapRouter | 130,896 | 260 | 603 | 154,415 |
| Arbitrum | Uniswap v4, minimal locker | 135,006 | 292 | 644 | 159,234 |
The mainnet pattern carries over: the whole transaction is 14.9% and 11.4% cheaper than v4 and v3 on Base, 14.0% and 11.4% on Arbitrum, since both chains run the same EVM at the same opcode prices. The data component decides nothing at these blocks: 0.2% to 0.4% of every leg, and on Base not even smallest for Ekubo, whose dense route compresses worse than Uniswap’s zero-padded calldata. Compact calldata buys nothing measurable while L1 data is this cheap.
## Why it is cheaper
[Section titled “Why it is cheaper”](#why-it-is-cheaper)
* **Cheaper pool core.** One packed storage word and Q64 fixed-point math cost about 20k gas per swap, against 29k for v4’s multi-slot state and 37k for v3’s slot0, fee-growth and tick bookkeeping (lab flamegraphs in `benchmarks/`).
* **Net settlement.** Ekubo and v4 settle once per transaction (2 token transfers for a route of any length); v3 pays 2 per pool. On the production contracts each extra pool costs about 22k of execution on Ekubo, 34k on v4, 59k on v3.
* **Compact calldata.** The Yul router packs a single-pool route into 184 bytes; Uniswap’s Universal Router needs 1,092 for a single v4 swap with its Permit2 funding, v3’s SwapRouter 260.
## Measured honestly
[Section titled “Measured honestly”](#measured-honestly)
Providing liquidity is measured where providers actually touch it: production position managers on the live USDC/USDT pools. Minting a new \~10,000 USDC/USDT position costs 244,513 on Ekubo against 297,390 on v4 (17.8% less) and 381,653 on v3 (35.9% less). One-time costs (pool creation, tick initialization, first-swap accumulator writes) are excluded by design everywhere on this page, and the v4 Universal Router was measured only at one pool. Scope: EVM only, concentrated pools, no hooks or extensions on any side — any v4 hook or Ekubo extension changes every number here.
## How it was measured
[Section titled “How it was measured”](#how-it-was-measured)
* Production Ekubo Core, Positions and Yul router; Uniswap v3 factory and SwapRouter; v4 PoolManager and Universal Router, on a fork of the latest mainnet block at run time, four fresh ERC20 tokens, 0.3% pools of matched liquidity, no tick crossings.
* Warmed, isolated steady state: one identical unmeasured call first, every touched slot already nonzero, each measured call its own transaction, so the snapshot is execution gas net of refunds; exact calldata recorded per call.
* Minimal single-lock routers of the same shape on all sides for route scaling, which favors Uniswap: its production routers cost more than the minimal figures, Ekubo’s costs less.
* L2s: Base and Arbitrum One forks pinned by block, deployed Uniswap routers and pools, the deployed Ekubo Yul router on a fresh pool mirroring the v3 pool; L1 data read from each chain’s fee oracle, not modeled.
* Pools per swap: a uniform 10% sample of a month of mainnet block receipts over public JSON-RPC, every event signature hashed from its protocol’s source. Not counted: order-book and RFQ fills unless they land in a pool, the legacy Ekubo v2 core, long-tail AMMs. Sources, snapshots, calldata, pool identifiers and reproduction commands: `benchmarks/` in the docs repository.
# Pool math
> Where to learn the concentrated-liquidity math Ekubo uses, and the parameters that are specific to Ekubo
Ekubo’s concentrated liquidity pools use the same underlying math as other concentrated-liquidity AMMs: a constant-product curve where each position is active only within a price range, pool state tracked as a square-root price and a liquidity value, and swaps executed piecewise across regions of constant liquidity.
That math is well documented elsewhere, and the derivations are identical, so this page does not restate them. What it does cover is **the parameters that are specific to Ekubo** — the numbers you need to get right when implementing against it.
## Learn the math
[Section titled “Learn the math”](#learn-the-math)
| Reference | Best for |
| ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| [Uniswap v3 whitepaper](https://app.uniswap.org/whitepaper-v3.pdf) | The canonical statement of the model — sections 6.1–6.3 cover ticks, swapping within a tick, and crossing ticks |
| [Liquidity Math in Uniswap v3](https://atiselsts.github.io/pdfs/uniswap-v3-liquidity-math.pdf), Atis Elsts | The clearest derivation of the position and amount formulas, worked step by step |
| [Uniswap v3 Development Book](https://uniswapv3book.com/) | A build-it-yourself walkthrough, if you learn best from implementation |
| [Concentrated liquidity](https://docs.uniswap.org/concepts/protocol/concentrated-liquidity) | A short conceptual introduction |
For the concepts in plain language without the formulas, see [Key concepts](/concepts/key-concepts/).
## What Ekubo does differently
[Section titled “What Ekubo does differently”](#what-ekubo-does-differently)
### Tick base
[Section titled “Tick base”](#tick-base)
Ekubo’s tick base is **`1.000001`**, not `1.0001`. Tick `i` corresponds to the price `1.000001^i`, so one tick is 1/100th of a basis point — 100 times finer than the more common convention. Every tick-to-price conversion you take from an external reference must use this base.
The tick range is correspondingly wider:
| Parameter | EVM (V3) | Starknet |
| ---------------- | ----------- | ----------- |
| Min / max tick | ±88,722,835 | ±88,722,883 |
| Max tick spacing | 698,605 | 354,892 |
### Price and sqrt-ratio range
[Section titled “Price and sqrt-ratio range”](#price-and-sqrt-ratio-range)
Ekubo supports prices from `2^-128` to `2^128`, so the square root of the price ranges from `2^-64` to `2^64`.
### Representation
[Section titled “Representation”](#representation)
The square-root price is stored differently on each deployment: a 128-bit fixed-point number on Starknet, and a compact 96-bit floating-point-style type on EVM. Fees are encoded as binary fractions, with a different denominator on each chain. Both are documented in [Price representation](/reference/price-representation/) — read that before implementing any conversion.
### Token decimals
[Section titled “Token decimals”](#token-decimals)
Ekubo performs no decimal adjustment anywhere in its math. If `token0` and `token1` have different decimals, a raw price of `1` corresponds to a human-readable price of `10**token1_decimals / 10**token0_decimals`. See [Reading pool price](/integration-guides/reading-pool-price/) for a worked conversion.
### Other pool types
[Section titled “Other pool types”](#other-pool-types)
Concentrated liquidity is one of three pool types. **Stableswap** pools concentrate liquidity around a configurable center tick with an amplification factor, and **full-range** pools span the entire price range — the cheapest configuration, and equivalent to a constant-product AMM. See [Providing liquidity](/products/liquidity/).
## Reference implementations
[Section titled “Reference implementations”](#reference-implementations)
Rather than reimplementing the conversions, use an SDK — both handle Starknet and EVM, and both produce exactly the values the contracts use:
* [`ekubo_sdk`](https://crates.io/crates/ekubo_sdk) (Rust) — full quoting across every pool type and extension
* [`@ekubo/sdk`](https://www.npmjs.com/package/@ekubo/sdk) (TypeScript) — tick, price, liquidity, and swap math
See [SDKs](/integration-guides/sdks/).
# Price representation
> How Ekubo represents pool prices: sqrt ratios, ticks, and the compact floating-point SqrtRatio type used by Ekubo V3 on EVM chains
Every Ekubo pool tracks its current price as a **square root ratio**: the square root of the price of `token1` in terms of `token0`.
```plaintext
sqrt_ratio = sqrt(token1 / token0)
```
Working with the square root of the price (rather than the price itself) makes the concentrated liquidity math cheaper and more precise — see the [Math 1-pager](/reference/pool-math/) for why. Prices are supported over the enormous range `[2^-128, 2^128]`, so the sqrt ratio always lies in `[2^-64, 2^64]`.
Ticks are the same on every deployment: tick `i` corresponds to the sqrt ratio `sqrt(1.000001^i)`, i.e. each tick is **1/100th of a basis point** of price. The valid tick range is roughly ±88.72 million (`±88,722,835` on EVM, `±88,722,883` on Starknet — the EVM range is very slightly narrower).
How the sqrt ratio is *encoded* differs between the Starknet and EVM deployments.
## Starknet: 64.128 fixed point
[Section titled “Starknet: 64.128 fixed point”](#starknet-64128-fixed-point)
On Starknet, `sqrt_ratio` is a `u256` interpreted as a binary fixed-point number with **128 fractional bits**. A sqrt ratio of `1.0` (price of 1) is `2^128`. Since the sqrt ratio never exceeds `2^64`, the value always fits in 192 bits, which lets Core pack the pool price and tick into a single `felt252` storage slot.
* `MIN_SQRT_RATIO = 18446748437148339061` (≈ `2^-64 · 2^128`)
* `MAX_SQRT_RATIO = 6277100250585753475930931601400621808602321654880405518632` (≈ `2^64 · 2^128`)
To convert to a price: divide by `2^128`, then square. See [Reading pool price](/integration-guides/reading-pool-price/) for a worked example.
## EVM (V3): the floating-point `SqrtRatio` type
[Section titled “EVM (V3): the floating-point SqrtRatio type”](#evm-v3-the-floating-point-sqrtratio-type)
Ekubo V3 on EVM chains introduces a purpose-built **96-bit dynamic fixed-point type** — a compact floating-point-style encoding invented for Ekubo:
```solidity
type SqrtRatio is uint96;
```
### Why a float?
[Section titled “Why a float?”](#why-a-float)
A full-precision 64.128 sqrt ratio needs 192 bits, which doesn’t leave room for anything else in a 256-bit storage word. The AMM doesn’t actually need 192 bits of precision at every magnitude — it needs *relative* precision. By encoding the sqrt ratio in 96 bits, the entire pool state packs into a **single storage slot**:
```plaintext
poolState (bytes32) = sqrtRatio (96 bits) | tick (32 bits) | liquidity (128 bits)
```
This makes updating pool state during a swap dramatically cheaper. The arithmetic on the type is designed so that any rounding error is biased in favor of the pool.
### Bit layout
[Section titled “Bit layout”](#bit-layout)
The 96 bits consist of a **2-bit exponent selector** (bits 95–94) and a **94-bit mantissa** (bits 93–0). The exponent selects how the mantissa is interpreted as a binary fixed-point number:
| Exponent (bits 95–94) | Mantissa interpretation | Value range covered |
| --------------------- | ------------------------ | ------------------------- |
| `00` | 0.126 (mantissa / 2^126) | sqrt ratios below `2^-32` |
| `01` | 0.94 (mantissa / 2^94) | `2^-32` up to `1` |
| `10` | 32.62 (mantissa / 2^62) | `1` up to `2^32` |
| `11` | 64.30 (mantissa / 2^30) | `2^32` up to `2^64` |
There is no implicit leading bit and no bias — each exponent value simply shifts the radix point by 32 bits. A sqrt ratio of exactly `1.0` is encoded with exponent `10` and mantissa `2^62`:
```solidity
SqrtRatio constant ONE = SqrtRatio.wrap(uint96((1 << 95) + (1 << 62)));
```
Key properties:
* **Monotonic**: a larger raw `uint96` always means a larger sqrt ratio, so comparisons are plain integer comparisons — no decoding required.
* **Normalized**: a valid value always has at least 63 significant mantissa bits (`isValid` requires the mantissa to be ≥ `2^62`, so bit 62 is set, and the raw value to be within the min/max bounds). Effective precision therefore ranges from 63 to 94 significant bits depending on where in the range the value falls — far more than enough for 1/100th-basis-point ticks.
* **Bounded**: `MIN_SQRT_RATIO` (raw `4611797791050542631`) through `MAX_SQRT_RATIO` (raw `79227682466138141934206691491`), covering approximately the same `[2^-64, 2^64]` sqrt ratio range as Starknet — the EVM bounds are very slightly tighter, matching its slightly narrower tick range.
### Converting to and from 64.128
[Section titled “Converting to and from 64.128”](#converting-to-and-from-64128)
The canonical “expanded” form on EVM is the same 64.128 fixed point used on Starknet. Conversion is a single shift, chosen by the exponent:
```solidity
// SqrtRatio -> 64.128 fixed point (uint256)
function toFixed(SqrtRatio sqrtRatio) pure returns (uint256);
// shift left by 2, 34, 66 or 98 bits for exponents 00, 01, 10, 11
// 64.128 fixed point -> SqrtRatio (rounding toward or away from zero)
function toSqrtRatio(uint256 sqrtRatioFixed, bool roundUp) pure returns (SqrtRatio);
```
Both directions round-trip losslessly for any valid `SqrtRatio`. To compute a human-readable price from the 64.128 value: divide by `2^128`, square, and adjust for the two tokens’ decimals (worked example in [Reading pool price](/integration-guides/reading-pool-price/)).
### Where you’ll encounter it
[Section titled “Where you’ll encounter it”](#where-youll-encounter-it)
* **Swap parameters**: `sqrtRatioLimit` in swap params and router `RouteNode`s is a `SqrtRatio` (a value of `0` means “no limit”).
* **Pool state and events**: `PoolInitialized` emits the initial `SqrtRatio`; extension hooks (`afterSwap`, `afterUpdatePosition`, …) receive the packed pool state containing it.
* **Reading prices**: `CoreDataFetcher.poolPrice()` returns the price already expanded to 64.128 via `toFixed()`, so most integrators never need to decode the packed form manually. Its `poolState()` returns the packed `SqrtRatio` undecoded.
* **SDKs**: [`@ekubo/sdk`](https://www.npmjs.com/package/@ekubo/sdk) (TypeScript) and the [Rust SDK](https://github.com/EkuboProtocol/rust-sdk) implement the exact tick and sqrt-ratio conversions for both Starknet and EVM.
## Fee encoding differs too
[Section titled “Fee encoding differs too”](#fee-encoding-differs-too)
While ticks are chain-agnostic, the **pool fee** encoding differs between deployments:
| Deployment | Fee type | Denominator | Example: 0.3% |
| ---------- | -------------------------- | ----------- | --------------------------------------- |
| Starknet | `u128`, 0.128 fixed point | `2^128` | `1020847100762815411640772995208708096` |
| EVM (V3) | `uint64`, 0.64 fixed point | `2^64` | `55340232221128654` |
In both cases the fee is a binary fraction of the full width — `fee / 2^128` (Starknet) or `fee / 2^64` (EVM) — applied to the swap input amount, rounded up in favor of the pool.
# Add liquidity
> How to become a liquidity provider on Ekubo
To add liquidity to Ekubo and start earning fees from swaps, [create a position](https://ekubo.org/positions/new).
Note
Bookmark the app and beware of signing transactions from other websites claiming to be Ekubo that are not hosted on ekubo.org. [Read more](https://en.wikipedia.org/wiki/Phishing) about phishing scams.
Adding liquidity takes three steps:
* Selecting a pool
* Selecting a price range
* Specifying an amount
## Selecting a pool
[Section titled “Selecting a pool”](#selecting-a-pool)
Every position is tied to a specific pool, and a pool is a combination of token pair, fee, tick spacing, and extension.
The two tokens you select — the “pair” — are the market you want to make. To market-make ETH against dollars, for example, choose ETH as the base token and USDC or DAI as the quote token. Here, the quote token is the numerator of the prices shown on the following pages. Swapping which token is which does not change the pair; it only changes how prices are displayed.
The fee you select is how much swappers are charged to trade against your liquidity. A fixed protocol fee is deducted from the swap fees you collect (currently 20% on Starknet and 10% on EVM chains, applied by the Positions contract when fees are collected). The canonical deployments take no fee on your principal when you withdraw.
Tick spacing should typically be about twice the fee. It determines how narrow your price range can be, and therefore how much leverage you can take.
## Selecting a price range
[Section titled “Selecting a price range”](#selecting-a-price-range)
Once you’ve selected a pool, you must select the range of prices in which you would like to market make. If the price leaves this selected price range, your position will become “out of range,” meaning it is no longer actively earning fees. If the market price reaches the upper boundary, your position will hold entirely the quote token; at the lower boundary, entirely the base token.
You should choose your price range to maximize capital efficiency: a narrower range earns more fees per dollar of principal while the price stays inside it, but requires more active management.
## Specifying an amount
[Section titled “Specifying an amount”](#specifying-an-amount)
Once you choose the parameters of your position, all that is left is to decide how much capital you wish to deposit. Enter an amount within your balance and confirm to create the position.
You are now an Ekubo liquidity provider.
# Dollar-cost average orders
> Also known as TWAMM. Place orders that execute over time to enter and exit positions more efficiently or even programmatically
TWAMM, or “time-weighted average market maker,” is an Ekubo extension that powers the “DCA orders” and “DCA-enabled pools” features in the [interface](https://ekubo.org). DCA orders are orders to sell a token at a specified rate between a start and end time. The mechanism involved in executing Ekubo’s DCA orders is best described by [Paradigm](https://www.paradigm.xyz/2021/07/twamm).
DCA-enabled pools are liquidity pools that use the extension to support these orders by providing liquidity for the buy/sell side when DCA orders are imbalanced. In practice, orders are split into per-second pieces and netted against each other; whenever the ratio of buy to sell orders does not exactly match the current pool price, the difference is swapped against the pool. That ratio rarely matches the pool price exactly, which is why this liquidity is necessary for DCA orders to price well.
Note
In practice, virtual orders execute at most once per block. DCA orders are therefore best suited to trades that play out over longer periods than the chain’s block time. The shorter the block time, the more pieces an order is split into, so the same liquidity supports larger or shorter-duration orders.
Orders on both sides of a DCA-enabled pool are netted against each other, and the difference is swapped on the pool to compute the resulting price for the orders.
Ekubo’s TWAMM implementation is integrated into the core protocol via [extensions](/concepts/extensions/). There are two features in the user interface:
* **DCA-enabled pools:** liquidity pools that use the TWAMM extension
* Provide liquidity to these pools to earn fees from DCA orders as well as regular swap volume
* Concentrated liquidity is not supported for these pools
* **DCA orders:** orders that sell a token over a specified period for a specific pool
* Because the price is not known at the time of the swap, “exact output” swaps are not supported
* There is no price protection, but you may stop an order at any time
* Arbitrage keeps pricing efficient
Because TWAMM pools are cheaply aggregated with other pools in Ekubo, and opposite orders are netted against each other, DCA orders provide efficient pricing and lower fees for large swaps, i.e. you are likely to receive the time-weighted average price of the pair on your order.
Consider using TWAMM in the following cases:
* When moving large amounts of capital into and out of positions where the order books are thin
* When converting tokens programmatically
* When you want to buy/sell a token but don’t want to time the entry/exit
Note that the price you receive on your DCA orders is heavily dependent on the liquidity in the pools and other orders on the pool over the period that the order executes. If you want to ensure good execution, it’s best to supply liquidity to the pool on which you want to place the order to limit the price impact of each swap.
## Creating DCA orders
[Section titled “Creating DCA orders”](#creating-dca-orders)
You can place DCA orders in the [interface](https://ekubo.org/dca).
DCA orders pay fees when the orders are imbalanced: you pay swap fees to liquidity providers of the pool on which you placed the order to swap your tokens, depending on how one-sided the orders are. Volume that nets directly against opposing orders pays no swap fee. Stopping or decreasing an order is free.
Note
An order’s start and end times must each be a multiple of a step size that grows the further in the future they are. The smallest step is 256 seconds on EVM chains (16 on Starknet), so the shortest possible order is 256 seconds; the longest is just under `2**32` seconds.
## Adding liquidity to DCA pools
[Section titled “Adding liquidity to DCA pools”](#adding-liquidity-to-dca-pools)
Creating a DCA-enabled pool provides the backstop liquidity that orders on it execute against, and that liquidity has exclusive rights to that pool’s TWAMM volume. Orders cannot be placed between two tokens until at least one direct DCA-enabled pool exists.
Splitting an order into per-second pieces minimizes its price impact. You choose the DCA-enabled pool the order runs against and specify an amount; the order is then netted against volume flowing the other way, and any volume that never reaches the backstop pool pays no fee at all. Placing an order mints an NFT representing your ownership of it.
Caution
As with the NFT you receive when you create a position, you should **never sell this NFT** — selling it gives up the right to the capital behind the order.
## Fees
[Section titled “Fees”](#fees)
All fees from the DCA orders are directed towards liquidity providers of the pool on which the order is placed.
DCA orders incur *fees* only when there is no volume on the other side of the trade and the order must be executed against the liquidity pool. The fees paid to liquidity providers when orders are executed against the pool are like regular swap fees. Order volume that nets against opposing orders pays no fee, and stopping an order early is free.
# EKUBO token
> Information about the EKUBO token
The EKUBO token is an [L1 Ethereum token](https://etherscan.io/token/0x04C46E830Bb56ce22735d5d8Fc9CB90309317d0f) bridged to [Starknet](https://voyager.online/token/0x075afe6402ad5a5c20dd25e10ec3b3986acaa647b77e4ae24b0cbc9a54a27a87), developed for the purpose of decentralizing ownership of Ekubo Protocol.
## Initial distribution
[Section titled “Initial distribution”](#initial-distribution)
The total supply of the EKUBO token is 10 million (`10,000,000`), split into three equal parts of `3,333,333` tokens. The three distribution categories at the token generation event were:
* **Airdrop**: one third of the total supply was distributed to users via airdrop
* **Team**: one third of the total supply is held by Ekubo, Inc.
* **Sale**: one third of the total supply was sold by the DAO for ETH, USDC, and STRK using Ekubo’s DCA orders
### Airdrop
[Section titled “Airdrop”](#airdrop)
Each user’s allocation was calculated from their share of total leaderboard points, with two adjustments: points were raised to an exponent (`p^x`) that depended on the user’s role in the ecosystem, and accounts with fewer than 1,000 points were excluded. As a result, users who concentrated their activity in fewer accounts received slightly more EKUBO.
* Moderators had a boost of `1.01`
* Active translators received a boost of `1.001`
* Everyone else received a boost of `1.0001`
The airdrop contract is deployed at the address `0x04bfacd0fcf70f444815de9150008fd12b5fb6721562707e502ce71ccb327d88` ([Starkscan](https://starkscan.co/contract/0x04bfacd0fcf70f444815de9150008fd12b5fb6721562707e502ce71ccb327d88), [Voyager](https://voyager.online/contract/0x04bfacd0fcf70f444815de9150008fd12b5fb6721562707e502ce71ccb327d88)). It uses the open-source airdrop contract found [here](https://github.com/EkuboProtocol/governance/releases/tag/v2.2.1). There is no deadline to claim the airdrop.
An airdrop can be claimed by using a block explorer to submit a transaction using data found in this spreadsheet:
[A CSV containing the entirety of the merkle tree for the EKUBO airdrop](https://raw.githubusercontent.com/EkuboProtocol/gitbook/main/.gitbook/assets/airdrop_data.csv.zip)
#### Claiming your airdrop
[Section titled “Claiming your airdrop”](#claiming-your-airdrop)
You may claim your airdrop from the [app](https://ekubo.org/rewards), or use this guide to claim it directly from a block explorer.
#### Download the airdrop data CSV file
[Section titled “Download the airdrop data CSV file”](#download-the-airdrop-data-csv-file)
This CSV contains a list of all the accounts and allocation amounts that were included in the airdrop. Each row corrresponds to a single address’s claim in the airdrop.
#### Find your address in the CSV file
[Section titled “Find your address in the CSV file”](#find-your-address-in-the-csv-file)
Extract the `.csv` from the `.zip` file, then open it in any text editor. Search for the line containing your address. If you cannot find it, remove any leading zeroes from your address and search again — if your address is `0x0000abcd`, search for `0xabcd`. If you still cannot find it, you did not receive an airdrop.
Caution
Do not use Excel or Numbers to open the spreadsheet. They will round the numbers in the columns causing you to get “Invalid proof” when you try to submit your transaction.
#### Copy the row into Voyager block explorer
[Section titled “Copy the row into Voyager block explorer”](#copy-the-row-into-voyager-block-explorer)
The columns in the CSV are claimee, id, amount and proof from left to right. For the row containing your address, copy each column value into the [block explorer](https://voyager.online/contract/0x04bfacd0fcf70f444815de9150008fd12b5fb6721562707e502ce71ccb327d88#writeContract). Remove the quotes and curly braces from the `proof` column data. Then connect your wallet and it should look something like this.

#### Click transact to claim!
[Section titled “Click transact to claim!”](#click-transact-to-claim)
When you click transact, the transaction should simulate successfully, and if you are transacting from the `claimee` address you should see an EKUBO balance increase from the simulation.
### Sale
[Section titled “Sale”](#sale)
The DCA orders executed over 2 months, starting 5/24/24, 2:48 AM UTC and ending 7/23/24, 7:09 PM. The proceeds of the DCA order were owned by the [Governor ](https://voyager.online/contract/0x053499f7aa2706395060fe72d00388803fb2dcc111429891ad7b2d9dcea29acd), a.k.a. the DAO.
The following pools were used: [EKUBO/ETH 5%](https://ekubo.org/positions/new?poolType=twamm\"eCurrency=ETH\&baseCurrency=EKUBO\&fee=17014118346046923173168730371588410570\&poolOnly=true), [EKUBO/STRK 5%](https://ekubo.org/positions/new?poolType=twamm\"eCurrency=STRK\&baseCurrency=EKUBO\&fee=17014118346046923173168730371588410570\&poolOnly=true), and [EKUBO/USDC 5%](https://ekubo.org/positions/new?poolType=twamm\"eCurrency=USDC\&baseCurrency=EKUBO\&fee=17014118346046923173168730371588410570\&poolOnly=true).
Approximately `3,269,920` EKUBO was sold for `343.675` ETH, `1,204,770` USDC, and `1,549,920` STRK.
### Team
[Section titled “Team”](#team)
The company Ekubo, Inc., a service provider to the DAO, holds one-third of the total supply. There is no vesting schedule for these tokens. The team has come to an agreement with the DAO to hold these tokens indefinitely via [governance proposal](https://ekubo.org/governance/proposals/0x1bfc2ccdd2f9a718c45a9aa3a88770435f5272fbfeeb38ca2b3ad54c51c81e9) to fund the company’s provision of services to the DAO.
## Value accrual
[Section titled “Value accrual”](#value-accrual)
As of August 2025, the DAO currently holds total ownership of Ekubo Protocol smart contracts, meaning it can upgrade the contracts on Starknet, and collect revenue on EVM and Starknet. Note there is no upgrade mechanism for EVM contracts.
It is the right and responsibility of the DAO to decide what to do with the revenue. The DAO may direct protocol revenue towards EKUBO buybacks, and then may further direct that EKUBO to active participants in the protocol, such as governance stakers or liquidity providers.
As of August 2025, the position of Ekubo, Inc. is that the revenue should be directed towards growth of the protocol, and we will vote accordingly until the protocol achieves sustainable revenue, i.e. the revenue earned by the protocol exceeds the total costs to keep the protocol running.
The team allocation of EKUBO tokens is entirely held by Ekubo, Inc., and the company is committed to maintaining its share of Ekubo Protocol. The end-goal is for Ekubo, Inc. to be sustained by protocol revenue that is directed towards token holders. This aligns the equity holders with the interests of the protocol. The vision is further described in the [alignment proposal](https://ekubo.org/governance/proposals/0x1bfc2ccdd2f9a718c45a9aa3a88770435f5272fbfeeb38ca2b3ad54c51c81e9).
## Governance contracts
[Section titled “Governance contracts”](#governance-contracts)
The governance contract addresses can be found in the [governance contracts reference](/reference/contracts/governance/).
You can learn more about how the governance contracts work [here](./).
## Disclaimer
[Section titled “Disclaimer”](#disclaimer)
The EKUBO token serves only to decentralize the ownership role of the on-chain instance of the core Ekubo Protocol.
Ekubo, Inc., is not in any way obligated to provide service, maintenance, or development for Ekubo Protocol and related tools or services except as explicitly agreed via governance proposal.
The governance infrastructure is necessary security infrastructure: it protects users of Ekubo Protocol from malicious upgrades by requiring a decentralized and interested majority of stakeholders to come to consensus on the validity and safety of the changes.
# Participate in governance
> How to stake, delegate, propose, and vote in Ekubo governance
This guide covers the practical steps. For how the system is designed — what governance controls, the contracts involved, and Ekubo, Inc.’s role — see [Governance](/products/governance/).
## Stake and delegate
[Section titled “Stake and delegate”](#stake-and-delegate)
Voting power comes from staked [EKUBO](/user-guides/ekubo-token/), delegated to an address. Delegate to yourself to vote directly, or to someone else to have them vote on your behalf.
Use the [Delegate page](https://ekubo.org/governance/simple-delegate) in the app, or call the Staker contract directly:
| Function | Effect |
| -------------------------------------------------------------------------------- | ----------------------------------------------- |
| `stake(delegate)` | Stakes your full approved balance to a delegate |
| `stake_amount(delegate, amount)` | Stakes a specific amount |
| `withdraw(delegate, recipient)` / `withdraw_amount(delegate, recipient, amount)` | Withdraws staked tokens |
There is **no lockup and no penalty** for withdrawing at any time.
Note
Your voting weight is not your staked balance. It is the *average* amount delegated to you over the voting weight smoothing duration ending when voting opens — currently 1 day. Because the voting start delay is only 1 hour, stake well before a proposal you care about is created, not after.
## Create a proposal
[Section titled “Create a proposal”](#create-a-proposal)
You need voting weight at or above the proposal creation threshold (currently 100,000 EKUBO) and no other active proposal of your own.
Create one from the [proposal page](https://ekubo.org/governance/create-proposal), or on-chain:
* `propose(calls)` — submit the calls the proposal will execute
* `describe(proposalId, description)` — attach a description
* `propose_and_describe(calls, description)` — both at once
A proposal commits to its calls by hash. The **same calls must be supplied again at execution**, so keep them.
Voting opens after the voting start delay (currently 1 hour). Discuss proposals in the [Discord](https://discord.ekubo.org) before submitting — the delay exists so problems can be caught early.
## Vote
[Section titled “Vote”](#vote)
Call `vote(proposalId, yea)` with `true` for yea or `false` for nay, or vote in the app. One vote per address, only during the voting period (currently 4 days).
A proposal passes if it reaches quorum (currently 3,250,000 EKUBO in yea votes) **and** receives strictly more yea than nay. A tie fails.
## Cancel, if needed
[Section titled “Cancel, if needed”](#cancel-if-needed)
`cancel(proposalId)` — available to the proposer only, and only before voting opens. This is the correction window for a mistake in a submitted proposal.
## Execute
[Section titled “Execute”](#execute)
After a proposal passes and the execution delay elapses (currently 1 hour), anyone can call `execute(proposalId, calls)` with the original calls. Execution must happen within the execution window (currently 30 days) or the proposal expires.
Execution is atomic: all calls succeed or none do. A proposal that reverts can be retried within the window.
***
Current parameter values and what governance controls are documented in [Governance](/products/governance/#proposal-lifecycle). Contract addresses are in the [governance contracts reference](/reference/contracts/governance/).
# Ekubo Wallet
> Use Ekubo Wallet with AI agents and WalletConnect dapps while keeping keys, policies, and approvals in a native desktop application
Ekubo Wallet is a native desktop wallet for Ethereum and EVM-compatible networks. It gives people, local AI agents, and WalletConnect dapps one place to use accounts. Within the wallet’s own interfaces, private keys and owner-only decisions stay in the native application.
Windows and Linux key-storage warning
The current Windows and Linux credential backends do not isolate raw private keys from other processes running as the same logged-in user. Same-user malware, including a prompt-injected local agent that can execute code, can extract a key outside the wallet and bypass policy and review. Read the [platform limitation](/wallet/security/#windows-and-linux-credential-store-limitation) before installing.
This section is the canonical landing page for Ekubo Wallet information, setup, and security documentation.
## What the wallet does
[Section titled “What the wallet does”](#what-the-wallet-does)
Ekubo Wallet can hold multiple accounts, show balances across enabled networks, simulate and submit transactions, sign messages and typed data, connect to dapps through WalletConnect, run scheduled [automations](/wallet/automations/) that react to on-chain conditions, and serve as a local wallet for supported AI agents.
An agent can inspect public wallet state, obtain exact actions from producer services, ask the wallet to simulate and submit them, and wait for the result. The wallet does not prepare transfers, protocol actions, or calldata. An agent cannot approve a request, export a private key, install a signing policy, or change security-sensitive settings. Those actions stay in the native application and require the owner where appropriate.
| Task | Where it happens |
| ----------------------------------------- | ----------------------------------- |
| Create or import an account | Ekubo Wallet |
| Read balances and enabled networks | Ekubo Wallet or an authorized agent |
| Prepare an Ekubo swap or liquidity action | Public Ekubo MCP service |
| Simulate, review, sign, and submit | Ekubo Wallet |
| Approve a request or change permissions | Owner in Ekubo Wallet |
The public [Ekubo MCP server](/products/mcp-server/) complements the wallet. It can resolve tokens, compare swap quotes, read Ekubo pools and positions, and prepare unsigned execution plans. The wallet independently retrieves the selected plan, simulates it against current chain state, and owns signing and submission.
Because the wallet executes plans rather than implementing protocols, its coverage extends well past Ekubo — currently 0x, Across, LayerZero, LI.FI, Aave V3, Morpho, Sky, Lido, and Merkl as well — and grows without a wallet update. See [Supported protocols](/wallet/protocols/), which also covers what makes a third-party MCP server compatible.
## Get started
[Section titled “Get started”](#get-started)
1. [Install Ekubo Wallet](/wallet/install/) and create or import an account.
2. [Connect an AI agent](/wallet/agents/) or [pair a dapp with WalletConnect](/wallet/walletconnect/).
3. Learn how [native reviews](/wallet/approvals/) and [signing policies](/wallet/policies/) decide what can be signed.
4. Read the [security and privacy model](/wallet/security/) to understand what the wallet protects and where its boundaries end.
Questions about setup or behavior are welcome in the [Ekubo Discord](https://discord.ekubo.org).
# Use Ekubo Wallet with AI agents
> Connect an AI agent and use Ekubo Wallet as its local custody and execution boundary
Ekubo Wallet exposes a local Model Context Protocol (MCP) server to supported agents on the same computer. The agent receives a restricted wallet capability; owner-only capabilities remain in the native application. The local connection uses a versioned stdio bridge and same-user operating-system IPC. It has no HTTP listener, OAuth flow, bearer token, or client secret.
Follow the [installation guide](/wallet/install/) to configure a supported harness. Installing the fixed, credential-free connection entry is not an approval and grants no owner capability.
## How a request works
[Section titled “How a request works”](#how-a-request-works)
1. The agent reads the wallet and enabled-network inventory instead of guessing an account or chain.
2. It reads balances, trusted token metadata, and the active signing policy as needed.
3. It obtains an exact execution plan from an appropriate producer. The wallet does not construct transfers, protocol actions, or calldata.
4. Ekubo Wallet simulates the exact calls against the configured network.
5. The active policy allows the transaction, denies it, or sends it to the owner for native review.
6. The agent waits until the request reaches a final result.
A simulation is not approval. If a request is waiting on the owner, review it in Ekubo Wallet’s **Inbox**. The agent should continue waiting rather than returning a request identifier and treating the task as finished.
If you ask to inspect one transaction that your policy would otherwise send automatically, the agent can submit it with `must_review` enabled. That adds native review to this submission only; it does not change policy, approve anything, or make a denied transaction sendable.
Typed-data and personal-message signatures always require native owner review. An agent cannot export a key, accept legal terms, approve its own request, or install a proposed policy.
An agent can also install an [automation](/wallet/automations/): bytecode the wallet polls on a schedule and whose returned calls take this same path. That is a way to suggest transactions without staying awake, not a way to gain authority — the policy still decides whether any of them send, and an automation whose calls the policy does not allow stops on its first tick.
A transaction allowed by policy can reach the wallet’s core signer without a native review. That gives the local server a narrow signing operation, not the key itself: the agent never receives raw key material or an owner-authentication capability. Read [Security and privacy](/wallet/security/) for the complete local boundary.
Signing policy belongs to the wallet account rather than to one agent connection. The same allow rule also applies to an equivalent transaction requested by an installed automation or a connected WalletConnect dapp. Read [Signing policies](/wallet/policies/) before granting unattended authority.
Windows and Linux prompt-injection boundary
The statement above describes the MCP interface. On Windows and Linux, a prompt-injected harness that can execute shell commands or programs as the logged-in user can bypass MCP and query the current per-user credential backend directly for raw keys. Wallet policy and review cannot govern an external signer created from an extracted key. Read the [credential-store limitation](/wallet/security/#windows-and-linux-credential-store-limitation).
When a producer returns an `artifact_reference`, the agent must pass that JSON object to the wallet verbatim. It must not encode the object as a JSON string, rename fields, or reconstruct the execution plan. The wallet fetches the body itself and verifies the declared integrity and byte count.
The `reference` argument is an object, never quoted JSON text. Its execution-plan shape is:
```json
{
"kind": "artifact_reference",
"artifact_type": "execution_plan",
"url": "https://…",
"integrity": {
"algorithm": "keccak256",
"value": "0x…"
},
"bytes": 1234
}
```
Those values are illustrative only. Copy the complete object returned by the producer; do not substitute placeholders or reuse an expired reference.
## Local wallet and public Ekubo service
[Section titled “Local wallet and public Ekubo service”](#local-wallet-and-public-ekubo-service)
For harnesses whose configuration supports both local and remote MCP, agent setup installs two complementary entries:
* The local Ekubo Wallet entry reads wallet state, simulates, queues reviews, signs, submits, and reports results.
* The public Ekubo service at `https://mcp.ekubo.org/mcp` reads protocol data and prepares unsigned actions such as transfers, swaps, and liquidity operations. It receives no additional authority over the wallet.
The hosted service can temporarily store exact execution plans and other artifact bodies so the wallet can retrieve them by reference. Those plans can identify a sender and intended action. The local wallet independently retrieves, verifies, and simulates supported artifacts. It does not trust a friendly description in place of the exact calls it will display and potentially sign. Read [Security and privacy](/wallet/security/) for the service boundary.
Claude Desktop’s configuration file receives only the local entry. Add the public service separately as an account-level custom connector through **Customize → Connectors**.
The hosted service is not the only possible producer, and its protocol coverage changes without a wallet release. [Supported protocols](/wallet/protocols/) lists what it prepares today and states the contract a third-party MCP server satisfies to produce plans this wallet will execute.
## Harness limitations
[Section titled “Harness limitations”](#harness-limitations)
Supported harnesses include Codex, Claude Code, Claude Desktop, Gemini CLI, Cursor, OpenCode, and Grok Build. The ChatGPT desktop app can use the installed local MCP connection from its **Work** or **Code** tab; there is no ChatGPT wallet plugin.
Harness providers apply their own tool-use rules before a request reaches Ekubo Wallet. Some clients classify transaction submission as financial activity and refuse to call the wallet even though the wallet would require its own policy check or native review. Claude Desktop is a known example: use **Code** mode for the best chance of reaching wallet tools. If a harness still refuses, move the task to another supported harness. Changing harnesses does not weaken Ekubo Wallet’s local policy, simulation, or owner-review boundary.
## Example requests
[Section titled “Example requests”](#example-requests)
* “`/loop` Manage my USDC/USDG liquidity position on Ethereum. Claim fees, rebalance when the range drifts, and ask before every transaction.”
* “Claim all fees from my ve33 votes, sell balances worth more than their gas, and max stake the STONX proceeds.”
* “Compare moving half my ETH/USDC liquidity into USDC/USDG using current pool data. Show expected balances, gas, and range risk before preparing anything.”
* “Find every nonzero token approval from my company wallet, rank the risky spenders, and prepare revocations for the ones I choose.”
* “Propose a policy that only permits this recurring action, including exact constraints on its tuple arguments.”
Addresses, chain IDs, amounts, calldata, typed data, and message bytes are exact values. Check them in the native [approval review](/wallet/approvals/) rather than relying only on an agent’s summary.
## Remove a connection
[Section titled “Remove a connection”](#remove-a-connection)
Open **Settings** and use the detected-agent controls to remove wallet-managed entries. This removes only the exact `ekubo_wallet` local entry and, where supported, the `ekubo` hosted companion entry. The wallet does not issue a persistent local bearer credential to revoke. Claude Desktop account connectors are managed in Claude’s **Customize → Connectors** screen.
# Review requests
> Understand native transaction and signature reviews in Ekubo Wallet
Requests that need the owner appear in Ekubo Wallet’s **Inbox**. An agent or dapp can create a request, but it cannot operate the review controls or authenticate as the owner.
Every review starts on **Reject**. **Approve** remains unavailable until you reach the end of the complete review. The document keeps the exact payload available alongside interpreted details, including calldata, message bytes, complete typed data, digests, warnings, Unicode controls, and visually confusable characters.
## Before approving
[Section titled “Before approving”](#before-approving)
Check the account, network, destination, value, and exact operation. Treat token names, dapp descriptions, simulation output, and agent explanations as context rather than authority. If the request is unexpected or any exact value is wrong, reject it.
Selecting **Approve** starts operating-system owner authentication. After authentication, the wallet reloads the request and active policy and verifies that the reviewed document still matches what will be signed. If anything relevant changed, it does not sign the stale review.
For a transaction, approval also submits the exact signed envelope before the review closes; no agent or dapp has to return and ask the wallet to broadcast it. If every configured endpoint refuses those bytes, the activity remains **Signed** and the wallet reports the failure so **Send now** can retry the same envelope.
Closing a review records no decision. Reopening or refreshing it starts safely on **Reject** again. Cancelling owner authentication leaves the request pending.
Notifications and tray menus do not contain approval actions. Detailed notifications are the default: they name the account and network, while a private preview tells you to open Ekubo Wallet. Neither form shows the request identifier or exact payload. Transactions, message signatures, typed-data signatures, and WalletConnect pairing proposals all raise notifications. Opening a waiting request goes to its exact review; opening a decided or lifecycle update goes to its Activity record. A pairing proposal names the dapp, because at that point there is no account or network to name yet. Return to the native wallet to make a decision.
## Signature reviews
[Section titled “Signature reviews”](#signature-reviews)
A signature review opens the same way a transaction review does, by stating what approving it lets someone else do, and then shows the exact payload unaltered beneath that reading.
For typed data, an **Effects** section leads. When the payload is a recognized token permit — ERC-2612, a DAI-style permit, a Permit2 allowance, or a Permit2 signature transfer — the review names the amount and token, who may draw it, and the two lifetimes separately: how long the allowance itself lasts and how long the signature stays usable. Showing only the second can make an allowance that effectively never lapses look like one that expires within the hour. A deadline set to a sentinel beyond any readable date is reported as never rather than printed as digits.
Token names and amounts in that reading come from the owner-confirmed token database alone, never from the contract being signed for, so an unlisted token stays unnamed. An effectively unlimited allowance is called unlimited and carries a warning. A Permit2 signature transfer is flagged as one, because the signature itself moves the tokens once and needs no further transaction from the owner. Every recognized permit carries the standing warning that it moves tokens exactly as an on-chain approval does, is redeemed by whoever holds it rather than by this wallet, and will not appear in this wallet’s activity when it is used.
A payload the wallet does not recognize as a permit says so plainly, and says that this is not a promise the payload grants nothing — the types and values below are the only authority on what it means. A payload shaped like a permit that names some other account as the owner is still rendered, with a warning saying it is signed for somebody else’s benefit; refusing to display it would leave the owner deciding from raw JSON.
Message signatures lead with the fact that nothing moves and that the signature proves control of the signing address to whoever holds it, for as long as they hold it. An ERC-4361 sign-in message is read field by field, including the domain it signs you in to, rather than left as prose. Recognition is structural, so a message that merely mentions signing in is not given a login’s framing.
## What policies change
[Section titled “What policies change”](#what-policies-change)
A matching allow rule can let a call proceed without a native review. A local agent can also ask for one otherwise allowed submission to be reviewed, which adds this screen without changing policy. A matching review rule sends it here instead. A matching deny rule rejects it and cannot be overridden from the review screen. If no rule matches, the request follows the ordinary owner-review path. A transaction takes the least permissive result across all of its calls, so one call needing review brings the whole batch here.
Policy rules do not distinguish the requesting agent, automation, or WalletConnect dapp. The same allow rule applies to the same matching transaction from any of those sources; the source shown in the review and activity history is context, not an authorization input.
Typed-data and personal-message signatures always require native review because they can create reusable authority outside a transaction. Read [Signing policies](/wallet/policies/) before allowing recurring agent actions.
# Automations
> Run installed bytecode on a schedule so the wallet reacts to on-chain conditions without a live agent
An automation is EVM runtime bytecode plus a schedule, installed against one wallet and one network. On each tick Ekubo Wallet runs that bytecode against live chain state and reads back a list of calls. A tick that returns no calls ends there. A tick that returns calls turns them into one atomic batch and hands it to the same path every other transaction takes: exact simulation, evaluation against the active [signing policy](/wallet/policies/), and only then a signature.
The feature exists for latency. An agent that would otherwise have to be awake and holding a conversation to notice a chain condition instead compiles the condition once and lets the wallet watch for it.
Automations have their own tab in the native application, directly after **Policies**, because an automation is only ever as permitted as the policy it runs under.
## An automation adds no authority
[Section titled “An automation adds no authority”](#an-automation-adds-no-authority)
An automation is a source of proposed transactions, not a permission. It supplies calls the same way an agent supplies an execution plan, and the policy decides whether any of them send. There is no automation-specific signing path and no policy exemption.
That is why installing one is not a widening of authority, and why it does not open an approval dialog. Bytecode can only emit calls; every call is evaluated against the installed policy at send time, and a batch that does not resolve to allow for every call never reaches the signer. An automation that an owner installs and then forgets is bounded by the policy they installed, in the same way an agent they leave running is.
Automations run only while the application is running. There is no separate application-level lock state, headless mode, or background service, and nothing runs after you quit. An automatic signature also depends on the operating system’s credential service allowing the wallet process to read the key.
## What one tick does
[Section titled “What one tick does”](#what-one-tick-does)
A tick polls the bytecode through an `eth_simulateV1` request against the network’s configured endpoint, with the bytecode installed as the code at the wallet’s own address and the call originating from that same address. Inside the poll, `msg.sender` is the wallet, which is what makes a `msg.sender`-gated `claim` or `withdraw` behave during the poll the way it will behave in the batch the wallet actually sends.
The bytecode exposes one entry point:
```solidity
interface IEkuboAutomation {
struct Call {
address to;
uint256 value;
bytes data;
}
/// Runs as the wallet, at the wallet's address, inside a simulation whose
/// writes are discarded. Deliberately not `view`: probe freely.
function automate(bytes calldata config) external returns (Call[] memory);
}
```
The poll’s writes are discarded with the rest of the simulation. Nothing the bytecode does during a tick reaches the chain; only the calls it returns can, and only after the policy allows them.
An empty array means there is nothing to do, which is the normal, healthy outcome of most ticks. A non-empty array becomes a single batch that succeeds or reverts as a unit. A revert, or a return value that is not an `(address,uint256,bytes)[]`, is a failed tick.
Because the poll uses `eth_simulateV1`, the network’s endpoint has to support that method. An endpoint that does not will fail every tick. When a network has several endpoints, a tick that fails for RPC reasons tries the next one; a revert or an undecodable return is a fact about the bytecode, so it is reported rather than retried elsewhere.
## Schedules
[Section titled “Schedules”](#schedules)
A schedule is a six-field cron expression, seconds first, evaluated in UTC. Six fields rather than the usual five because minute resolution cannot express “about every block”, which is the cadence this feature exists for. A five-field expression pasted from a crontab is rejected rather than reinterpreted, and an expression that names no real moment, such as the 31st of February, is refused at install time instead of displaying a next run that never arrives.
| Expression | Meaning |
| ---------------- | ------------------- |
| `*/12 * * * * *` | roughly every block |
| `0 */5 * * * *` | every five minutes |
| `0 0 * * * *` | hourly, on the hour |
| `0 30 13 * * *` | daily at 13:30 UTC |
Schedules are UTC because a local-time schedule has to answer what happens to an 02:30 job during a daylight-saving transition. Write the UTC hour you mean.
**Missed ticks are skipped, never backfilled.** If the application was closed, the wallet was locked, or the machine was asleep across ten fire times, the automation runs once at the next tick rather than ten times in a row. Every tick derives its calls from live chain state, so a backlogged tick would be acting on an intent computed for a chain that no longer exists.
**One transaction is in flight at a time per wallet and network.** A tick that arrives while this automation’s own last transaction is still settling, or while any other send holds that wallet and network, is skipped with the reason recorded. It is skipped rather than queued, for the same reason: the next scheduled tick recomputes from scratch. Several automations on one wallet and network are scheduled independently but share that single slot, so the ones that lose it record honestly that it was taken instead of stacking up.
Very frequent expressions are therefore self-limiting. An automation asking to fire every second is polled at most once a second, and while a transaction it sent is still settling, its ticks skip.
For a per-block cadence, a network configured to treat a transaction as settled after a single confirmation lets the next tick proceed sooner. The exposure that accepts is that a reorg can un-mine the transaction the next tick was planned on top of; bytecode that re-derives its intent from live state every tick self-corrects when that happens. A network carrying reviewed transfers may prefer a deeper confirmation setting.
Because an automation’s transactions send without a review, the rule that allows them is worth constraining beyond its calls. A policy rule can match the exact prepared envelope as well, including `max_fee_per_gas`, `gas_limit`, and `max_priority_fee_per_gas`, which is what bounds the fee a dishonest endpoint can cause an unreviewed send to pay. Automations add no separate fee cap of their own.
## Installing one
[Section titled “Installing one”](#installing-one)
An authorized agent installs an automation directly, without an owner confirmation step, for the reason above: an automation cannot exceed the authority the owner already granted through the policy. What an install must do is name the policy revision it was written for. Naming a revision that is no longer active is refused outright, so an automation is never bound to a policy the agent did not read.
Installs are idempotent under a caller-chosen key that is unique per wallet. Installing again under an existing key replaces that automation rather than adding a second one, which makes retrying a timed-out install safe and is also how new bytecode, a new config, or a new schedule ships for the same job. Replacement clears the failure count, the stopped reason, and the record of the last transaction, because those described bytecode that is no longer installed.
A `config` byte string is stored alongside the bytecode and passed to `automate` on every tick. It exists so one compiled blob serves many parameterizations, such as an address, a threshold, or a pool identifier, without recompiling.
## Bound to one wallet, one network, one policy revision
[Section titled “Bound to one wallet, one network, one policy revision”](#bound-to-one-wallet-one-network-one-policy-revision)
Every automation records the policy revision that was active when it was installed. A tick reads the current revision first, and if it does not match, the automation does not run. It has not failed; it moves to **awaiting relink** and waits for the owner to look at it again.
The threat this closes is one that only appears later. An automation whose calls the policy rejects stops, which is correct and visible. But if the owner then widens the policy for some unrelated reason, a design that only checked the policy at send time would silently re-arm that stopped job, now authorized by a rule written for something else entirely. Nobody decided that. Binding to the revision makes re-arming an explicit act, which is also why the binding is to the whole revision rather than to the particular rules an automation happens to touch: narrowing it would be a guess about which edits matter.
An automation is likewise bound to one wallet and one network. It does not follow a key imported elsewhere, and there is no operation that moves one between wallets or chains. Bytecode is network-specific in two independent ways, since both the addresses it references and the EVM features it was compiled for belong to a particular chain.
## Why an automation stops
[Section titled “Why an automation stops”](#why-an-automation-stops)
Nothing retries. Every terminal disappointment stops the automation and records why, because bytecode that emitted a reverting or disallowed call will emit it again on the next tick, and stopping is the only response that does not burn gas or fill the approval queue in a loop.
| Reason | State |
| ------------------------------------------------------------------------------------------- | --------------- |
| The policy did not allow every call, or the batch’s simulation failed | disabled |
| The batch reverted on chain | disabled |
| The batch did not mine within 30 minutes | disabled |
| Ten consecutive failed ticks (RPC error, revert, undecodable return, or a failed broadcast) | disabled |
| The owner pressed **Stop**, or an agent disabled it | disabled |
| The signing policy revision changed | awaiting relink |
When a policy rejects the batch, the request that was left waiting for review is the diagnostic: it shows exactly which call the policy did not permit. Exactly one such request survives, rather than one per tick, precisely because the automation stops.
To start a stopped automation again, use **Run again** on its card in the Automations tab, which rebinds it to the current policy revision. The card is where the bytecode hash, size, schedule, bound revision, and stop reason are shown, so what is being restarted is on screen next to the control that restarts it. An agent can also reinstall it under the same key, which replaces the bytecode and restarts it against the active revision. There is no separate re-enable operation for an agent, so restarting always states exactly what will run.
## The Automations tab
[Section titled “The Automations tab”](#the-automations-tab)
Each automation appears as a card carrying its name, the account it spends from, and its network; its schedule and next run time in UTC; and, when it has stopped, the reason first, because that is the only thing on the screen the reader has to act on. Below that are the outcome of the last run and the automation’s identity: the keccak256 hash of its bytecode, the bytecode’s size in bytes, the policy revision it is bound to, and its key.
The tab cannot show what bytecode does. It shows the hash, which is what an owner can compare against the hash the agent that compiled it reported.
Every tick is appended to that automation’s run history, including the quiet ones that found nothing to do and the ones that skipped. A log that kept only eventful runs could not tell a quiet automation from a stopped one, which is usually the distinction a reader came for. The history is capped per automation and trimmed oldest first.
A run that produced a transaction links straight to that transaction’s ordinary detail view. That link keeps working however long ago the run happened, because clearing activity history hides those records rather than deleting them: a hidden record is absent from every list and still opens by reference. The per-wallet history cap likewise skips any record an automation run points at. The cap exists to bound storage, not to break the record of what the wallet did while nobody was watching.
Owner controls are **Stop** on a running automation and **Run again** on a stopped one.
## Writing one
[Section titled “Writing one”](#writing-one)
The wallet does not compile anything and has no opinion about the source language. It takes deployed runtime bytecode as hex, which is a compiler’s `deployedBytecode` rather than its creation `bytecode`. Handing over creation code is the most common first mistake, since the wallet never runs a constructor and never deploys anything.
An agent connected over [MCP](/wallet/agents/) has four tools — install, list, disable, and dry run — and a bundled skill at `wallet://skills/write-ekubo-automation/SKILL.md` that carries the interface, the rules below, and worked Solidity examples. The skill is the authoring reference; what follows is why the rules exist.
The dry-run tool is the compile-test-fix loop. It runs bytecode once against live chain state without installing, scheduling, signing, or storing anything, and reports the calls it returned, the full simulation of the batch those calls become, the policy findings that decide whether the batch could send automatically, and, on failure, the revert selector, a decoded `Error(string)` or `Panic(uint256)`, and the raw return bytes. Given a cron expression it also validates it and shows the next few times it would fire. Dry-running before installing is what prevents the most common broken automation: one whose calls the active policy was never going to allow, which stops on its first tick.
Four constraints follow from where the code runs, and each produces a bug that looks like nothing else:
* **Declare no state variables.** The code runs at the wallet’s address, so `SLOAD` reads the wallet’s storage at the delegated account implementation’s layout, not the contract’s own. Use `constant` values or read parameters out of `config`.
* **No `immutable` values, no constructor, no external libraries.** All three leave unresolved placeholders in a deployed-bytecode artifact. Internal libraries inline and are fine.
* **There is no memory between ticks.** Every write is discarded, so a counter, a cursor, or a “last run” timestamp has to be derived from chain state the bytecode can read: the effect of its own previous transaction, a timestamp the target contract stores, a balance that changed.
* **Receiver hooks and ERC-1271 do not answer during the poll.** The state override replaces the wallet’s code for the duration of the tick, so a contract that calls back into the wallet reaches the automation’s dispatcher instead of the account implementation. Probing such a call requires answering those selectors, or delegating to the canonical implementation for anything unhandled. This affects the poll only; the batch the wallet sends executes through the intact delegation.
Because the poll’s writes are thrown away, probing is the intended pattern rather than a workaround. Bytecode is free to perform the state-changing call it is considering, inspect the result, and emit it only if the result clears a threshold.
## Limits
[Section titled “Limits”](#limits)
| Limit | Value |
| ----------------------------------------- | -------------- |
| Bytecode | 49,152 bytes |
| Config | 8,192 bytes |
| Name and key | 120 characters |
| Automations per wallet and network | 32 |
| Calls returned by one tick | 4,096 |
| Calldata across one batch | 8 MiB |
| Run records kept per automation | 2,000 |
| Consecutive failed ticks before it stops | 10 |
| Time a sent batch may go unmined | 30 minutes |
| Time one tick’s RPC conversation may take | 20 seconds |
The call and calldata limits are the execution plan’s own, not a second set, so a batch this rejects is one the plan would have rejected a moment later.
# Install Ekubo Wallet
> Install Ekubo Wallet, understand platform key storage, and set up desktop integrations
Windows and Linux key-storage warning
The current Windows and Linux builds store raw account private keys and the encrypted database key in a per-user credential service that does not isolate them to Ekubo Wallet. Malware running as the same logged-in user can extract those keys without wallet approval. This includes a prompt-injected local AI agent or harness that is allowed to execute shell commands or programs as you. An extracted key can be used outside the wallet, bypassing every signing policy and native review. Read the [platform limitation](/wallet/security/#windows-and-linux-credential-store-limitation) before downloading either build.
Download the current stable package from [wallet.ekubo.org](https://wallet.ekubo.org/). The release provides an Apple Silicon DMG for macOS, an x86-64 installer for Windows, and x86-64 AppImage and DEB packages for Linux. The Windows installer is Authenticode-signed through Azure Trusted Signing and names Ekubo, Inc. as the publisher. Microsoft Defender SmartScreen can still interrupt the install until the certificate accrues download reputation; the publisher name in that prompt is what identifies a genuine build.
After installation, open Ekubo Wallet and accept the legal documents shown by the application. Create a new account or import an existing private key from the **Accounts** screen before connecting an agent or dapp. New keys go directly to the operating system’s credential service and are not shown by the wallet UI. On Windows and Linux, that service encrypts credentials at rest but does not protect them from other processes running as the same user. The current macOS Keychain backend applies application code-signature access controls.
Caution
Anyone who obtains an imported or exported private key can control that account. Never paste a private key into a website, chat, issue, log, or agent prompt.
## Connect a supported AI agent
[Section titled “Connect a supported AI agent”](#connect-a-supported-ai-agent)
Open **Settings** in Ekubo Wallet. The **Detected agents** section shows Codex, Claude Code, Claude Desktop, Gemini CLI, Cursor, OpenCode, and Grok Build when they are present on the same computer. A check or X shows whether each detected agent is configured. Choose **Install** or **Remove** on that row to change only that agent. Installation adds the local wallet and, where the harness configuration supports remote MCP, the public Ekubo service.
Installation status describes the managed configuration entry, not whether an agent process is currently running. A configured agent starts its credential-free stdio bridge when it needs the wallet, and that bridge may connect and disconnect as the agent starts or exits.
The wallet writes an exact `ekubo_wallet` entry containing only the absolute path of its versioned bridge and the fixed `--client ` argument. Where supported, it also writes an `ekubo` entry containing only `https://mcp.ekubo.org/mcp`. Neither entry contains an access token, refresh token, authorization header, client secret, or wallet key. Creating or repairing these credential-free entries does not require owner authentication.
Keep Ekubo Wallet running while an agent uses wallet tools. The installed bridge reconnects when the same wallet version opens or restarts, so the harness can remain open. If the wallet and bridge versions differ during initialization, the bridge exits instead of retrying. Repair that agent’s installation in Settings and start a new agent session so the harness launches the matching helper. Closing the wallet window keeps the application available from its tray or menu-bar icon; on macOS, clicking the Dock icon opens the wallet window again.
See [Use Ekubo Wallet with AI agents](/wallet/agents/) for the request flow and trust boundary.
## ChatGPT and Claude Desktop
[Section titled “ChatGPT and Claude Desktop”](#chatgpt-and-claude-desktop)
There is no ChatGPT wallet plugin. Install the Codex connection from the wallet’s **Detected agents** settings, then use the ChatGPT desktop app’s **Work** or **Code** tab to reach that local MCP server. Ordinary chat modes do not expose the local wallet tools.
There is no Claude Desktop wallet plugin or MCP Bundle. Ekubo Wallet writes Claude Desktop’s local stdio entry directly. Claude Desktop does not accept the hosted service in that file, so add a custom connector named **Ekubo** with URL `https://mcp.ekubo.org/mcp` through **Customize → Connectors**. That connector belongs to your Claude account.
Use Claude Desktop in **Code** mode. Claude may classify transaction tools as financial activity and refuse before a request reaches the wallet, especially outside Code mode. If it still refuses, use another supported harness such as Codex, Claude Code, Gemini CLI, Cursor, or OpenCode. The wallet applies the same simulation, policy, and native-review boundary regardless of harness.
## Linux owner authentication
[Section titled “Linux owner authentication”](#linux-owner-authentication)
Owner-authenticated operations on Linux require the packaged polkit policy to be installed in the system polkit actions directory by an administrator. If the policy is unavailable, Ekubo Wallet reports what must be installed instead of silently bypassing owner authentication.
## Updates
[Section titled “Updates”](#updates)
Packaged installations check for stable releases. Ekubo Wallet shows the available version and asks for confirmation before downloading an update. It verifies the downloaded artifact before shutting down to install it. Some Linux package installations use the release page rather than updating in place.
# Signing policies
> Control which wallet transactions are allowed, denied, or sent to native review
A signing policy controls transaction requests from local agents, installed automations, and connected WalletConnect dapps. It does not grant any requester access to private keys or owner-only settings.
New accounts begin with an empty policy, so transactions ordinarily wait for owner review. Policy rules are evaluated from top to bottom, and the first matching rule decides each call:
* **Allow** lets a matching call proceed without asking the owner again.
* **Review** requires explicit owner approval for a matching call.
* **Deny** rejects a matching call without queuing it, and cannot be overridden from the review screen.
* No match also requires [native review](/wallet/approvals/).
Every call is evaluated, and the transaction takes the least permissive result among them, ordering deny below review below allow. So a batch proceeds automatically only when every call in it is allowed, and a single denied call rejects the whole batch.
Policies are not scoped to a requester
Rules match transaction fields, not the MCP client, harness, automation, dapp, WalletConnect session, or displayed plan source. An allow rule written for an agent also allows the same matching transaction when an installed automation or connected dapp requests it. Personal-message and typed-data signatures remain native-review-only.
## Build a narrow rule
[Section titled “Build a narrow rule”](#build-a-narrow-rule)
Open **Policies** in Ekubo Wallet to use the guided editor. A rule can constrain the network, destination, native value, and calldata of each call. Conditions in one rule are combined; leaving a field unrestricted broadens what the rule matches.
A rule can also constrain the exact prepared transaction envelope that carries those calls: its `transaction_type`, `nonce`, `gas_limit`, `max_fee_per_gas`, `max_priority_fee_per_gas`, the `delegation` target of a signed EIP-7702 authorization, and the envelope’s own `envelope_to` and `envelope_native_value`. Per-call native value and envelope native value are distinct, and neither is a cumulative spending budget. The fee matchers are the ones worth setting on any rule that signs without a review, because they bound what a misreporting endpoint can cause that signature to pay.
For contract calls, prefer a full function signature whose top-level parameters are all named, such as `approve(address spender, uint256 amount)`, and constrain arguments by those names. Put narrow exceptions before broader rules because order is part of the policy’s authority. The wallet rejects rules that are provably unreachable behind an earlier rule.
Tuple arguments use a positional `tuple` predicate. Its array must have exactly the tuple’s ABI arity; a predicate constrains that position and `null` leaves only that position unrestricted. Tuple predicates can nest, and an array of tuples uses `{"each":{"tuple":[...]}}`. For example, `{"tuple":[{"eq":"1"},null,{"eq":"true"}]}` constrains the first and third values of a three-element tuple.
Danger
An unrestricted allow rule permits any transaction that an agent, installed automation, or connected dapp can request from that account. Use the narrowest rule that expresses the recurring action you intend.
## JSON Schema and editor
[Section titled “JSON Schema and editor”](#json-schema-and-editor)
The editor below uses the same generated JSON Schema offered for download. It provides JSON syntax highlighting, schema-driven property and value completion, inline structural validation, and automatic formatting when focus leaves valid JSON. Choose an example to replace the editor contents with formatted JSON, then change it to explore the policy language. Examples containing addresses use obvious placeholders and are not ready to propose unchanged. Choose **Review every transaction (Default)** to restore the default behavior of asking the owner about every transaction.
### Policy JSON editor
Press `Ctrl`+`Space` to see properties and values from the schema. Errors are underlined and listed in the diagnostics panel.
Example policy
Review every transaction (Default) (review)
An empty policy leaves every transaction for owner review.
Loading the policy schema…
The interactive editor requires JavaScript. The schema remains available from the download link below.
[Download the Ekubo Wallet policy JSON Schema](/schemas/ekubo-wallet-policy.schema.json)
View the complete JSON Schema
```
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Ekubo Wallet policy",
"type": "object",
"properties": {
"$schema": { "type": ["string", "null"] },
"version": { "type": "integer", "minimum": 1, "maximum": 1 },
"rules": { "type": "array", "maxItems": 256, "items": { "$ref": "#/$defs/Rule" } }
},
"additionalProperties": false,
"required": ["version", "rules"],
"$defs": {
"Rule": {
"type": "object",
"properties": {
"effect": { "$ref": "#/$defs/Effect" },
"label": { "type": ["string", "null"] },
"chain_id": { "anyOf": [{ "$ref": "#/$defs/Predicate" }, { "type": "null" }] },
"to": { "anyOf": [{ "$ref": "#/$defs/Predicate" }, { "type": "null" }] },
"native_value": { "anyOf": [{ "$ref": "#/$defs/Predicate" }, { "type": "null" }] },
"calldata": { "anyOf": [{ "$ref": "#/$defs/Predicate" }, { "type": "null" }] },
"transaction_type": { "anyOf": [{ "$ref": "#/$defs/Predicate" }, { "type": "null" }] },
"nonce": { "anyOf": [{ "$ref": "#/$defs/Predicate" }, { "type": "null" }] },
"gas_limit": { "anyOf": [{ "$ref": "#/$defs/Predicate" }, { "type": "null" }] },
"max_fee_per_gas": { "anyOf": [{ "$ref": "#/$defs/Predicate" }, { "type": "null" }] },
"max_priority_fee_per_gas": { "anyOf": [{ "$ref": "#/$defs/Predicate" }, { "type": "null" }] },
"delegation": { "anyOf": [{ "$ref": "#/$defs/Predicate" }, { "type": "null" }] },
"envelope_to": { "anyOf": [{ "$ref": "#/$defs/Predicate" }, { "type": "null" }] },
"envelope_native_value": { "anyOf": [{ "$ref": "#/$defs/Predicate" }, { "type": "null" }] }
},
"additionalProperties": false,
"required": ["effect"]
},
"Effect": {
"oneOf": [
{ "type": "string", "const": "allow" },
{ "type": "string", "const": "deny" },
{ "type": "string", "const": "review" }
]
},
"Predicate": {
"oneOf": [
{ "type": "string", "const": "any_value" },
{ "$ref": "#/$defs/Eq" },
{ "$ref": "#/$defs/In" },
{ "$ref": "#/$defs/Lt" },
{ "$ref": "#/$defs/Lte" },
{ "$ref": "#/$defs/Gt" },
{ "$ref": "#/$defs/Gte" },
{ "$ref": "#/$defs/Selector" },
{ "$ref": "#/$defs/Each" },
{ "$ref": "#/$defs/Tuple" },
{ "$ref": "#/$defs/Any" },
{ "$ref": "#/$defs/All" },
{ "$ref": "#/$defs/Not" },
{ "$ref": "#/$defs/Length" }
]
},
"Eq": { "type": "object", "properties": { "eq": { "type": "string" } }, "required": ["eq"], "additionalProperties": false },
"In": { "type": "object", "properties": { "in": { "type": "array", "uniqueItems": true, "items": { "type": "string" } } }, "required": ["in"], "additionalProperties": false },
"Lt": { "type": "object", "properties": { "lt": { "type": "string" } }, "required": ["lt"], "additionalProperties": false },
"Lte": { "type": "object", "properties": { "lte": { "type": "string" } }, "required": ["lte"], "additionalProperties": false },
"Gt": { "type": "object", "properties": { "gt": { "type": "string" } }, "required": ["gt"], "additionalProperties": false },
"Gte": { "type": "object", "properties": { "gte": { "type": "string" } }, "required": ["gte"], "additionalProperties": false },
"Selector": { "type": "object", "properties": { "selector": { "$ref": "#/$defs/SelectorPredicate" } }, "required": ["selector"], "additionalProperties": false },
"SelectorPredicate": {
"type": "object",
"properties": {
"abi": { "type": "string" },
"args": { "type": "object", "additionalProperties": { "$ref": "#/$defs/Predicate" } }
},
"additionalProperties": false,
"required": ["abi"]
},
"Each": { "type": "object", "properties": { "each": { "$ref": "#/$defs/Predicate" } }, "required": ["each"], "additionalProperties": false },
"Tuple": { "type": "object", "properties": { "tuple": { "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/Predicate" }, { "type": "null" }] } } }, "required": ["tuple"], "additionalProperties": false },
"Any": { "type": "object", "properties": { "any": { "type": "array", "items": { "$ref": "#/$defs/Predicate" } } }, "required": ["any"], "additionalProperties": false },
"All": { "type": "object", "properties": { "all": { "type": "array", "items": { "$ref": "#/$defs/Predicate" } } }, "required": ["all"], "additionalProperties": false },
"Not": { "type": "object", "properties": { "not": { "$ref": "#/$defs/Predicate" } }, "required": ["not"], "additionalProperties": false },
"Length": { "type": "object", "properties": { "length": { "$ref": "#/$defs/Predicate" } }, "required": ["length"], "additionalProperties": false }
},
"description": "Ordered stateless signing policy over each call and the exact prepared transaction envelope. The first matching rule decides each call: allow signs automatically, review requires explicit owner approval, deny rejects without queuing, and reaching the end also requires review. Every call is evaluated before the transaction takes the least-permissive result (deny < review < allow). Omitted matchers mean anything and present matchers are ANDed. Per-call native value and outer envelope native value are distinct; neither is a cumulative spending budget."
}
```
Schema validation checks document shape, required properties, allowed values, and predicate structure. The wallet remains the final authority: it also checks values in context, validates ABI signatures and predicate applicability, and rejects unreachable rules. A document that passes this browser editor is not installed and grants no permission.
The optional `$schema` property is only an editor hint. Ekubo Wallet does not fetch or trust content from that URL when it evaluates a policy.
## Agent proposals
[Section titled “Agent proposals”](#agent-proposals)
An authorized agent can read the active policy and propose a complete replacement based on its current revision. A proposal does not change permissions, and an agent cannot install it. The owner must inspect the permission diff in the native wallet. A transition that widens or ambiguously changes authority requires operating-system authentication; a transition that the wallet proves only tightens the active policy can be installed from the owner UI without an additional operating-system challenge.
Do not broaden policy merely to finish the request currently waiting. A one-time action can use the ordinary native review path without changing future permissions.
Display labels, token symbols, RPC simulation output, and other network-provided facts are review context, not policy inputs. Policy matching uses exact transaction fields.
## Installing a policy stops automations
[Section titled “Installing a policy stops automations”](#installing-a-policy-stops-automations)
Each [automation](/wallet/automations/) is bound to the policy revision that was active when it was installed. Installing any policy therefore stops every automation on that account until you review each one and choose to run it again. That is deliberate: without it, widening a policy for some unrelated reason would silently re-arm a scheduled job under a rule written for something else.
# Build a plan producer
> Write an MCP server that prepares execution plans Ekubo Wallet will fetch, verify, simulate, and sign, and that gives an agent what it needs to decide
Ekubo Wallet does not build transactions. It resolves an execution plan somebody else prepared, verifies it byte for byte, simulates the exact calls, evaluates the owner’s [signing policy](/wallet/policies/), presents any required [review](/wallet/approvals/), signs, and submits. Everything upstream of that — reading protocol state, choosing a route, encoding calldata, deciding how many calls the action takes — belongs to a *producer*: a Model Context Protocol server the agent talks to alongside the wallet.
The [public Ekubo server](/products/mcp-server/) is one producer. Nothing about the boundary is specific to it, and the first half of this page is the contract a new one satisfies. Satisfy it and the wallet executes your plans without a wallet release, because a plan naming Aave calldata and a plan naming Ekubo calldata are the same kind of document to the wallet: it never learns what protocol it just used.
Validating is not the same as being useful, though, so the second half covers what nothing enforces: how to surface the data behind a decision rather than only its outcome, and how to publish the workflow knowledge that makes a protocol safe to use at all.
## The shape of the exchange
[Section titled “The shape of the exchange”](#the-shape-of-the-exchange)
Three parties, and the middle one carries as little as possible:
1. The agent reads the wallet’s inventory first, so it knows the account and chain before anything is prepared.
2. It calls your preparation tool with that exact sender and chain.
3. You build the plan, store its body somewhere fetchable, and return an `artifact_reference` envelope — not the plan.
4. The agent passes that envelope through to the wallet verbatim, as one object argument.
5. The wallet fetches the body itself, recomputes the digest, validates the plan, simulates it, checks policy, reviews if required, signs, and submits.
The agent never fetches, restates, paraphrases, or reconstructs your plan body, and you should say so in your tool descriptions. This is not only a safety property. A plan body is calldata, ABI fragments, and failure-policy prose: content only the wallet reads, and every byte of it that passes through the agent is paid for twice in model output. The envelope is a few hundred bytes regardless of how large the plan is.
## Return a reference, not a body
[Section titled “Return a reference, not a body”](#return-a-reference-not-a-body)
Every executable preparation result carries an envelope like this, conventionally under a key named `execution_plan_reference`:
```json
{
"kind": "artifact_reference",
"artifact_type": "execution_plan",
"url": "https://plans.example.com/artifact/0f8c2b1e-…",
"integrity": {
"algorithm": "keccak256",
"value": "0x5bec7df0e57e9f5c55461eb450a7aa567a42403f4b32ea55df8e5cff346e1ba5"
},
"bytes": 1632,
"instruction": "Pass this reference object unchanged as the wallet's reference argument."
}
```
| Field | Requirement |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `kind` | Exactly `artifact_reference`. |
| `artifact_type` | `execution_plan`, `read_calls`, or `token_list`. The wallet rejects a reference whose type is not the one the tool being called accepts. |
| `url` | A public `https` URL, or a bounded `data:application/json` URI carrying the body inline. |
| `integrity` | `algorithm` must be `keccak256`; `value` is the 32-byte digest of the exact bytes served. The integrity object is parsed strictly: no extra fields. |
| `bytes` | The exact byte length of the stored body. A body of any other length is refused before it is parsed. |
| `instruction` | Optional prose the agent reads. Use it to say “pass this through unchanged” and “a 404 means re-run this tool”. |
`integrity` and `bytes` are **required** whenever the body travels over the network. A `data:` URI is the exception: there the bytes *are* the reference, so both are verified only if you supply them.
The envelope tolerates additive fields, so a future producer can enrich it without stranding wallets already deployed. Do not rely on that for anything load-bearing: the wallet acts only on the fields above.
### Nothing about time travels in the envelope
[Section titled “Nothing about time travels in the envelope”](#nothing-about-time-travels-in-the-envelope)
There is no `expires_at`, and you should not invent one. A plan’s semantic validity is expressed by the deadline inside its own calldata and enforced by the wallet’s simulation against current chain state, never by comparing two clocks. Storage expiry surfaces as an ordinary `404` on fetch, which the wallet reports as “expired or never existed, re-run the producer’s preparation tool”. Keep bodies fetchable long enough for a human to read a review — minutes, not seconds — and let a stale reference fail loudly rather than serving a plan built against state that has moved.
### Where the body may live
[Section titled “Where the body may live”](#where-the-body-may-live)
The wallet’s plan fetch is the only outbound request it makes that is not a configured chain RPC, so admission is deliberately narrow. A URL that fails any of these is refused before a connection is opened:
* `https` only, on the default port, with no credentials in the authority and no fragment.
* A public, resolvable host. Names ending in `.local`, `.internal`, `.localhost`, `.onion`, or `.home.arpa` are refused, as is any host resolving to a private or reserved address.
* No redirects. The wallet does not follow one, so serve the body at the URL you published.
* A hard body cap of 16 MiB for plans and read bundles, enforced while streaming.
Standard HTTP content encodings are decompressed first, and both `integrity.value` and `bytes` describe those decompressed bytes rather than the wire encoding. Serve the same bytes you hashed.
If you have no public host, or you are still developing, the `data:` alternative removes the hosting requirement entirely:
```plaintext
data:application/json;base64,eyJzY2hlbWFfdmVyc2lvbiI6IjEiLC…
```
Media type must be `application/json`; `;base64` and a `charset=` parameter are the only accepted parameters. Nothing touches the network, and the reference is bounded by the same body cap. This is the shortest path from a new server to a real signature, and it is a legitimate production shape for a small plan.
## The execution plan
[Section titled “The execution plan”](#the-execution-plan)
The stored body is one signer-neutral, ordered transaction sequence. Parsing is strict on both sides: unknown fields anywhere in the plan are a rejection, not a warning. The only place producer-specific data belongs is the `extensions` bag.
| Field | Required | Meaning |
| --------------------------- | -------- | ----------------------------------------------------------------------------------- |
| `schema_version` | yes | The string `"1"`. Not a number. |
| `chain_id` | yes | Canonical unsigned decimal string. Not hex. |
| `caip2_chain_id` | yes | Exactly `eip155:` followed by the same `chain_id`. A mismatch is rejected. |
| `sender` | yes | The one account that signs every step. |
| `ordered_steps` | yes | 1 to 4,096 steps, one-indexed and consecutive. |
| `required_capabilities` | no | Behaviors the wallet must implement to execute this plan. Omit unless you need one. |
| `extensions` | no | An object the wallet ignores, bounded at 64 KiB serialized. |
| `simulation_failure_policy` | no | What the agent should do about each class of simulation failure. |
Each step is `step`, `kind`, `transaction`, and an optional `revert_decode`. The transaction carries `chain_id`, `from`, `to`, `data`, `value`, and an optional `gas`.
The wallet checks all of the following before it will simulate anything:
* **Quantities are canonical decimal strings, not hex.** `"0"`, or a digit string with no leading zero, fitting in a `uint256`. That covers `chain_id`, `value`, and `gas`. A JSON number is rejected, and so is `"0x0"`.
* **`data` is `0x`-prefixed, even-length hex**, possibly just `"0x"`.
* **Steps are one-indexed and consecutive.** Step `n` must be at array position `n - 1`.
* **Every step’s `chain_id` equals the plan’s**, and every step’s `from` equals the plan’s `sender`. A plan is single-chain and single-signer by construction; a cross-chain action is two plans.
Emit addresses checksummed. The wallet parses either case, but a producer that normalizes on the way out catches its own transcription errors, and the Ekubo server’s producer-side validator refuses anything else.
### Step kinds
[Section titled “Step kinds”](#step-kinds)
`kind` is not decoration. The wallet shows the owner why a step they did not ask for is in the plan, and it reads that reason off the kind.
| Kind | What it says |
| ------------------------------- | ----------------------------------------------------------- |
| `approval` | Grants a spending allowance the next call needs. |
| `execution` | Does the work the user asked for. |
| `allowance_cleanup` | Takes an allowance back afterwards. |
| `signature_dependent_execution` | Spends a signature approved earlier. |
| `other` | Anything else, and the review says nothing useful about it. |
Label the approval you added as an `approval` rather than folding it into `execution`. It is the difference between a review that explains itself and one that shows the owner two opaque calls.
### Capabilities
[Section titled “Capabilities”](#capabilities)
`required_capabilities` names behavior the wallet must implement, and a wallet that does not implement a listed capability rejects the plan outright rather than adapting it. Today the wallet implements exactly one, `atomic_batch`, so anything else you emit produces plans no deployed wallet accepts.
Multi-step plans are executed as one atomic EIP-7702 batch regardless, so declare `atomic_batch` when the action is only correct as a batch — not on every multi-step plan out of caution. Names are bounded at 64 printable ASCII characters and at most 32 entries.
### Revert decoding
[Section titled “Revert decoding”](#revert-decoding)
An execution step may carry a `revert_decode` plan so the wallet can decode a custom error locally. In an execution plan there is exactly one accepted shape:
```json
{
"kind": "error_result",
"abi": [
{
"type": "error",
"name": "MinimumOutputNotMet",
"inputs": [
{ "name": "minimum", "type": "uint256" },
{ "name": "actual", "type": "uint256" }
]
}
],
"required": false
}
```
The `abi` array holds 1 to 128 entries, at most 64 KiB serialized, must parse as a JSON ABI, and must contain at least one `error`. Supply the target contract’s canonical custom-error ABI and nothing else.
This is worth stating flatly because it is the most common way a producer’s first plan is rejected: **the decode shapes available to prepared reads are not available here.** `function_result`, `abi_parameters`, `multicall3`, `function_result_bytes_array`, and `semantic_value` decode *read results*. An execution plan’s `revert_decode` accepts `error_result` and nothing else. The two surfaces are disjoint.
The wallet owns any batch-wrapper decoding. It unwraps its own execution-layer errors, keeps both the outer and the innermost revert bytes, and applies your step ABI to the innermost. You do not need to know or describe the wrapper.
### Simulation failure policy
[Section titled “Simulation failure policy”](#simulation-failure-policy)
If you supply `simulation_failure_policy`, all three branches are required, and each is an `action` plus a 1 to 2,000 character `instruction`.
| Branch | Meaning | Allowed actions |
| ------------------------ | ----------------------------------------------------------- | ------------------------------- |
| `rpc_error` | The simulation infrastructure failed, not the calls. | any |
| `execution_reverted` | The exact calldata reverted against current state. | `reprepare_plan`, `user_review` |
| `simulation_setup_error` | No trustworthy simulation environment could be established. | `reprepare_plan`, `user_review` |
`retry_same_plan` on either of the last two is a validation failure, not a preference the wallet overrules. Calldata that reverted against current state reverts again; a directive that says otherwise is a producer telling an agent to burn gas in a loop. Swap and bridge reverts, slippage included, always mean re-prepare against fresh state.
### Caps
[Section titled “Caps”](#caps)
The limits are part of the contract, and the wallet enforces them on your input:
| Limit | Value |
| --------------------------- | --------------------- |
| Steps per plan | 1 to 4,096 |
| Total calldata across steps | 8 MiB |
| Serialized plan | 16 MiB |
| `extensions` serialized | 64 KiB |
| `required_capabilities` | 32 entries |
| Revert-decode ABI | 128 entries, 64 KiB |
| Failure-policy instruction | 1 to 2,000 characters |
## A complete example
[Section titled “A complete example”](#a-complete-example)
The body a producer stores for a two-step swap, an approval followed by the swap itself:
```json
{
"schema_version": "1",
"chain_id": "1",
"caip2_chain_id": "eip155:1",
"sender": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"ordered_steps": [
{
"step": 1,
"kind": "approval",
"transaction": {
"chain_id": "1",
"from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"to": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"data": "0x095ea7b30000000000000000000000002222222222222222222222222222222222222222000000000000000000000000000000000000000000000000000000000ee6b280",
"value": "0"
}
},
{
"step": 2,
"kind": "execution",
"transaction": {
"chain_id": "1",
"from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"to": "0x2222222222222222222222222222222222222222",
"data": "0x9d9892cd000000000000000000000000000000000000000000000000000000000ee6b28000000000000000000000000000000000000000000000000001626218b45860000000000000000000000000000000000000000000000000000000000068a4f0c0",
"value": "0"
},
"revert_decode": {
"kind": "error_result",
"abi": [
{
"type": "error",
"name": "MinimumOutputNotMet",
"inputs": [
{ "name": "minimum", "type": "uint256" },
{ "name": "actual", "type": "uint256" }
]
}
]
}
}
],
"simulation_failure_policy": {
"rpc_error": {
"action": "retry_same_plan",
"instruction": "Simulation infrastructure failed rather than the calls themselves. Retry the identical plan once the RPC recovers."
},
"execution_reverted": {
"action": "reprepare_plan",
"instruction": "The exact calldata reverted against current state. Return to the preparation tool for fresh state and calldata; do not resend these bytes."
},
"simulation_setup_error": {
"action": "user_review",
"instruction": "The wallet could not establish a trustworthy simulation environment. Check the selected wallet, network, and RPC chain before continuing."
}
}
}
```
The addresses and calldata above are illustrative. The `integrity.value` and `bytes` shown in the earlier envelope are the real keccak256 digest and byte length of this body serialized without whitespace, which is what a wallet would recompute: the digest covers the exact bytes you serve, so hash the string you store rather than a pretty-printed copy of it.
## Prepared reads
[Section titled “Prepared reads”](#prepared-reads)
A producer that wants live chain state read through the owner’s own RPC — rather than proxying it, holding their credentials, or making the agent assemble calldata — returns a `read_calls` artifact. Same envelope, and the stored body is the wallet’s batch-read argument object exactly, with nothing added:
```json
{
"chain_id": "1",
"block_parameter": "latest",
"calls": [
{
"id": "usdc_balance",
"to": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"data": "0x70a08231000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"decode": {
"kind": "function_result",
"abi": [
{
"type": "function",
"name": "balanceOf",
"stateMutability": "view",
"inputs": [{ "name": "account", "type": "address" }],
"outputs": [{ "name": "", "type": "uint256" }]
}
],
"function_name": "balanceOf",
"required": true
},
"include_raw": true
}
]
}
```
`chain_id` is a positive decimal string, `block_parameter` defaults to `latest` and otherwise takes `pending`, `safe`, `finalized`, `earliest`, or a hex block number, `from` is optional, and `calls` holds 1 to 4,096 entries. The body is parsed with unknown fields rejected, which is the point: a fetched bundle cannot smuggle in a field the tool call itself did not declare.
A `decode` plan on a call is optional and may be `function_result`, `abi_parameters`, `multicall3`, `function_result_bytes_array`, or `semantic_value`. Keep `include_raw` true. Decoding is a convenience, and the raw return bytes are the thing that is actually authoritative — a producer that discards them has no recourse when a decode fails.
Two rules that are easy to get wrong. Do not ask the wallet to send you the decoded result for authoritative interpretation; supply the ABI, let the decode happen locally, and accept back only what you need. And where a read must be atomic with respect to a state-changing call — a virtual-order execution followed by the position read that depends on it — keep both in a single batched `eth_call` rather than splitting them, and never let those calls be broadcast.
A third artifact type, `token_list`, carries curated token metadata so a wallet can label transactions without the agent re-emitting thousands of rows. It travels through the identical envelope.
## What the wallet does next, and what you must not assume
[Section titled “What the wallet does next, and what you must not assume”](#what-the-wallet-does-next-and-what-you-must-not-assume)
Handing over a reference starts a pipeline you do not control and cannot shorten:
The wallet fetches and integrity-checks the body, refusing a mismatch. It validates the plan against everything above. It refuses a plan whose `chain_id` or `sender` disagrees with the connected chain and account — which is why the sender must be bound *before* preparation, from the wallet’s own inventory, and why a mismatch means re-preparing rather than rewriting the sender or switching networks. It simulates the exact calls against real chain state. It evaluates the owner’s current policy against the exact network, target, value, and calldata. Anything not explicitly allowed goes to native review; anything a deny rule matches fails without ever queueing.
Some consequences worth designing around:
* **A simulation is not an authorization.** The handle the wallet returns for a simulated plan is a short-lived pointer to those exact bytes. Sending it repeats simulation, transaction preparation, and the policy evaluation from scratch.
* **The owner’s approval binds the bytes that broadcast.** It covers the plan version, chain, sender, and each step’s number, kind, and transaction. It deliberately does not cover `gas`, `revert_decode`, `simulation_failure_policy`, `required_capabilities`, or `extensions`, none of which change what executes.
* **Do not ask for a second confirmation.** Prompting the user in the agent before invoking the wallet duplicates the wallet’s own authorization flow and trains people to click through it.
* **A new protocol is not a new permission.** Policies match exact networks, targets, values, and calldata shapes, so a first action against a protocol you just added arrives as a review. That is the design, not a misconfiguration.
* **You never receive anything.** No key material, no owner authentication, no ability to approve a request or install a policy. What you do see is the tool arguments the agent sends, which can include the owner’s address and intended action.
Simulation, policy, and native review are what contain a producer. Your good behavior is not the mechanism, which is exactly why a third party can build one.
## Surface the decision, not just the plan
[Section titled “Surface the decision, not just the plan”](#surface-the-decision-not-just-the-plan)
Everything above this point is enforced. Break it and the wallet rejects your plan with a specific error. Nothing in the rest of this page is checked by anything, which is exactly why it is worth writing down: it is the difference between a producer whose plans validate and a producer an agent can actually use well.
A plan is the end of a decision. If the only thing your server returns is calldata, the agent has to reconstruct the reasoning from somewhere else, and it will guess. Eight conventions the hosted Ekubo server follows, each visible in its public tool results and its discovery document at `https://mcp.ekubo.org/`:
**Return options, not an answer.** A swap request comes back with every available quote across every provider, normalized into comparable fields, and the server picks none of them. Ranking on the user’s behalf inside the server hides the trade-off they wanted to see; returning the raw set and a comparable denominator lets the agent present it.
**Attach the plan to each option.** Every quote already carries the `execution_plan_reference` that executes it, so choosing is the whole flow and there is no second preparation step. This is worth copying for the property it produces: the option the user compared is the option that executes. A design where the agent compares quotes and then calls `prepare` reopens a window in which state moves, and if preparation costs a metered upstream quote it pays for the same information twice.
**Bind preparation to the state that was reviewed.** Where a decision depends on indexed state, return a `state_id` with the read and require it back on the prepare call. When the underlying state has moved, refuse with a structured error naming both values rather than preparing against the new world:
```json
{
"code": "ve33_state_changed",
"message": "The indexed VeToken allocation changed after it was reviewed; fetch the current allocation again",
"data": {
"expected_state_id": "0x…",
"actual_state_id": "0x…"
}
}
```
This is the decision-integrity twin of the reference’s integrity digest. The digest guarantees the wallet executes the bytes you built; the snapshot id guarantees you built them from the state the user was shown.
**Say when a ranking is provisional.** Some numbers cannot be computed server-side without becoming the data path. Return the ranking you can compute with `ranking_complete: false` and a `local_read_requirement` the agent executes through the owner’s own wallet and passes back decoded, then rank properly on the second call. The read travels as a `read_calls` reference exactly as described under [prepared reads](#prepared-reads), so you get the number without ever touching the owner’s RPC or holding their credentials.
**Say when a result is a prefix.** A limit that silently truncates reads as the complete set. Return the count before the limit, or a `complete: false`, and let the agent tell the user it is looking at part of the picture.
**Stay out of the data path for protocols you do not operate.** The hosted server publishes `external_market_data.server_role` as `"none"`: agents read Aave, Morpho, Sky, and Lido state from those protocols’ own official APIs and from the owner’s RPC directly, and the server contributes a fixed deployment catalog and transaction preparation. It does not proxy, cache, authenticate to, or replay them, and it refuses to receive an on-chain read result back for authoritative decoding. Not being in the middle of live protocol data is a smaller server, a smaller trust surface, and one fewer thing that can be stale at signing time.
**Refuse in a shape an agent can branch on.** A rate limit answers with the exhausted scope and a retry delay rather than prose, so the agent waits instead of retrying through another route. Every refusal an agent might reasonably retry should carry the machine-readable reason it should not.
**Publish your freshness windows.** The discovery document carries a `polling_guidance` block giving the interval at which each kind of data actually changes. Without it a client either polls far too fast, spending budget on identical answers, or too slowly to be correct. Publishing the number means nobody has to guess it, and you can change it without a client release.
## Ship the workflow, not just the tools
[Section titled “Ship the workflow, not just the tools”](#ship-the-workflow-not-just-the-tools)
A tool description explains one call. It cannot explain that Lido withdrawals are an irreversible asynchronous queue rather than a swap, that a ve(3,3) vote change discards pending fees unless they are claimed first, or which three reads must happen before a deposit is safe. That knowledge is what makes a protocol usable, and if you do not publish it the agent invents it.
Publish it as MCP resources, in two flavors.
**Workflow resources** document your own conventions: the canonical end-to-end sequence, how the plan handoff works, how a particular product’s state should be read. The hosted server publishes several under `ekubo://docs/`, including `ekubo://docs/execution-plan`. An agent reads them directly instead of inferring conventions from tool names.
**Skills** cover a protocol. The shape is a `SKILL.md` with `name` and `description` front matter, a short numbered workflow, and a set of operation-specific gates — the conditions that make each action safe, written as reads with thresholds rather than as advice. Lido’s, for example, requires reading `isStakingPaused()` and `getCurrentStakeLimit()` before a stake, bounds a withdrawal request between 100 wei and 1000 stETH, and requires `ownerOf` and `getWithdrawalStatus` to confirm a claim is owned, finalized, and unclaimed. Alongside it, a `references/discovery.md` names the canonical documentation URLs and the exact contract reads to perform, so the agent goes to the protocol’s own sources rather than to yours.
Serve each skill twice: as an MCP resource at a stable URI such as `ekubo://skills/use-lido`, and as plain HTTP at a path such as `/skills/use-lido/SKILL.md`. The resource is what an agent reads mid-conversation; the HTTP file is what a harness with filesystem skills can install ahead of time. Advertise `listChanged` for both tools and resources so clients learn when the surface moves.
Finally, publish a discovery document at your root that ties it together: the MCP endpoint and transport, the tool catalog and its revision, the resource URIs, the skills map, the freshness and rate-limit contract, and a safety block stating plainly that the server does not sign, does not submit, and requires wallet validation. A client can read all of that before it opens the MCP transport, and a reviewer can read it without running anything.
## Writing the tool descriptions
[Section titled “Writing the tool descriptions”](#writing-the-tool-descriptions)
The agent between you and the wallet is a model reading two servers’ tool descriptions at once, and most integration failures are description failures rather than schema failures. Four things are worth saying explicitly in yours:
Bind the sender first: instruct the agent to read the wallet’s account and network before calling your preparation tool, not after. Relay the envelope whole: say that it is an object argument, never a JSON-encoded string, and that renaming or reconstructing it is wrong. Do not re-prepare a plan you already hold: an agent that “refreshes” a plan before signing discards the one the user compared and, if your preparation costs a metered quote, pays for it twice. And name the 404: a fetch failure means the reference expired and the fix is to call you again, never to rebuild the plan from a description.
## Checklist
[Section titled “Checklist”](#checklist)
Before pointing a wallet at a new producer:
* Plan bodies validate against every rule above, including the ones you think cannot fire. Validate on the way out, fail closed, and never emit a plan you would not accept.
* Bodies are served over public HTTPS with no redirect, or inlined as bounded `data:` URIs.
* `integrity.value` is the keccak256 of the exact served bytes, and `bytes` is their exact length.
* `revert_decode` uses `error_result` only.
* No `simulation_failure_policy` branch tells an agent to resend calldata that reverted.
* `required_capabilities` is omitted unless the plan genuinely needs `atomic_batch`.
* Producer-specific data lives in `extensions`, under 64 KiB, and nothing depends on the wallet reading it.
* Every option you return carries the plan that executes it, so the thing compared is the thing signed.
* A prepare tool whose correctness depends on state the user reviewed takes that snapshot’s id back and refuses when it has moved.
* Every protocol whose safe use needs more than a tool description has a skill, with its gates written as reads and thresholds.
* Truncated results, provisional rankings, and refusals say so in a field, not in prose the agent has to parse.
## The authoritative contract
[Section titled “The authoritative contract”](#the-authoritative-contract)
This page describes the boundary; the servers publish it. `https://mcp.ekubo.org/` returns the current boundary and operational semantics as JSON, its `ekubo://docs/execution-plan` resource documents the handoff in full, and the wallet’s own MCP tool descriptions and argument schemas are the exact surface its execution tools accept. Where any of those disagree with this page, they are correct and the page is stale.
[Supported protocols](/wallet/protocols/) covers what the hosted Ekubo producer prepares today and why coverage grows without a wallet release. [Use Ekubo Wallet with AI agents](/wallet/agents/) is the same boundary from the agent’s side, and [Security and privacy](/wallet/security/) states the complete local boundary.
# Supported protocols
> Which protocols Ekubo Wallet can transact with, why that coverage grows without a wallet update, and how any MCP server becomes compatible
Ekubo Wallet does not implement protocols. It validates an exact execution plan, simulates it against the configured network, evaluates the active signing policy, presents any required native review, signs, and submits. The calldata itself comes from a *producer*: a Model Context Protocol server that reads live protocol state and returns an unsigned plan for the wallet to execute.
That separation is why the wallet’s protocol coverage is not a property of the version you installed. Most of it lives in the public [Ekubo MCP server](/products/mcp-server/), which is deployed on its own schedule.
## What the hosted Ekubo server prepares today
[Section titled “What the hosted Ekubo server prepares today”](#what-the-hosted-ekubo-server-prepares-today)
| Protocol | Actions |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Ekubo** | Swaps, liquidity deposits, withdrawals, and earnings claims, pool initialization, DCA and TWAMM orders, [ve(3,3)](/products/ve33/) staking, voting, reallocation and fee claims, auctions, and [rewards](/products/rewards/) claims |
| **0x** | Same-chain swap quotes, returned alongside Ekubo’s for the same request so the two are compared before either is executed |
| **Across** | Any-to-any bridging, quoted alongside LayerZero and LI.FI whenever a swap’s origin and destination chains differ |
| **LayerZero** | Any-to-any transfers through the Value Transfer API, priced for an exact input amount, and tracked from the origin transaction through to delivery |
| **LI.FI** | Any-to-any bridging priced in either direction, so an exact-output bridge is quoted twice, and tracked through to delivery |
| **Aave V3** | Supply, withdraw, borrow, repay, collateral toggles, and eMode categories on Ethereum, Base, Arbitrum, Optimism, Polygon, and Avalanche |
| **Morpho Vault V2** | Vault deposits, withdrawals, and redemptions on Ethereum and Base |
| **Sky Savings** | USDS and sUSDS deposits, withdrawals, and redemptions on Ethereum |
| **Lido** | ETH staking, wstETH wrapping and unwrapping, and withdrawal requests and claims on Ethereum |
| **Merkl** | Incentive reward claims, for campaigns on any protocol Merkl covers, on the 22 chains where the Distributor deployment has been verified |
| **Aerodrome** | Base only: v2 liquidity deposits and withdrawals, gauge staking and emissions claims, veAERO locks, gauge votes, and voter fee, bribe, and rebase claims, alongside reads of the Sugar lens contracts that feed them |
More protocols are being added, so the live catalog is worth checking rather than this table: `https://mcp.ekubo.org/tools` returns the current tools, uncached, with the server version and catalog revision they came from.
The three bridge sources are compared rather than chosen for you: one cross-chain request returns whichever of them can serve it, each option already carrying the plan that executes it. A bridge is also the one plan whose successful origin transaction does not mean the funds have arrived, so a LayerZero or LI.FI transfer is tracked through to delivery on the destination chain before it is reported as finished.
For the non-Ekubo protocols the server prepares transactions but is deliberately not in the data path. Live market, vault, queue, and balance state is read by the agent from each protocol’s own public API or from your wallet’s RPC, and the wallet’s simulation of the exact calls is what decides whether an action succeeds.
## Why a new protocol needs no wallet update
[Section titled “Why a new protocol needs no wallet update”](#why-a-new-protocol-needs-no-wallet-update)
An execution plan is signer-neutral: an ordered list of unsigned calls, each with its chain, sender, target, value, and calldata, plus optional ABI material for decoding a revert. Nothing in it names a protocol. A plan that supplies Aave calldata and a plan that supplies Ekubo calldata are the same kind of document to the wallet, so a producer can start preparing a protocol the wallet has never heard of and the wallet executes it under the same rules.
The one thing a plan can require of the wallet is a named *capability*. The wallet rejects a plan listing any capability it does not implement, and today it implements exactly one — `atomic_batch`, the EIP-7702 batch. So protocol additions ship server-side and typically need no wallet release; only a plan that needed a new execution capability from the wallet itself would.
Your [signing policy](/wallet/policies/) is the other side of this. Policies match exact networks, targets, values, and calldata shapes, so a policy written for one protocol grants nothing over a newly supported one. A first action against a new protocol arrives as a native review, not as an automatic signature.
## Third-party producers
[Section titled “Third-party producers”](#third-party-producers)
Nothing about the wallet’s execution boundary is specific to Ekubo’s server. Any MCP server that returns plans in the published shape works, and third parties are free to build them. [Build a plan producer](/wallet/producers/) is the full contract, with worked examples; what follows is its outline.
A compatible producer returns a reference rather than the plan itself:
```json
{
"kind": "artifact_reference",
"artifact_type": "execution_plan",
"url": "https://…",
"integrity": {
"algorithm": "keccak256",
"value": "0x…"
},
"bytes": 1234
}
```
The agent relays that object verbatim to the wallet, which fetches the body itself, recomputes the keccak256 digest over the exact bytes, checks the declared byte count, and refuses a mismatch. What a producer has to satisfy:
* **The stored body is a schema version `1` execution plan.** It carries `chain_id` and a matching `eip155:` CAIP-2 identifier, one sender that every step’s `from` equals, and consecutively numbered `ordered_steps`. Unknown fields are rejected; producer-specific data belongs in the bounded `extensions` object, which the wallet ignores.
* **The URL is publicly fetchable HTTPS.** Admission is narrow: `https` on the default port to a public, resolvable host, with no credentials, fragments, redirects, or addresses in private or reserved ranges, and a hard size cap. A producer that has no public host can instead inline the body as a bounded `data:` URI, where the bytes are the reference.
* **Capabilities stay within what the wallet implements.** Omit `required_capabilities` unless the plan genuinely needs atomic batching.
* **A revert directive never says to retry identical calldata.** If a producer supplies a `simulation_failure_policy`, its `execution_reverted` and `simulation_setup_error` branches must direct the agent to re-prepare or to ask you, not to resend the plan that just failed.
Prepared on-chain reads travel the same way as `read_calls` artifacts whose body is an exact batch-call argument object, which lets a producer hand the wallet reads it wants performed against your RPC without proxying them.
The authoritative contract is published by the servers themselves rather than by this page: `https://mcp.ekubo.org/` returns the boundary and operational semantics as JSON, the hosted server’s `ekubo://docs/execution-plan` resource documents the handoff in full, and the local wallet’s own tool list describes the exact envelope its execution tools accept.
## What a producer never gets
[Section titled “What a producer never gets”](#what-a-producer-never-gets)
Adding a producer is not a grant of authority, and the wallet treats every one of them the same way regardless of who operates it:
* It fetches and integrity-checks the plan body itself instead of trusting a relayed copy or a friendly summary of it.
* It simulates the exact calls against your configured network before anything is signed.
* It evaluates your active policy against the exact network, target, value, and calldata, and routes anything not explicitly allowed to native [owner review](/wallet/approvals/).
* It never hands over key material, an owner-authentication capability, or the ability to approve a request or install a policy.
What a producer does see is the tool arguments an agent sends it, which can include your address and intended action, and it can propose any calls it likes. Simulation, policy, and native review are what contain that — not the producer’s good behavior. Add producers you are willing to have propose transactions to you, and read the exact values in the review rather than an agent’s description of them. [Security and privacy](/wallet/security/) covers the complete boundary.
# Security and privacy
> Understand Ekubo Wallet's custody, local-agent, storage, and network trust boundaries
Within the wallet process and its supported MCP interface, Ekubo Wallet keeps private keys and signing inside its core wallet authority. The native application retains owner-only capabilities, while agents receive a narrower interface that cannot approve requests, export keys, install policies, accept legal terms, or change security-sensitive settings. The current Windows and Linux credential backends have a separate same-user extraction weakness described below.
## Keys and local storage
[Section titled “Keys and local storage”](#keys-and-local-storage)
Wallet state is stored in an encrypted local database. Private keys and the database key are placed in the operating system’s credential service rather than an agent configuration file. Exporting a private key through Ekubo Wallet requires owner authentication and an explicit action in the native application.
Agent configuration written by the wallet contains fixed connection settings, not wallet credentials. The local bridge has no OAuth flow, bearer token, or client secret.
## Windows and Linux credential-store limitation
[Section titled “Windows and Linux credential-store limitation”](#windows-and-linux-credential-store-limitation)
Current Windows and Linux builds do not provide application-scoped key custody
Malware running as the same operating-system user can extract raw account private keys and the SQLCipher database key without using Ekubo Wallet or satisfying owner authentication. Once extracted, a private key can sign elsewhere and bypass wallet policy, native review, and wallet audit records.
The Windows build uses generic Windows Credential Manager entries. Microsoft documents that [generic credentials can be read and written by user processes](https://learn.microsoft.com/en-us/windows/win32/secauthn/kinds-of-credentials). The service and account names identify an entry; they do not restrict it to Ekubo Wallet.
The Linux build uses the Secret Service default collection. The [Secret Service specification](https://specifications.freedesktop.org/secret-service/latest/ch10.html) does not require application access controls, and GNOME states that [any application with the same user’s privileges can read secrets in an unlocked keyring](https://wiki.gnome.org/Projects%282f%29GnomeKeyring%282f%29SecurityFAQ.html). Other Secret Service implementations may differ, but Ekubo Wallet does not establish or verify an application-specific restriction on Linux.
A prompt-injected agent is relevant when its harness can run shell commands, programs, or arbitrary code as the desktop user. Such an agent can call the operating-system credential API directly. This is not a key export through MCP: the MCP server never returns raw key material, but its restrictions cannot govern an attacker that bypasses the wallet process. Closing the wallet does not delete the persistent credential entries.
SQLCipher still protects a copied database when the database key is unavailable, and the operating-system credential service protects against other users and offline disk access. Those controls do not protect a live Windows or Linux session from same-user malware. Until the custody mechanism changes, use valuable accounts on those platforms only if you trust every process and local agent allowed to run as the wallet user.
The current macOS build uses Keychain item access controls. Apple documents that [the creating application is automatically trusted and access is tracked using its code-signing requirement](https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/AboutCS/AboutCS.html). This is a stronger application boundary, but it does not protect a compromised wallet process, signing identity, authenticated owner session, or operating system.
## Native approval boundary
[Section titled “Native approval boundary”](#native-approval-boundary)
Reviews begin on **Reject**, expose the exact payload, require the complete document to be viewed, and use operating-system authentication for approval. The wallet rechecks the request and policy after authentication before signing. Read [Review requests](/wallet/approvals/) for the full flow.
## Owner-only settings
[Section titled “Owner-only settings”](#owner-only-settings)
Changes that can widen signing authority or replace trusted inputs require operating-system owner authentication. This includes widening or ambiguously changing a signing policy, adding or editing a network, enabling a disabled network, and adding or replacing trusted token metadata.
Three native owner actions are fail-safe reductions and do not open a fresh operating-system challenge: installing a policy that the wallet proves only tightens the active policy, disabling the exact network currently displayed, and removing the exact trusted-token row currently displayed. The wallet verifies current state again at its encrypted persistence boundary and commits the change atomically. Agents cannot invoke any of these owner-only settings operations.
## Transaction policy scope
[Section titled “Transaction policy scope”](#transaction-policy-scope)
Signing policy belongs to a wallet account rather than to a particular requester. Rules match exact calls and prepared transaction fields; they do not match the MCP client, harness, automation, dapp, WalletConnect session, or displayed plan source. The same allow rule therefore applies to an equivalent transaction requested by a local agent, an installed automation, or a connected WalletConnect dapp. Source labels remain useful review and audit context, but they grant and restrict nothing.
Personal-message and typed-data signatures always require native review. For transactions, a local agent may ask for one otherwise allowed submission to receive review; that can only add a prompt and cannot override a deny or approve anything.
## Local agent boundary
[Section titled “Local agent boundary”](#local-agent-boundary)
Supported harnesses start the installed MCP bridge over stdio. The bridge connects to same-user local IPC: a private Unix socket on macOS and Linux, or a current-user named pipe on Windows. The wallet verifies the local peer identity and gives the connection only the restricted agent API. Installing the connection entry does not itself grant an agent owner capabilities.
Restricted does not mean read-only. The local MCP server can read and persist the typed wallet state needed for proposals and transaction lifecycles, and it can ask the wallet’s core authority to use an operating-system-held key when the active policy allows an exact transaction automatically. It cannot obtain raw key material, export a key, decide a native review, authenticate as the owner, install policy, accept legal terms, or change owner-only settings.
It can also install an [automation](/wallet/automations/), which schedules bytecode the wallet polls and whose returned calls enter that same guarded path. An automation adds a source of proposed transactions and no signing or authorization path, so it cannot exceed the policy already installed; it is additionally bound to the policy revision it was installed against, and a later policy change stops it until the owner looks at it again.
This boundary protects against accidental and unauthorized local clients. It does not protect against malicious software already running as the same operating-system user. On Windows and Linux, that software can also use the [credential-store limitation](#windows-and-linux-credential-store-limitation) to retrieve keys directly, including after prompt injection into an agent harness with local code-execution capability. Keep the operating system and local agent software trusted and up to date.
## Notifications
[Section titled “Notifications”](#notifications)
Notifications use detailed previews by default, and are raised for transactions, message and typed-data signature requests, and WalletConnect pairing proposals. Their titles disclose lifecycle state, and their bodies name the local account and configured network, except for a pairing proposal, which names the dapp because no account or network has been chosen yet. They do not contain request identifiers, exact calldata, or approval and rejection actions. When private previews are in effect, the identifying details are replaced by an instruction to open Ekubo Wallet. A waiting request opens its exact review; a decided request or transaction lifecycle update opens Activity. The operating system controls whether either form appears on a lock screen or remains in notification history.
## External services
[Section titled “External services”](#external-services)
The wallet contacts the RPC endpoints configured for enabled networks to read state, simulate, and submit transactions. It fetches referenced execution plans and read bundles from the public HTTPS URL supplied by their producer. WalletConnect uses its relay while a session is active. Packaged installations contact the release service to check for updates and verify downloaded artifacts before installation.
For harnesses that support a remote MCP entry, agent setup also adds the credential-free public companion `https://mcp.ekubo.org/mcp`. Claude Desktop users add that hosted service as an account-level custom connector instead. The companion is operated by Ekubo, Inc. separately from local wallet custody. It can receive the tool arguments sent by an agent and temporarily store execution plans or other artifact bodies, which can identify a wallet address and intended action. It cannot read wallet keys, approve a request, install policy, or sign. The wallet independently fetches, integrity-checks, simulates, and policy-checks a referenced plan.
The application itself does not send private keys to these services. A transaction or signature can still reveal information by its nature, and RPC providers, dapps, WalletConnect infrastructure, and public blockchains have their own privacy properties.
The legal documents displayed inside the installed wallet are authoritative for the version you are using and remain available from **Settings**.
# Use WalletConnect
> Pair Ekubo Wallet with a dapp through WalletConnect and apply wallet policy to its requests
Ekubo Wallet accepts WalletConnect pairing URIs copied from a dapp’s connect-wallet dialog.
1. In the dapp, choose WalletConnect and copy its `wc:` pairing URI.
2. Open **WalletConnect** in Ekubo Wallet and paste the URI.
3. Review the dapp identity and requested networks, then choose the account to expose.
4. Approve or reject later requests that your signing policy does not decide automatically.
The wallet validates the pairing before connecting. An arriving proposal raises the wallet window and a notification naming the dapp, because a pairing is as much a decision as any other request. Multiple dapps can be connected at the same time, and each live session can be disconnected from the **WalletConnect** screen.
Account and chain requests, message signing, typed data, transactions, and supported batched-call requests use the same wallet authority as local requests. Personal messages and typed data always enter native review. Transactions are evaluated by the account’s signing policy: a matching allow rule can sign and submit without another prompt, a review rule or no match opens native review, and a deny rule rejects the request.
Policies are not scoped to a dapp or WalletConnect session. A rule written for an agent or automation also applies when a connected dapp requests the same matching transaction. A connected dapp cannot approve its own request, ask to weaken review, or change policy.
## Session lifetime
[Section titled “Session lifetime”](#session-lifetime)
WalletConnect pairings and session keys remain in memory. They do not persist or reconnect after Ekubo Wallet restarts. Explicitly quitting the wallet or installing an update disconnects every live session.
The pairing URI is a secret while it is usable. Copy it only from the dapp you intend to connect, and do not paste a pairing URI into a chat or agent prompt.