# Welcome to Aquarius

Aquarius is the liquidity layer of the Stellar network — an AMM protocol governed by AQUA holders.

Aquarius is the liquidity layer of the Stellar network: an AMM protocol where anyone can swap assets, provide liquidity, and earn rewards — and where AQUA holders vote to decide which markets those rewards flow to. It runs on its own [Soroban](/ecosystem-overview/stellar-essentials) smart-contract AMM, making liquidity provision on Stellar fully decentralized.

## What do you want to do?

* **Trade** — [swap](/user-guides/swap) any Stellar assets at the best available rate.
* **Earn** — [provide liquidity](/user-guides/pools) to earn trading fees, [AQUA rewards](/voting-and-rewards/aquarius-amm-rewards), and [pool incentives](/for-projects/pool-incentives).
* **Vote** — [lock AQUA into ICE](/aqua-and-ice/ice-tokens-locking-aqua-and-getting-benefits) to boost your rewards and [direct AQUA emissions](/user-guides/how-to-vote-for-markets-on-aquarius) to the markets you value.
* **Grow your project's liquidity** — attract liquidity providers with [bribes and Pool Incentives](/for-projects/for-projects).
* **Build** — integrate Aquarius swaps and pools into your wallet, app, or bot (see below).

New to the underlying tech? [Stellar Essentials](/ecosystem-overview/stellar-essentials) covers Soroban, trustlines, and other Stellar concepts these docs rely on.

## Build on Aquarius

These docs are written developers-first. If you are integrating Aquarius into a wallet, app, or trading bot, start here:

{% content-ref url="/pages/6UVZqTEb50O1s8JdyFrm" %}
[Quickstart](/developers/quickstart)
{% endcontent-ref %}

{% content-ref url="/pages/VIrjZN1Dgsuvk2UYwrq2" %}
[Integrating with Aquarius](/developers/integrating-with-aquarius)
{% endcontent-ref %}

* **Official SDKs:** [`aquarius-sdk`](https://pypi.org/project/aquarius-sdk/) for Python and [`@aquariusdefi/sdk`](https://www.npmjs.com/package/@aquariusdefi/sdk) for TypeScript wrap routing, execution, liquidity, and fees — the [Quickstart](/developers/quickstart) runs a first swap on them in about five minutes.
* **Smart contracts:** the [AMM router](/developers/reference/router-and-pool-contracts) is the entry point for swaps, deposits, and withdrawals; [concentrated liquidity pools](/developers/concentrated-liquidity) have a dedicated contract interface reference.
* **Backend API:** ready-made [path finding and pool data endpoints](/developers/code-examples/get-pools-info), fully documented at [amm-api.aqua.network/api/schema/redoc](https://amm-api.aqua.network/api/schema/redoc/).
* **Monetization:** integrators can [charge a fee on swaps](/developers/code-examples/add-fees-to-swap) executed through their integration.

## Community & support

* **Discord** — [discord.gg/sgzFscHp4C](https://discord.gg/sgzFscHp4C): the fastest way to get help, for users and integrators alike.
* **Telegram** — [t.me/aquarius\_official\_community](https://t.me/aquarius_official_community)
* **X (Twitter)** — [@AquariusDeFi](https://x.com/AquariusDeFi)
* **GitHub** — [github.com/AquariusDeFi](https://github.com/AquariusDeFi)
* **Email** — <hello@aqua.network>


# Quickstart

Execute your first Aquarius swap in about five minutes — on testnet, with the official SDK and nothing to configure.

This page gets you from zero to a completed on-chain swap with the official Aquarius SDK. It runs on **testnet**, creates and funds its own throwaway account, and has no placeholders to fill in — install, run, and watch a swap execute.

{% hint style="success" %}
Both scripts below are complete and runnable as-is.
{% endhint %}

{% hint style="warning" %}
Stellar testnet is wiped 2–4 times per year. Contract addresses persist across resets, but pools and balances are recreated — if the script fails right after a reset, the testnet stack may still be rebuilding. See [Testing on testnet](/developers/testing-on-testnet).
{% endhint %}

### 1. Install the SDK

{% tabs %}
{% tab title="Python" %}

```bash
pip install aquarius-sdk
```

{% endtab %}

{% tab title="JavaScript" %}

```bash
npm install @aquariusdefi/sdk @stellar/stellar-sdk
```

{% endtab %}
{% endtabs %}

### 2. Run your first swap

The script funds a fresh testnet account with friendbot, adds an AQUA trustline, quotes the best route from 10 XLM to AQUA, and executes the swap on-chain. Save it as `swap.py` or `swap.mjs` and run it:

{% tabs %}
{% tab title="Python" %}

```python
from stellar_sdk import Keypair
from aquarius import AquariusClient, Asset, XLM

AQUA = Asset.classic("AQUA", "GAHPYWLK6YRN7CVYZOO4H3VDRZ7PVF5UJGLZCSPAEIKJE2XSWF5LAGER")

aqua = AquariusClient(network="testnet", signer=Keypair.random())
aqua.fund_with_friendbot()
aqua.ensure_trustline(AQUA)

quote = aqua.quote(XLM, AQUA, amount_in=100_000_000)  # 10 XLM in stroops (1 XLM = 10^7 stroops)
print(f"Route found: expected output {quote.amount_out / 10**7} AQUA")

receipt = quote.execute()
print(f"Swap executed: received {receipt.amount_out / 10**7} AQUA")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
import { Keypair } from "@stellar/stellar-sdk";
import { AquariusClient, Asset, XLM } from "@aquariusdefi/sdk";

const AQUA = Asset.classic("AQUA", "GAHPYWLK6YRN7CVYZOO4H3VDRZ7PVF5UJGLZCSPAEIKJE2XSWF5LAGER");

const aqua = new AquariusClient({ network: "testnet", signer: Keypair.random() });
await aqua.fundWithFriendbot();
await aqua.ensureTrustline(AQUA);

const quote = await aqua.quote({ from: XLM, to: AQUA, amountIn: 100_000_000n }); // 10 XLM in stroops
console.log(`Route found: expected output ${Number(quote.amountOut) / 1e7} AQUA`);

const receipt = await quote.execute();
console.log(`Swap executed: received ${Number(receipt.amountOut) / 1e7} AQUA`);
```

{% endtab %}
{% endtabs %}

Expected output:

```
Route found: expected output 2247.836633 AQUA
Swap executed: received 2247.836633 AQUA
```

### 3. What the script did

1. **Friendbot** created and funded a fresh testnet account — nothing to configure or protect.
2. `ensure_trustline` let the account hold AQUA (required for any classic Stellar asset you want to receive).
3. `quote` asked the **Find Path API** for the best route across the pools — the same call returns exact-output quotes when you pass `amount_out` instead.
4. `execute` simulated, signed, and submitted the swap through the **AMM router**, guarded by the quote's slippage limit (default 1%), and read the exact amount received from the result.

That one flow — *route off-chain, execute on-chain* — is the core of every Aquarius integration.

### Next steps

* **Understand the pieces** — [Integrating with Aquarius](/developers/integrating-with-aquarius) explains the router, pools, and API architecture, and lists the [official SDKs](/developers/integrating-with-aquarius#official-sdks).
* **Go to mainnet** — construct the client with `network="mainnet"` and a funded account's secret key.
* **See the raw flow** — [Executing swaps through optimal path](/developers/code-examples/executing-swaps-through-optimal-path) documents what the SDK wraps: the Find Path API call and the router invocation, step by step, for any language.
* **Go deeper** — [Code examples](/developers/code-examples) covers liquidity provision, reading pool data, and charging integrator fees.
* If the script fails and the [testnet page](/developers/testing-on-testnet) doesn't explain it, ask in [Discord](https://discord.gg/sgzFscHp4C).


# Integrating with Aquarius

Architecture overview, contract addresses, and integration paths for building on Aquarius

This section is for developers integrating Aquarius into a wallet, app, or trading bot. All guides come with fully functional code examples in Python and JavaScript, built on the Stellar SDK.

{% hint style="success" %}
For the fastest start, the [Quickstart](/developers/quickstart) executes a complete swap on testnet in about five minutes — no configuration needed.
{% endhint %}

## Official SDKs

The SDKs wrap the full swap flow — routing, simulation, submission, retries, and typed errors:

| Language                | Package                                                                | Install                         |
| ----------------------- | ---------------------------------------------------------------------- | ------------------------------- |
| Python                  | [`aquarius-sdk`](https://pypi.org/project/aquarius-sdk/)               | `pip install aquarius-sdk`      |
| TypeScript / JavaScript | [`@aquariusdefi/sdk`](https://www.npmjs.com/package/@aquariusdefi/sdk) | `npm install @aquariusdefi/sdk` |

The SDKs cover the full integration surface: swaps (exact-input and exact-output), liquidity deposits and withdrawals, reward claims, concentrated positions, and integrator fee collectors. The [Quickstart](/developers/quickstart) and the [code examples](/developers/code-examples) use them throughout; the raw API and contract flow each SDK call wraps is documented in the [Reference](/developers/reference) section.

## How Aquarius fits together

An integration touches up to three layers:

1. **On-chain AMM — Soroban smart contracts.** The [AMM router](/developers/reference/router-and-pool-contracts) is the single entry point for the classic operations: swaps (including multi-hop `swap_chained`), deposits, withdrawals, reward claims, and pool discovery. The router deploys and indexes the underlying pool contracts, so you rarely need to talk to a pool directly. The one exception is [concentrated liquidity](/developers/concentrated-liquidity) — positions there are managed by calling the pool contract itself.
2. **Backend API — `amm-api.aqua.network`.** A public REST API providing path finding and indexed pool data, fully documented at [amm-api.aqua.network/api/schema/redoc](https://amm-api.aqua.network/api/schema/redoc/). It is a convenience layer: the find-path endpoints return a ready-to-execute XDR swap chain, but execution always happens on-chain, and you can bypass the API entirely by querying pools through the router.
3. **Your application** — builds transactions with the [Stellar SDK](https://developers.stellar.org/docs/tools/sdks), simulates them against Soroban RPC, then signs and submits.

A typical swap looks like this:

```mermaid
sequenceDiagram
    participant App as Your app
    participant API as Find Path API (v2)
    participant RPC as Soroban RPC
    participant Router as AMM router
    App->>API: POST /find-path with token in, token out, amount
    API-->>App: swap_chain_xdr, route, estimated amounts
    App->>RPC: simulate swap_chained(swaps_chain, in_amount, out_min)
    RPC-->>App: resource footprint and expected result
    App->>Router: sign and submit the transaction
    Note over Router: executes the hops across pools (up to 4)
    Router-->>App: amount received
```

## Pool types

| Pool type        | Price formula                             | Swap fee tiers   |
| ---------------- | ----------------------------------------- | ---------------- |
| **Volatile**     | Constant product (x·y=k)                  | 0.1% / 0.3% / 1% |
| **Stable**       | Stableswap (amplified, for pegged assets) | 0.01%–1%         |
| **Concentrated** | Tick-based ranges (Uniswap v3 style)      | 0.1% / 0.3% / 1% |

A share of every swap fee goes to liquidity providers and a share to the protocol; the split is configured on-chain and can be read from the router via `get_protocol_fee_fraction()`.

## Addresses & endpoints

All contract addresses, API base URLs, and RPC endpoints for both networks live in [Addresses & networks](/developers/reference/addresses-and-networks) — the single source of truth. The backend API endpoints and versioning are documented in the [Backend API reference](/developers/reference/backend-api); integrations should use API version `v2`. Everything works on [testnet](/developers/testing-on-testnet) too.

## Choose your integration path

* **Execute swaps** — the most common integration (wallets, bots). The [official SDKs](#official-sdks) cover it end to end; [Executing swaps through optimal path](/developers/code-examples/executing-swaps-through-optimal-path) documents the raw flow.
* **Provide liquidity programmatically** — [Deposit](/developers/code-examples/deposit-liquidity), [Withdraw](/developers/code-examples/withdraw-liquidity), and [Claim LP rewards](/developers/code-examples/claim-lp-rewards).
* **Monetize your integration** — [charge your own fee on swaps](/developers/code-examples/add-fees-to-swap) executed through your app.
* **Concentrated liquidity positions** — the dedicated [contract interface reference](/developers/concentrated-liquidity/contract-interface-reference) and [code examples](/developers/concentrated-liquidity/managing-positions-code-examples).
* **Read market data** — [Get pools info](/developers/code-examples/get-pools-info) via smart contracts or the backend API ([full endpoint reference](https://amm-api.aqua.network/api/schema/redoc/)).

## Errors & slippage

Things every integration should handle:

* **No path found.** The find-path endpoints return HTTP 200 with `success: false` and zeroed fields when no route exists — always check the `success` flag.
* **Slippage protection.** Swaps take an `out_min` (exact-input) or `max_in` (exact-output) guard. If the pool can't satisfy it, the contract call fails with error `2006 OutMinNotSatisfied` or `2020 InMaxNotSatisfied`. For multi-hop swaps the guard is enforced end-to-end, not per hop. The code examples use 1% slippage — the same default as the Aquarius app (which offers 0.1% / 0.5% / 1%). Use tighter values for stable pairs, wider for illiquid markets.
* **Token ordering.** Token vectors passed to the router must be sorted by contract address, or the call fails with `2002 TokensNotSorted`. The `order_token_ids` helper in [Prerequisites & basics](/developers/code-examples/prerequisites-and-basics) handles this.
* **Trustlines.** The receiving account must hold a [trustline](/ecosystem-overview/stellar-essentials) for any classic Stellar asset it receives.

The full table of contract error codes — including pool-pause states and concentrated-pool specifics — is in the [Error codes reference](/developers/reference/error-codes).

{% hint style="info" %}
**Terminology:** a pool is identified inside the router by its **pool hash** (a 32-byte value also called *pool index* in function signatures, derived from the pool type and fee tier). The pool's deployed **contract address** is a separate value — `get_pools(tokens)` returns the mapping between the two.
{% endhint %}

## Support

Questions or stuck on an integration? Reach the team and other builders on [Discord](https://discord.gg/sgzFscHp4C), or explore the source on [GitHub](https://github.com/AquariusDeFi).


# Code examples

Python and JavaScript code samples for interacting with Aquarius

All code examples are fully functional and provided in both Python and JavaScript, ready for you to copy and paste into your local environment.

{% hint style="success" %}
New to Aquarius? The [Quickstart](/developers/quickstart) runs a complete swap on testnet in five minutes before you dive into the details here.
{% endhint %}

## Suggested reading order

1. [Deposit liquidity](/developers/code-examples/deposit-liquidity) → [Withdraw liquidity](/developers/code-examples/withdraw-liquidity) → [Claim LP rewards](/developers/code-examples/claim-lp-rewards) — the liquidity-provider lifecycle, on the [official SDKs](/developers/integrating-with-aquarius#official-sdks).
2. [Get pools info](/developers/code-examples/get-pools-info) — pools, reserves, and positions, no signer required.
3. [Executing swaps through optimal path](/developers/code-examples/executing-swaps-through-optimal-path) — the raw swap flow via the Find Path API and the router, step by step, for any language. [Prerequisites & basics](/developers/code-examples/prerequisites-and-basics) collects the constants and helpers it relies on.
4. [Executing swaps through specific pool](/developers/code-examples/executing-swaps-through-specific-pool) — swapping against one known pool, without the path-finding API.
5. [Add fees to swap](/developers/code-examples/add-fees-to-swap) — monetizing your integration with provider fees.

Looking for concentrated liquidity? Position management examples live in the dedicated [Concentrated liquidity](/developers/concentrated-liquidity) section.

{% hint style="info" %}
If you don't have a local execution environment, platforms like Replit work for testing — use a throwaway key holding minimal funds, never a real one.
{% endhint %}


# Prerequisites & basics

Dependencies, commonly used URLs, contract addresses and useful methods

{% hint style="info" %}
The [official SDKs](/developers/integrating-with-aquarius#official-sdks) bundle the constants and helpers on this page — integrations in Python or JavaScript can start from the [Quickstart](/developers/quickstart) instead. This page serves integrations that call the API and contracts directly.
{% endhint %}

### Prerequisites

Before executing scripts, ensure you have Stellar SDK installed:

{% tabs %}
{% tab title="Python" %}

* **Python 3.10+**: The script is written in Python and requires Python version 3.10 or higher (required by current stellar-sdk releases).
* **Stellar SDK**: Install via pip:

```
pip install stellar-sdk
```

{% endtab %}

{% tab title="JavaScript" %}

* **Node 18+**: The script is written in JavaScript and requires Node version 18 or higher.
* **Stellar SDK** (v15 or newer — the same major version the Aquarius app runs on): Install via npm or yarn.

```bash
npm install @stellar/stellar-sdk
```

or

```bash
yarn add @stellar/stellar-sdk
```

{% endtab %}
{% endtabs %}

### Constants

Below are some API endpoints and contract addresses that will be used in other code examples.

#### For mainnet

{% tabs %}
{% tab title="Python" %}

```python
# The contract ID of the Aquarius AMM contract
router_contract_id = "CBQDHNBFBZYE4MKPWBSJOPIYLW4SFSXAXUTSXJN76GNKYVYPCKWC6QUK"
# Soroban RPC server address
soroban_rpc_server = "https://mainnet.sorobanrpc.com"
# Horizon server address
horizon_server = "https://horizon.stellar.org"
# Aquarius backend API URL
base_api = "https://amm-api.aqua.network/api/external/v2"
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
// The contract ID of the Aquarius AMM contract
const routerContractId = "CBQDHNBFBZYE4MKPWBSJOPIYLW4SFSXAXUTSXJN76GNKYVYPCKWC6QUK";
// Soroban RPC server address
const sorobanRpcServer = "https://mainnet.sorobanrpc.com";
// Horizon server address
const horizonServer = "https://horizon.stellar.org";
// Aquarius backend API URL
const baseApi = "https://amm-api.aqua.network/api/external/v2";
```

{% endtab %}
{% endtabs %}

#### For testnet

{% tabs %}
{% tab title="Python" %}

```python
# The contract ID of the Aquarius AMM contract
# Address updated on February 2026 and should be valid across testnet resets.
router_contract_id = "CBCFTQSPDBAIZ6R6PJQKSQWKNKWH2QIV3I4J72SHWBIK3ADRRAM5A6GD"
# Soroban RPC server address
soroban_rpc_server = "https://soroban-testnet.stellar.org:443"
# Horizon server address
horizon_server = "https://horizon-testnet.stellar.org"
# Aquarius backend API URL
base_api = "https://amm-api-testnet.aqua.network/api/external/v2"
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
// The contract ID of the Aquarius AMM contract
// Address updated on February 2026 and should be valid across testnet resets.
const routerContractId = "CBCFTQSPDBAIZ6R6PJQKSQWKNKWH2QIV3I4J72SHWBIK3ADRRAM5A6GD";
// Soroban RPC server address
const sorobanRpcServer = "https://soroban-testnet.stellar.org:443";
// Horizon server address
const horizonServer = "https://horizon-testnet.stellar.org";
// Aquarius backend API URL
const baseApi = "https://amm-api-testnet.aqua.network/api/external/v2";
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
**API versions:** use `v2`. Version `v1` remains available and is identical except for one endpoint — `v2`'s `find-path-strict-receive` correctly accounts for [provider fees](/developers/code-examples/add-fees-to-swap) in the required input amount. No API key is currently required.
{% endhint %}

### Transaction fees

The fee passed to `TransactionBuilder` is your **inclusion-fee bid** in stroops — the resource fees for Soroban execution are calculated during simulation and added automatically by `prepare_transaction` / `prepareTransaction`. Any bid at or above the network minimum (100 stroops) is accepted; the examples use higher values, which helps transactions get included during network surge pricing without changing what you pay in quiet conditions.

### Helper functions

Common utility methods that will be used in other code examples.

#### Get asset contract ID

To interact with any Soroban assets, you need their **smart contract address**.

This code snippet demonstrates how to retrieve the contract address of an asset on the Stellar network. The code uses the PUBLIC network by default, but you can switch to the desired network (for example, TESTNET).

{% tabs %}
{% tab title="Python" %}

```python
from stellar_sdk import Asset, Network

# Create the native asset
asset = Asset.native()
# Or create a custom asset
# asset = Asset("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA")

# Retrieve the contract ID for the PUBLIC network
contract_id = asset.contract_id(Network.PUBLIC_NETWORK_PASSPHRASE)

print(contract_id)
# Example output:
# "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA"

```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const StellarSdk = require('@stellar/stellar-sdk');

// Create the native asset
const asset = StellarSdk.Asset.native();
// Or create a custom asset
// const asset = new StellarSdk.Asset('AQUA', 'GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA');

// Retrieve the contract ID for the PUBLIC network
const contractId = asset.contractId(StellarSdk.Networks.PUBLIC);

console.log(contractId);
// console.log example
// "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA"
```

{% endtab %}
{% endtabs %}

#### Get pool contract ID and pool hash

To interact with an Aquarius pool (for example, make deposits or direct swaps), you need the **address of the pool contract**.

One option to find the pool address is to refer to the [pool page](https://aqua.network/pools/CCY2PXGMKNQHO7WNYXEWX76L2C5BH3JUW3RCATGUYKY7QQTRILBZIFWV/) on Aquarius website, please see below:

<figure><img src="/files/p1sKWAfIDHPM7gCnngaL" alt=""><figcaption><p>Example of pool contract address, click to copy.</p></figcaption></figure>

To retrieve this information programmatically, see [Get pools info](/developers/code-examples/get-pools-info).

#### Order tokens IDs

Most of the time, if an array of token IDs needs to be passed as arguments in a contract call, they should be sorted.

{% tabs %}
{% tab title="Python" %}

```python
def order_token_ids(tokens: List[xdr.SCVal]) -> List[xdr.SCVal]:
   """
   Orders token IDs based on their contract ID to maintain consistency.

   Args:
       tokens (List[xdr.SCVal]): List of token addresses as SCVal objects.

   Returns:
       List[xdr.SCVal]: Ordered list of token SCVal objects.
   """
   return sorted(tokens, key=lambda token: int(token.address.contract_id.hash.hex(), 16))
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
function orderTokensIds(tokensIds) {
    /**
     * Orders token IDs based on their contract ID to maintain consistency.
     *
     * @param {Array} tokensIds - List of token addresses as SCVal objects.
     * @returns {Array} Ordered list of token SCVal objects.
     */
    return tokensIds.sort((a, b) => {
        const aHash = BigInt('0x' + a.address().contractId().toString('hex'));
        const bHash = BigInt('0x' + b.address().contractId().toString('hex'));

        // Compare BigInts directly without converting to number
        if (aHash < bHash) return -1;
        if (aHash > bHash) return 1;
        return 0;
    });
}
```

{% endtab %}
{% endtabs %}

#### Data conversion utilities

Here are methods and code snippets for converting data between basic types and ScVal (Smart Contract Value).

{% tabs %}
{% tab title="Python" %}

```python
from stellar_sdk import Address, scval

# ================
# Basic => ScVal
# ================

# Contract Id To ScVal
contract_id = "C..."
contract_id_scval = scval.to_address(contract_id)

# Array To ScVal
scval.to_vec([contract_id_scval, contract_id_scval])

# Public Key To ScVal
public_key = "G..."
public_key_scval = scval.to_address(public_key)

# Number To Uint32 ScVal
number_u32_scval = scval.to_uint32(1_000_000)


# Number To Uint128 ScVal
number_u128_scval = scval.to_uint128(1_000_000)

# Hash To ScVal 
pool_hash = "a1b2...."
pool_hash_scval = scval.to_bytes(bytes.fromhex(pool_hash))

# ================
#  ScVal => Basic
# ================

def u128_to_int(value: UInt128Parts) -> int:
   """
   Converts UInt128Parts from Stellar's XDR to a Python integer.

   Args:
       value (UInt128Parts): UInt128Parts object from Stellar SDK.

   Returns:
       int: Corresponding Python integer.
   """
   return int(value.hi.uint64 << 64) + value.lo.uint64
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const StellarSdk = require('@stellar/stellar-sdk');
const { Address, StrKey, XdrLargeInt, xdr } = StellarSdk;

// ================
// Basic => ScVal
// ================

function contractIdToScVal(contractId) {
    /**
     * Converts a contract ID to a Stellar SCVal (Smart Contract Value) format.
     *
     * @param {string} contractId - The contract ID to convert.
     * @returns {StellarSdk.xdr.ScVal} - The SCVal representation of the contract ID.
     *
     * @throws {Error} Throws an error if the contract ID is invalid or cannot be decoded.
     */
    return Address.contract(StrKey.decodeContract(contractId)).toScVal();
}

function arrayToScVal(array) {
    /**
     * Converts an array of ScVal objects into a single ScVal vector.
     *
     * @param {StellarSdk.xdr.ScVal[]} array - An array of ScVal objects to be converted into a vector.
     * @returns {StellarSdk.xdr.ScVal} A ScVal object representing the vector.
    */
    return xdr.ScVal.scvVec(array);
}


function publicKeyToScVal(pubkey) {
    /*
     * Converts a Stellar public key string into an ScVal address.
     *
     * @param {string} pubkey - The Stellar public key to be converted.
     * @returns {StellarSdk.xdr.ScVal} An ScVal object representing the address derived from the public key.
    */
    return xdr.ScVal.scvAddress(Address.fromString(pubkey).toScAddress());
}

function numberToUint32(number) {
    /**
     * Converts a number to an ScVal representing a 32-bit unsigned integer (Uint32).
     *
     * @param {number} number - The number to be converted. Must be a non-negative integer within the range of Uint32 (0 to 2^32 - 1).
     * @returns {StellarSdk.xdr.ScVal} An ScVal object representing the 32-bit unsigned integer.
    */
    return xdr.ScVal.scvU32(number);
}

function numberToUint128(number) {
    /**
     * Converts a number to an ScVal representing a 128-bit unsigned integer (Uint128).
     *
     * @param {number} number - The number to be converted. Must be a non-negative value that fits within the range of Uint128.
     * @returns {StellarSdk.xdr.ScVal} An ScVal object representing the 128-bit unsigned integer.
    */
    return new XdrLargeInt(
        'u128',
         number.toFixed(),
    ).toU128();
}

function bufferToScVal(buffer) {
    /**
     * Converts a Buffer object into an ScVal representing a byte array.
     *
     * @param {Buffer} buffer - The Buffer object containing the byte data to be converted.
     * @returns {StellarSdk.xdr.ScVal} An ScVal object representing the byte array.
    */
    return xdr.ScVal.scvBytes(buffer);
}


function hashToScVal(hash) {
    /**
     * Converts a hexadecimal hash string into an ScVal representing a byte array.
     *
     * @param {string} hash - The hash string in hexadecimal format to be converted.
     * @returns {StellarSdk.xdr.ScVal} An ScVal object representing the byte array derived from the hash.
    */
    return xdr.ScVal.scvBytes(Buffer.from(hash, 'hex'));
}

// ================
// SvVal => Basic
// ================

function u128ToInt(value) {
    /**
     * Converts UInt128Parts from Stellar's XDR to a JavaScript number.
     *
     * @param {Object} value - UInt128Parts object from Stellar SDK, with `hi` and `lo` properties.
     * @returns {number|null} Corresponding JavaScript number, or null if the number is too large.
     */
    const result = (BigInt(value.hi()._value) << 64n) + BigInt(value.lo()._value);

    // Check if the result is within the safe integer range for JavaScript numbers
    if (result <= BigInt(Number.MAX_SAFE_INTEGER)) {
        return Number(result);
    } else {
        console.warn("Value exceeds JavaScript's safe integer range");
        return null;
    }
}
```

{% endtab %}
{% endtabs %}


# Executing swaps through optimal path

Executing swaps is the most common use of Aquarius protocol. This article explains how to prepare, find the best path and execute swap.

{% hint style="info" %}
This page documents the raw flow — the Find Path API call and the router invocation — as a reference for any language. In Python or JavaScript, the [official SDK](/developers/integrating-with-aquarius#official-sdks) wraps this entire page: see the [Quickstart](/developers/quickstart).
{% endhint %}

This is the recommended way to execute swaps with Aquarius, as it provides the best swap results. It supports swaps through multiple pools (multi-hop, up to 4 pools).

In this example we will swap XLM to AQUA with Aquarius AMM. The swap will be executed using router and will be optimized for best results. The example will also demonstrate the ability to specify either the amount of the asset being sent or the amount of the asset to be received, similar to *strict-send* and *strict-receive* behavior.

{% hint style="warning" %}
Make sure the account has an established trustline for the asset to receive — otherwise the swap will fail until the trustline is created. See the [Stellar documentation on trustlines](https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/accounts#trustlines).
{% endhint %}

See the [complete code examples](#complete-code-examples) at the bottom of this page.

### Executing swap: step by step guide

To perform a swap, you need to follow these steps:

1\. **Specify user secret key, input token, output token, whether it’s a send or receive operation, and the token amount:**\
You need to specify the input and output tokens and **a single amount** (in stroops) — either the amount of the token to **send** or to **receive**, depending on the selected mode.

{% tabs %}
{% tab title="Python" %}

<pre class="language-python"><code class="lang-python"># This account must have at least 3 XLM and a trustline to AQUA.
user_secret_key = "S..."
# XLM
token_in = Asset.native()
# AQUA
token_out = Asset("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA")
<strong># If True, the swap behaves like strict-send: the amount of the sending asset is fixed.
</strong># If False, the swap behaves like strict-receive: the amount of the receiving asset is fixed.
<strong>is_send=True
</strong><strong># Amount of 1 XLM or 1 AQUA in stroops (depending on is_send)
</strong>amount = 1_0000000
</code></pre>

{% endtab %}

{% tab title="JavaScript" %}

<pre class="language-javascript"><code class="lang-javascript">const userSecretKey = 'S...';
// XLM
const tokenIn = Asset.native();
// AQUA
const tokenOut = new Asset('AQUA','GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA'); 
// if true, the swap behaves like strict-send: the amount of the sending asset is fixed.
// if false, the swap behaves like strict-receive: the amount of the receiving asset is fixed.
<strong>const isSend = true;
</strong><strong>// amount of 1 XLM or 1 AQUA in stroops (depending on isSend)
</strong>const amount = 10000000;
</code></pre>

{% endtab %}
{% endtabs %}

2\. **Call the Find Path API to calculate the swap route and generate the XDR:**\
Send a `POST` request to the appropriate endpoint — either `/api/external/v2/find-path/` (for strict-send) or `/api/external/v2/find-path-strict-receive/` (for strict-receive).

The request body must be a JSON object containing the following fields:

* `token_in_address`: The address of the token you want to swap **from**.
* `token_out_address`: The address of the token you want to swap **to**.
* `amount`: The amount of the token to be swapped, in stroops. This represents either the input amount (for send mode) or the output amount (for receive mode), depending on the endpoint used.

{% tabs %}
{% tab title="Python" %}

```python
def find_swap_path(base_api: str, token_in_address: str, token_out_address: str, amount: int, is_send: bool) -> (int, str):
    data = {
        'token_in_address': token_in_address,
        'token_out_address': token_out_address,
        'amount': amount
    }
    endpoint = '/find-path/' if is_send else '/find-path-strict-receive/'
    response = requests.post(f'{base_api}{endpoint}', json=data)
    swap_result = response.json()
    assert swap_result['success']
    return int(swap_result['amount']), swap_result['swap_chain_xdr']
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
async function findSwapPath() {
    const headers = { 'Content-Type': 'application/json' };
    const body = JSON.stringify({
        token_in_address: tokenIn.contractId(Networks.PUBLIC),
        token_out_address: tokenOut.contractId(Networks.PUBLIC),
        amount: amount.toString(),
    });

    const endpoint = isSend ? '/find-path/' : '/find-path-strict-receive/';
    const estimateResponse = await fetch(`${baseApi}${endpoint}`, { method: 'POST', body, headers });
    const estimateResult = await estimateResponse.json();

    console.log(estimateResult);
    // {
    //   success: true,
    //   swap_chain_xdr: 'AAAAEAAAAAE...SEu1QKQU3Ycwk9FM5LjU5ggGwgl5w==',
    //   pools: [ 'CDE57N6XTUPBKYYDGQMXX7E7SLNOLFY3JEQB4MULSMR2AKTSAENGX2HC' ],
    //   tokens: [
    //     'native',
    //     'AQUA:GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA'
    //   ],
    //   amount: 1724745895
    // }

    if (!estimateResult.success) {
        throw new Error('Estimate failed');
    }

    return estimateResult;
}
```

{% endtab %}
{% endtabs %}

\
3\. **Execute the AMM Router smart contract call using the swap XDR:**\
Use the XDR generated by the Find Path API to perform a smart contract invocation on the AMM Router.\
Depending on the swap type, call either the `swap_chained` (for strict-send) or `swap_chained_strict_receive` (for strict-receive) method.\
This XDR contains all the necessary routing and pricing logic for the optimal swap execution.

{% tabs %}
{% tab title="Python" %}

```python
def execute_swap(
        network: str,
        soroban_rpc_server: SorobanServer,
        horizon_server: Server,
        keypair: Keypair,
        router_contract_id: str,
        token_in_address: str,
        amount: int,
        amount_with_slippage: int,
        swap_path: str,
        is_send: bool,
) -> int:
    function_name = 'swap_chained' if is_send else 'swap_chained_strict_receive'
    tx = soroban_rpc_server.prepare_transaction(
        TransactionBuilder(
            horizon_server.load_account(keypair.public_key),
            network_passphrase=network,
            base_fee=10000
        ).set_timeout(300)
        .append_invoke_contract_function_op(
            contract_id=router_contract_id,
            function_name=function_name,
            parameters=[
                scval.to_address(keypair.public_key),
                SCVal.from_xdr(swap_path),
                scval.to_address(token_in_address),
                scval.to_uint128(amount),
                scval.to_uint128(amount_with_slippage),
            ],
        )
        .build()
    )
    tx.sign(keypair)

    submit_response = horizon_server.submit_transaction(tx)

    assert submit_response['successful']

    result_meta = soroban_rpc_server.get_transaction(submit_response['id']).result_meta_xdr
    transaction_meta = TransactionMeta.from_xdr(result_meta)
    result = (transaction_meta.v4 if transaction_meta.v == 4 else transaction_meta.v3).soroban_meta.return_value

    return u128_to_int(result.u128)
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
async function executeSwap(estimateResult) {
    const keypair = Keypair.fromSecret(userSecretKey);

    const sorobanServer = new rpc.Server(sorobanServerUrl);
    const horizonServer = new Horizon.Server(horizonServerUrl);

    // No need to generate swapsChain manually, use value received from find-path api
    const swapsChain = xdr.ScVal.fromXDR(estimateResult.swap_chain_xdr, 'base64');
    const tokenInScVal = Address.contract(StrKey.decodeContract(tokenIn.contractId(Networks.PUBLIC))).toScVal()
    const amountU128 = new XdrLargeInt('u128', amount.toFixed()).toU128();
    const amountWithSlippage = isSend ? estimateResult.amount * 0.99 : estimateResult.amount * 1.01; // slippage 1%
    const amountWithSlippageU128 = new XdrLargeInt('u128', amountWithSlippage.toFixed()).toU128();

    const account = await sorobanServer.getAccount(keypair.publicKey());
    const functionName = isSend ? 'swap_chained' : 'swap_chained_strict_receive';
    const tx = new TransactionBuilder(account, {
        fee: BASE_FEE,
        networkPassphrase: Networks.PUBLIC,
    })
        .addOperation(
            new StellarSdk.Contract(routerContractId).call(
                functionName,
                xdr.ScVal.scvAddress(Address.fromString(keypair.publicKey()).toScAddress()),
                swapsChain,
                tokenInScVal,
                amountU128,
                amountWithSlippageU128
            )
        )
        .setTimeout(TimeoutInfinite)
        .build();

    const preparedTx = await sorobanServer.prepareTransaction(tx);

    preparedTx.sign(keypair);

    const result = await horizonServer.submitTransaction(preparedTx);

    const meta = (await sorobanServer.getTransaction(result.id)).resultMetaXdr

    const returnValue = meta.value().sorobanMeta().returnValue();

    const swapResult = u128ToInt(returnValue.value());

    console.log('Swap successful!');
    console.log(`Swapped: ${amount / 1e7} ${tokenIn.code} => ${swapResult / 1e7} ${tokenOut.code}`);
}
```

{% endtab %}
{% endtabs %}

### Complete code examples

This code performs a swap using Aquarius AMM on the Stellar mainnet. Depending on the `is_send` parameter: if `is_send` is `True`, it swaps 1 XLM for AQUA; if `is_send` is `False`, it swaps XLM for 1 AQUA.

To successfully execute the code, provide the secret key of a Stellar account with at least 3 XLM and an established trustline for AQUA.

<details>

<summary>Copy the full code Python</summary>

<pre class="language-python"><code class="lang-python">import requests
from stellar_sdk import scval, SorobanServer, TransactionBuilder, Network, Server, Keypair, Asset
from stellar_sdk.xdr import SCVal, TransactionMeta, UInt128Parts

# =========================================
# Configuration &#x26; Setup
# =========================================

# This account must have at least 3 XLM and a trustline to AQUA.
user_secret_key = "S....."
keypair = Keypair.from_secret(user_secret_key)

# Input and output tokens
token_in = Asset.native()  # XLM
token_out = Asset("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA")

<strong># If True, the swap behaves like strict-send: the amount of the sending asset is fixed.
</strong># If False, the swap behaves like strict-receive: the amount of the receiving asset is fixed.
is_send=True
# Amount of 1 XLM or 1 AQUA in stroops (depending on is_send)
amount = 1_0000000

# Router contract for AMM swaps on mainnet
router_contract_id = "CBQDHNBFBZYE4MKPWBSJOPIYLW4SFSXAXUTSXJN76GNKYVYPCKWC6QUK"

# Soroban and Horizon servers
soroban_server = SorobanServer("https://mainnet.sorobanrpc.com")
horizon_server = Server("https://horizon.stellar.org/")
network = Network.PUBLIC_NETWORK_PASSPHRASE

# AQUA AMM API endpoint
base_api = 'https://amm-api.aqua.network/api/external/v2'


# =========================================
# Utility Function
# =========================================

def u128_to_int(value: UInt128Parts) -> int:
    """Convert Uint128Parts to Python int."""
    return (value.hi.uint64 &#x3C;&#x3C; 64) + value.lo.uint64


# =========================================
# Functions
# =========================================

def find_swap_path(base_api: str, token_in_address: str, token_out_address: str, amount: int, is_send: bool) -> (int, str):
    """
    Call the Find Path API to retrieve the swap chain and estimated amount.
    """
    print("Requesting swap path from AMM API...")
    data = {
        'token_in_address': token_in_address,
        'token_out_address': token_out_address,
        'amount': amount
    }
    endpoint = '/find-path/' if is_send else '/find-path-strict-receive/'
    response = requests.post(f'{base_api}{endpoint}', json=data)
    swap_result = response.json()
    print(swap_result)
    """
        {
          'success': True,
          'swap_chain_xdr': 'AAAAEAAAAAEAAAABAAAAEAAAAAEAAAADAAAAEAAAAAEAAAACAAAAEgAAAAEltPzYWa7C+mNIQ4xImzw8EMmLbSG+T9PLMMtolT75dwAAABIAAAABKIUvaMGYSI40b7EhLtUCkFN2HMJPRTOS41OYIBsIJecAAAANAAAAILLgL8/KbJb4rVy9hOd4Snd7NtnJaiRZQCxPRYRiqrfwAAAAEgAAAAEohS9owZhIjjRvsSEu1QKQU3Ycwk9FM5LjU5ggGwgl5w==',
          'pools': [
            'CDE57N6XTUPBKYYDGQMXX7E7SLNOLFY3JEQB4MULSMR2AKTSAENGX2HC'
          ],
          'tokens': [
            'native',
            'AQUA:GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA'
          ],
          'amount': 3627808902
        }
    """

    if not swap_result.get('success', False):
        raise Exception("Failed to retrieve swap path from the API.")

    print("Swap path retrieved. Estimated amount:", swap_result['amount'])
    return int(swap_result['amount']), swap_result['swap_chain_xdr']


def execute_swap(
    network: str,
    soroban_rpc_server: SorobanServer,
    horizon_server: Server,
    keypair: Keypair,
    router_contract_id: str,
    token_in_address: str,
    amount: int,
    amount_with_slippage: int,
    swap_path: str,
    is_send: bool,
) -> int:
    """
    Executes the chained swap transaction on Soroban and returns the final amount out.
    """
    print("Preparing and building swap transaction...")
    source_account = horizon_server.load_account(keypair.public_key)
    
    function_name = 'swap_chained' if is_send else 'swap_chained_strict_receive'

    # Build the transaction to invoke `swap_chained`
    tx = (
        TransactionBuilder(
            source_account=source_account,
            network_passphrase=network,
            base_fee=10000
        )
        .set_timeout(300)
        .append_invoke_contract_function_op(
            contract_id=router_contract_id,
            function_name=function_name,
            parameters=[
                scval.to_address(keypair.public_key),
                SCVal.from_xdr(swap_path),
                scval.to_address(token_in_address),
                scval.to_uint128(amount),
                scval.to_uint128(amount_with_slippage),
            ],
        )
        .build()
    )

    # Prepare transaction to get Soroban-specific data (footprint, etc.)
    print("Preparing transaction on Soroban...")
    prepared_tx = soroban_rpc_server.prepare_transaction(tx)

    # Sign the prepared transaction
    print("Signing transaction...")
    prepared_tx.sign(keypair)

    # Submit the transaction to Horizon
    print("Submitting transaction to Horizon...")
    submit_response = horizon_server.submit_transaction(prepared_tx)

    if not submit_response.get('successful', False):
        raise Exception("Transaction failed: " + str(submit_response))

    print("Transaction submitted successfully. Fetching result...")

    # Get the transaction result from Soroban server to access Soroban metadata
    tx_info = soroban_rpc_server.get_transaction(submit_response['id'])
    if not tx_info or not tx_info.result_meta_xdr:
        raise Exception("No transaction metadata found.")

    # Extract the result from the Soroban metadata
    transaction_meta = TransactionMeta.from_xdr(tx_info.result_meta_xdr)
    return_val = (transaction_meta.v4 if transaction_meta.v == 4 else transaction_meta.v3).soroban_meta.return_value
    final_amount = u128_to_int(return_val.u128)
    print("Swap executed successfully.")
    return final_amount


# =========================================
# Entry Point
# =========================================

print("Starting swap process...")
print(f"Swapping {token_in.code} for {token_out.code}...")

# 1. Find the swap path and estimated output
amount_estimated, swap_path_xdr = find_swap_path(
    base_api,
    token_in.contract_id(network),
    token_out.contract_id(network),
    amount,
    is_send,
)

# Apply 1% slippage tolerance
amount_with_slippage = int(amount_estimated * 0.99) if is_send else int(amount_estimated * 1.01)
print(f"Applying 1% slippage. Expected amount: {amount_with_slippage / 1e7}")

# 2. Execute the swap
amount = execute_swap(
    network,
    soroban_server,
    horizon_server,
    keypair,
    router_contract_id,
    token_in.contract_id(network),
    amount,
    amount_with_slippage,
    swap_path_xdr,
    is_send,
)

print("Swap completed successfully!")

if is_send:
    print(f"Amount out: {amount / 10 ** 7} {token_out.code}")  # If it's strict-send, show output amount
else:
    print(f"Amount in: {amount / 10 ** 7} {token_in.code}")  # If it's strict-receive, show input amount

</code></pre>

</details>

<details>

<summary>Copy the full code JavaScript</summary>

```javascript
const StellarSdk = require('@stellar/stellar-sdk');
const {
    xdr,
    Address,
    Asset,
    StrKey,
    XdrLargeInt,
    Networks,
    TransactionBuilder,
    rpc,
    BASE_FEE,
    TimeoutInfinite,
    Keypair,
    Horizon,
} = StellarSdk;

// =========================================
// Configuration & Setup
// =========================================

// Enter the secret key of the account executing the swap.
// The account must have at least 3 XLM and a trustline to AQUA.
const userSecretKey = 'S.....';

// Input and output tokens
const tokenIn = Asset.native();
const tokenOut = new Asset(
    'AQUA',
    'GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA'
);

// if true, the swap behaves like strict-send: the amount of the sending asset is fixed.
// if false, the swap behaves like strict-receive: the amount of the receiving asset is fixed.
const isSend = true;
// amount of 1 XLM or 1 AQUA in stroops (depending on isSend)
const amount = 10_000_000;

// Soroban and Horizon server endpoints
const horizonServerUrl = "https://horizon.stellar.org";
const sorobanServerUrl = 'https://mainnet.sorobanrpc.com';

// AQUA AMM API endpoint
const baseApi = 'https://amm-api.aqua.network/api/external/v2';

// Router contract ID
const routerContractId = "CBQDHNBFBZYE4MKPWBSJOPIYLW4SFSXAXUTSXJN76GNKYVYPCKWC6QUK";

// =========================================
// Utility Function
// =========================================
function u128ToInt(value) {
    const result = (BigInt(value.hi()._value) << 64n) + BigInt(value.lo()._value);
    if (result <= BigInt(Number.MAX_SAFE_INTEGER)) {
        return Number(result);
    } else {
        console.warn("Value exceeds JavaScript's safe integer range");
        return null;
    }
}

// =========================================
// Functions
// =========================================
async function findSwapPath() {
    console.log("Requesting swap path from the AMM API...");
    const headers = { 'Content-Type': 'application/json' };
    const body = JSON.stringify({
        token_in_address: tokenIn.contractId(Networks.PUBLIC),
        token_out_address: tokenOut.contractId(Networks.PUBLIC),
        amount: amount.toString(),
    });

    const endpoint = isSend ? '/find-path/' : '/find-path-strict-receive/';
    const estimateResponse = await fetch(`${baseApi}${endpoint}`, { method: 'POST', body, headers });
    const estimateResult = await estimateResponse.json();

    if (!estimateResult.success) {
        throw new Error('Failed to retrieve swap path from AMM API.');
    }

    if (isSend) {
        console.log(`Swap path obtained. Estimated output amount: ${estimateResult.amount / 1e7} ${tokenOut.code}`);
    } else {
        console.log(`Swap path obtained. Estimated input amount: ${estimateResult.amount / 1e7} ${tokenIn.code}`);
    }
    
    return estimateResult;
}

async function executeSwap(estimateResult) {
    console.log("Preparing swap transaction...");
    const keypair = Keypair.fromSecret(userSecretKey);
    const sorobanServer = new rpc.Server(sorobanServerUrl);
    const horizonServer = new Horizon.Server(horizonServerUrl);

    // Construct the parameters from the estimate result
    const swapsChain = xdr.ScVal.fromXDR(estimateResult.swap_chain_xdr, 'base64');
    const tokenInScVal = Address.contract(StrKey.decodeContract(tokenIn.contractId(Networks.PUBLIC))).toScVal();
    const amountU128 = new XdrLargeInt('u128', amount.toString()).toU128();

    // Apply 1% slippage
    const amountWithSlippage = isSend ? estimateResult.amount * 0.99 : estimateResult.amount * 1.01;
    const amountWithSlippageU128 = new XdrLargeInt('u128', Math.floor(amountWithSlippage).toString()).toU128();

    console.log("Loading account from Soroban server...");
    const account = await sorobanServer.getAccount(keypair.publicKey());

    console.log("Building transaction...");
    const functionName = isSend ? 'swap_chained' : 'swap_chained_strict_receive';
    const tx = new TransactionBuilder(account, {
        fee: BASE_FEE,
        networkPassphrase: Networks.PUBLIC,
    })
        .addOperation(
            new StellarSdk.Contract(routerContractId).call(
                functionName,
                xdr.ScVal.scvAddress(Address.fromString(keypair.publicKey()).toScAddress()),
                swapsChain,
                tokenInScVal,
                amountU128,
                amountWithSlippageU128
            )
        )
        .setTimeout(TimeoutInfinite)
        .build();

    console.log("Preparing transaction on Soroban server...");
    const preparedTx = await sorobanServer.prepareTransaction(tx);

    console.log("Signing transaction...");
    preparedTx.sign(keypair);

    console.log("Submitting transaction to Horizon...");
    const result = await horizonServer.submitTransaction(preparedTx);

    if (!result) {
        throw new Error("Transaction submission failed.");
    }

    console.log("Transaction successful. Extracting results...");
    
    const meta = (await sorobanServer.getTransaction(result.id)).resultMetaXdr;
    const returnValue = meta.value().sorobanMeta().returnValue();
    const swapResult = u128ToInt(returnValue.value());

    console.log('Swap successful!');
    
    if (isSend) {
        console.log(`Swapped: ${amount / 1e7} ${tokenIn.code} => ${swapResult / 1e7} ${tokenOut.code}`);
    } else {
        console.log(`Swapped: ${swapResult / 1e7} ${tokenIn.code} => ${amount / 1e7} ${tokenOut.code}`);
    }
}

// =========================================
// Entry Point
// =========================================
findSwapPath()
    .then(estimated => executeSwap(estimated))
    .catch(err => console.error("Error during swap process:", err));

```

</details>

### Swap example transactions

* Swap through 1 pool: <https://stellar.expert/explorer/public/tx/c52f43d518f8cebe8ec849fd5d50c394faa036f96399d723f1f2d286e36c7b94>
* Swap through 2 pools: <https://stellar.expert/explorer/public/tx/d80b317ec117d77433e268e3b544eefdaa702a3fe92169e1f793f613863de1f2>


# Executing swaps through specific pool

Pin a swap to one known pool with the official SDK — quote through the router's own estimator and execute without the path-finding API.

Swap against one pool you choose, bypassing path-finding entirely. `pool.quote()` asks the router's on-chain `estimate_swap` for that pool's hash, and `execute()` submits the router's single-pool swap with a minimum-out guard derived from the quote. The only network dependency is the RPC — for your own routing, a single-market integration, or isolation from other pools.

{% hint style="info" %}
This page uses the [official SDKs](/developers/integrating-with-aquarius) (0.4.0 or later). The [contract-level flow](#contract-level-flow-any-language) below covers other languages and contract sub-invocations.
{% endhint %}

{% hint style="warning" %}
Make sure the account has an established trustline for the asset to receive — otherwise the swap will fail until the trustline is created. See the [Stellar documentation on trustlines](https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/accounts#trustlines).
{% endhint %}

### 1. Set up the client and pick the pool

A pair can have several pools — different types and fee tiers. Pick the exact one you trust:

{% tabs %}
{% tab title="Python" %}

```python
from stellar_sdk import Keypair
from aquarius import AquariusClient, Asset, XLM

AQUA = Asset.classic("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA")

aqua = AquariusClient(network="mainnet", signer=Keypair.from_secret("S..."))

pools = aqua.pools_for_pair(XLM, AQUA)
pool = next(p for p in pools if p.type == "volatile" and p.fee_bps == 30)
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
import { Keypair } from "@stellar/stellar-sdk";
import { AquariusClient, Asset, XLM } from "@aquariusdefi/sdk";

const AQUA = Asset.classic("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA");

const aqua = new AquariusClient({ network: "mainnet", signer: Keypair.fromSecret("S...") });

const pools = await aqua.pools.forPair(XLM, AQUA);
const pool = pools.find(p => p.type === "volatile" && p.feeBps === 30);
```

{% endtab %}
{% endtabs %}

### 2. Quote against this pool only

The quote runs the router's `estimate_swap` for this pool's hash — no signer required, no path-finding API involved:

{% tabs %}
{% tab title="Python" %}

```python
quote = pool.quote(XLM, AQUA, amount_in=1000000, slippage=0.01)   # 0.1 XLM in stroops

print(f"estimated out: {quote.amount_out}")   # base units
print(f"guaranteed minimum: {quote.guard}")   # estimate reduced by slippage
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const quote = await pool.quote({ from: XLM, to: AQUA, amountIn: 1000000n, slippage: 0.01 });   // 0.1 XLM in stroops

console.log(`estimated out: ${quote.amountOut}`);    // base units
console.log(`guaranteed minimum: ${quote.guard}`);   // estimate reduced by slippage
```

{% endtab %}
{% endtabs %}

### 3. Execute

`execute()` submits the router's single-pool `swap` pinned to this pool, with the quote's guard as the on-chain minimum. If the pool moves past the guard between quote and inclusion, the transaction fails with a `SlippageError` whose `requote()` re-reads the same pool. For bots, `pool.swap()` combines quote, execute, and retries:

{% tabs %}
{% tab title="Python" %}

```python
receipt = pool.swap(XLM, AQUA, amount_in=1000000, slippage=0.01, retries=3)

print(f"swapped: {receipt.amount_in} -> {receipt.amount_out}")
print(f"transaction: {receipt.tx_hash}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const receipt = await pool.swap({ from: XLM, to: AQUA, amountIn: 1000000n, slippage: 0.01, retries: 3 });

console.log(`swapped: ${receipt.amountIn} -> ${receipt.amountOut}`);
console.log(`transaction: ${receipt.txHash}`);
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Single-pool swaps are exact-input only: the router has no single-pool strict-receive. For an exact output amount, use the routed path described in [Executing swaps through optimal path](/developers/code-examples/executing-swaps-through-optimal-path).
{% endhint %}

### Contract-level flow (any language)

The SDK methods above wrap the router's single-pool entry points. The steps below call the pool contract directly — useful from other languages and for contract sub-invocations, where a direct pool call costs fewer resources than a chained swap.

To perform a swap, you need to follow these steps:

1\. **Identify pool address**: This part was covered earlier, please check [corresponding article](/developers/code-examples/prerequisites-and-basics#get-pool-contract-id-and-pool-hash).

2\. **Specify user secret key, pool address, amount in, input and output token indices**: You need to specify pool address, the amount of the input token you want to swap in stroops and swap direction by in and out tokens.

{% tabs %}
{% tab title="Python" %}

```python
# Your Secret Key (keep it secure!)
user_secret_key = "S......."
# XLM/AQUA pool address
pool_address = "CCY2PXGMKNQHO7WNYXEWX76L2C5BH3JUW3RCATGUYKY7QQTRILBZIFWV"

# Amount in (0.1 XLM), in stroops (1 XLM = 10^7 stroops)
amount_in = 1000000
# tokens are [XLM, AQUA], so XLM index is 0
in_idx = 0
out_idx = 1
# slippage percent
slippage_percent = 1
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
// Your Secret Key (keep it secure!)
const userSecretKey = "S.......";
// XLM/AQUA pool address
const poolAddress = "CCY2PXGMKNQHO7WNYXEWX76L2C5BH3JUW3RCATGUYKY7QQTRILBZIFWV";
// 0.1 XLM in stroops
const amountIn = 1000000;
// tokens are [XLM, AQUA], so XLM index is 0
const inIdx = 0;
const outIdx = 1;
// slippage percent
const slippagePercent = 1;
```

{% endtab %}
{% endtabs %}

3\. **Calculate minimum amount of token to receive**: This can be achieved by simulating *estimate\_swap* pool method.

{% tabs %}
{% tab title="Python" %}

```python
def estimate_swap():
    print("Estimating swap amount...")
    soroban_server = SorobanServer(soroban_server_url)
    keypair = Keypair.from_secret(user_secret_key)

    source_account = soroban_server.load_account(keypair.public_key)
    tx = (
        TransactionBuilder(
            source_account=source_account,
            network_passphrase=Network.PUBLIC_NETWORK_PASSPHRASE,
            base_fee=100
        )
        .append_invoke_contract_function_op(
            contract_id=pool_address,
            function_name="estimate_swap",
            parameters=[
                scval.to_uint32(in_idx),
                scval.to_uint32(out_idx),
                scval.to_uint128(amount_in),
            ]
        )
        .set_timeout(300)
        .build()
    )

    print("Simulating 'estimate_swap' transaction...")
    sim_result = soroban_server.simulate_transaction(tx)
    if not sim_result or sim_result.error:
        print("Simulation failed.", sim_result.error if sim_result else "")
        return math.nan

    retval = xdr.SCVal.from_xdr(sim_result.results[0].xdr)
    estimated = u128_to_int(retval.u128)
    print(f"Estimated result: {estimated / 1e7}")
    return estimated
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
async function estimateSwap() {
    const sorobanServer = new rpc.Server(sorobanServerUrl);

    const amount = new XdrLargeInt("u128", amountIn.toFixed()).toU128();

    const inIdxSCVal = xdr.ScVal.scvU32(inIdx);
    const outIdxSCVal = xdr.ScVal.scvU32(outIdx);

    const keypair = Keypair.fromSecret(userSecretKey);
    // Load the user account information from Soroban server
    const account = await sorobanServer.getAccount(keypair.publicKey());

    const contract = new Contract(poolAddress);

    // Build the deposit transaction
    const tx = new TransactionBuilder(account, {
        fee: BASE_FEE,
        networkPassphrase: Networks.PUBLIC,
    })
        // Append the invoke_contract_function operation for deposit
        .addOperation(
            contract.call(
                "estimate_swap",
                inIdxSCVal,
                outIdxSCVal,
                amount,
            ),
        )
        .setTimeout(TimeoutInfinite)
        .build();

    // Simulate the result
    const simulateResult = await sorobanServer.simulateTransaction(tx);

    if (!simulateResult.result) {
        console.log(simulateResult.error);
        console.log("Unable to simulate transaction");
        return NaN;
    }

    const result = u128ToInt(simulateResult.result.retval.value());

    console.log(`Estimated result: ${result}`);

    return result;
}
```

{% endtab %}
{% endtabs %}

4\. **Perform swap operation by submitting corresponding transaction.**

{% tabs %}
{% tab title="Python" %}

```python
def execute_swap():
    print("Executing swap...")
    soroban_server = SorobanServer(soroban_server_url)
    horizon_server = Server(horizon_server_url)
    keypair = Keypair.from_secret(user_secret_key)

    estimated_result = estimate_swap()
    if math.isnan(estimated_result):
        print("Estimation failed. Cannot proceed with swap.")
        return

    # Calculate minimum out after slippage
    slippage_coefficient = (100 - slippage_percent) / 100.0
    minimum_out = math.floor(estimated_result * slippage_coefficient)

    # Build swap transaction
    source_account = soroban_server.load_account(keypair.public_key)
    tx = (
        TransactionBuilder(
            source_account=source_account,
            network_passphrase=Network.PUBLIC_NETWORK_PASSPHRASE,
            base_fee=100
        )
        .append_invoke_contract_function_op(
            contract_id=pool_address,
            function_name="swap",
            parameters=[
                scval.to_address(keypair.public_key),
                scval.to_uint32(in_idx),
                scval.to_uint32(out_idx),
                scval.to_uint128(amount_in),
                scval.to_uint128(minimum_out),
            ]
        )
        .set_timeout(300)
        .build()
    )

    print("Preparing transaction for submission...")
    prepared_tx = soroban_server.prepare_transaction(tx)
    prepared_tx.sign(keypair)

    print("Submitting transaction to Horizon...")
    response = horizon_server.submit_transaction(prepared_tx)
    if 'result_meta_xdr' not in response:
        print("Transaction failed.")
        return

    meta = xdr.TransactionMeta.from_xdr(response['result_meta_xdr'])
    meta_body = meta.v4 if meta.v == 4 else meta.v3
    if meta_body and meta_body.soroban_meta:
        out_value = u128_to_int(meta_body.soroban_meta.return_value.u128)
        print("Swap successful!")
        print(f"Received token out: {out_value / 1e7}")
    else:
        print("No result returned by the contract.")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
async function executeSwap() {
    const sorobanServer = new rpc.Server(sorobanServerUrl);
    const horizonServer = new Horizon.Server(horizonServerUrl);

    const amount = new XdrLargeInt("u128", amountIn.toFixed()).toU128();

    const slippageCoefficient = (100 - slippagePercent) / 100;
    const estimatedResult = await estimateSwap();
    const estimateWithSlippage = Math.floor(estimatedResult * slippageCoefficient);
    const minimumOut = new XdrLargeInt("u128", estimateWithSlippage.toFixed()).toU128();

    const inIdxSCVal = xdr.ScVal.scvU32(inIdx);
    const outIdxSCVal = xdr.ScVal.scvU32(outIdx);

    const keypair = Keypair.fromSecret(userSecretKey);
    // Load the user account information from Soroban server
    const account = await sorobanServer.getAccount(keypair.publicKey());

    const contract = new Contract(poolAddress);

    // Build the swap transaction
    const tx = new TransactionBuilder(account, {
        fee: BASE_FEE,
        networkPassphrase: Networks.PUBLIC,
    })
        // Append the invoke_contract_function operation for swap
        .addOperation(
            contract.call(
                "swap",
                xdr.ScVal.scvAddress(
                    Address.fromString(keypair.publicKey()).toScAddress(),
                ),
                inIdxSCVal,
                outIdxSCVal,
                amount,
                minimumOut,
            ),
        )
        .setTimeout(TimeoutInfinite)
        .build();

    // Prepare and sign the transaction
    const preparedTx = await sorobanServer.prepareTransaction(tx);
    preparedTx.sign(keypair);

    // Submit the transaction to the Horizon server
    const result = await horizonServer.submitTransaction(preparedTx);

    // Parse the transaction metadata to extract results
    const meta = (await sorobanServer.getTransaction(result.id)).resultMetaXdr;
    const returnValue = meta.value().sorobanMeta().returnValue();

    // Extract swapped amount
    const outValue = u128ToInt(returnValue.value());

    console.log("Swap successful!");
    console.log(`Received token out: ${outValue / 1e7}`);
}
```

{% endtab %}
{% endtabs %}

### Complete code examples

This code swaps 0.1 XLM to AQUA with Aquarius AMM on mainnet.

To successfully execute the code, provide the secret key of a Stellar account with at least 3 XLM and an established trustline for AQUA.

<details>

<summary>Copy the full code Python</summary>

```python
from stellar_sdk import (
    Keypair,
    TransactionBuilder,
    Network,
    Server,
    xdr,
    scval,
    SorobanServer,
)
import math

# =========================================
# Configuration and Input Variables
# =========================================

user_secret_key = "S......"  # Keep this secret
pool_address = "CCY2PXGMKNQHO7WNYXEWX76L2C5BH3JUW3RCATGUYKY7QQTRILBZIFWV"

# 0.1 XLM in stroops (1 XLM = 10^7 stroops)
amount_in = 1000000

# Token indices: [XLM, AQUA]
in_idx = 0
out_idx = 1

# Slippage in percent
slippage_percent = 1

# Servers
soroban_server_url = "https://mainnet.sorobanrpc.com"
horizon_server_url = "https://horizon.stellar.org"


# =========================================
# Utility Functions
# =========================================
def u128_to_int(parts: xdr.UInt128Parts) -> int:
    """Convert Uint128Parts to Python int."""
    return (parts.hi.uint64 << 64) + parts.lo.uint64


# =========================================
# Soroban Functions
# =========================================
def estimate_swap():
    print("Estimating swap amount...")
    soroban_server = SorobanServer(soroban_server_url)
    keypair = Keypair.from_secret(user_secret_key)

    source_account = soroban_server.load_account(keypair.public_key)
    tx = (
        TransactionBuilder(
            source_account=source_account,
            network_passphrase=Network.PUBLIC_NETWORK_PASSPHRASE,
            base_fee=100
        )
        .append_invoke_contract_function_op(
            contract_id=pool_address,
            function_name="estimate_swap",
            parameters=[
                scval.to_uint32(in_idx),
                scval.to_uint32(out_idx),
                scval.to_uint128(amount_in),
            ]
        )
        .set_timeout(300)
        .build()
    )

    print("Simulating 'estimate_swap' transaction...")
    sim_result = soroban_server.simulate_transaction(tx)
    if not sim_result or sim_result.error:
        print("Simulation failed.", sim_result.error if sim_result else "")
        return math.nan

    retval = xdr.SCVal.from_xdr(sim_result.results[0].xdr)
    estimated = u128_to_int(retval.u128)
    print(f"Estimated result: {estimated / 1e7}")
    return estimated


def execute_swap():
    print("Executing swap...")
    soroban_server = SorobanServer(soroban_server_url)
    horizon_server = Server(horizon_server_url)
    keypair = Keypair.from_secret(user_secret_key)

    estimated_result = estimate_swap()
    if math.isnan(estimated_result):
        print("Estimation failed. Cannot proceed with swap.")
        return

    # Calculate minimum out after slippage
    slippage_coefficient = (100 - slippage_percent) / 100.0
    minimum_out = math.floor(estimated_result * slippage_coefficient)

    # Build swap transaction
    source_account = soroban_server.load_account(keypair.public_key)
    tx = (
        TransactionBuilder(
            source_account=source_account,
            network_passphrase=Network.PUBLIC_NETWORK_PASSPHRASE,
            base_fee=100
        )
        .append_invoke_contract_function_op(
            contract_id=pool_address,
            function_name="swap",
            parameters=[
                scval.to_address(keypair.public_key),
                scval.to_uint32(in_idx),
                scval.to_uint32(out_idx),
                scval.to_uint128(amount_in),
                scval.to_uint128(minimum_out),
            ]
        )
        .set_timeout(300)
        .build()
    )

    print("Preparing transaction for submission...")
    prepared_tx = soroban_server.prepare_transaction(tx)
    prepared_tx.sign(keypair)

    print("Submitting transaction to Horizon...")
    response = horizon_server.submit_transaction(prepared_tx)
    if 'result_meta_xdr' not in response:
        print("Transaction failed.")
        return

    meta = xdr.TransactionMeta.from_xdr(response['result_meta_xdr'])
    meta_body = meta.v4 if meta.v == 4 else meta.v3
    if meta_body and meta_body.soroban_meta:
        out_value = u128_to_int(meta_body.soroban_meta.return_value.u128)
        print("Swap successful!")
        print(f"Received token out: {out_value / 1e7}")
    else:
        print("No result returned by the contract.")


# =========================================
# Entry Point
# =========================================
if __name__ == "__main__":
    execute_swap()

```

</details>

<details>

<summary>Copy the full code JavaScript</summary>

```javascript
const StellarSdk = require("@stellar/stellar-sdk");
const {
    Address,
    Contract,
    TransactionBuilder,
    rpc,
    Horizon,
    BASE_FEE,
    Networks,
    xdr,
    TimeoutInfinite,
    XdrLargeInt,
    Keypair,
} = StellarSdk;

// Step 1. Specify user secret key, pool address, amount in and direction

// User secret key (ensure this is kept secure)
const userSecretKey = "S.......";
// XLM/AQUA pool address
const poolAddress = "CCY2PXGMKNQHO7WNYXEWX76L2C5BH3JUW3RCATGUYKY7QQTRILBZIFWV";
// 0.1 XLM in stroops
const amountIn = 1000000;
// tokens are [XLM, AQUA], so XLM index is 0
const inIdx = 0;
const outIdx = 1;
// slippage percent
const slippagePercent = 1;

// ==========================
// Configuration Variables
// ==========================

// Soroban and Horizon server RPC endpoints
const sorobanServerUrl = "https://mainnet.sorobanrpc.com";
const horizonServerUrl = "https://horizon.stellar.org";

// ==========================
// Utility Functions
// ==========================

function u128ToInt(value) {
    /**
     * Converts UInt128Parts from Stellar's XDR to a JavaScript number.
     *
     * @param {Object} value - UInt128Parts object from Stellar SDK, with `hi` and `lo` properties.
     * @returns {number|null} Corresponding JavaScript number, or null if the number is too large.
     */
    const result =
        (BigInt(value.hi()._value) << 64n) + BigInt(value.lo()._value);

    // Check if the result is within the safe integer range for JavaScript numbers
    if (result <= BigInt(Number.MAX_SAFE_INTEGER)) {
        return Number(result);
    } else {
        console.warn("Value exceeds JavaScript's safe integer range");
        return null;
    }
}

// ==========================
// Swap Function
// ==========================

async function estimateSwap() {
    const sorobanServer = new rpc.Server(sorobanServerUrl);

    const amount = new XdrLargeInt("u128", amountIn.toFixed()).toU128();

    const inIdxSCVal = xdr.ScVal.scvU32(inIdx);
    const outIdxSCVal = xdr.ScVal.scvU32(outIdx);

    const keypair = Keypair.fromSecret(userSecretKey);
    // Load the user account information from Soroban server
    const account = await sorobanServer.getAccount(keypair.publicKey());

    const contract = new Contract(poolAddress);

    // Build the deposit transaction
    const tx = new TransactionBuilder(account, {
        fee: BASE_FEE,
        networkPassphrase: Networks.PUBLIC,
    })
        // Append the invoke_contract_function operation for deposit
        .addOperation(
            contract.call(
                "estimate_swap",
                inIdxSCVal,
                outIdxSCVal,
                amount,
            ),
        )
        .setTimeout(TimeoutInfinite)
        .build();

    // Simulate the result
    const simulateResult = await sorobanServer.simulateTransaction(tx);

    if (!simulateResult.result) {
        console.log(simulateResult.error);
        console.log("Unable to simulate transaction");
        return NaN;
    }

    const result = u128ToInt(simulateResult.result.retval.value());

    console.log(`Estimated result: ${result}`);

    return result;
}

// Step 3. Make a contract call to the pool's "swap" method.
async function executeSwap() {
    const sorobanServer = new rpc.Server(sorobanServerUrl);
    const horizonServer = new Horizon.Server(horizonServerUrl);

    const amount = new XdrLargeInt("u128", amountIn.toFixed()).toU128();

    const slippageCoefficient = (100 - slippagePercent) / 100;
    const estimatedResult = await estimateSwap();
    const estimateWithSlippage = Math.floor(estimatedResult * slippageCoefficient);
    const minimumOut = new XdrLargeInt("u128", estimateWithSlippage.toFixed()).toU128();

    const inIdxSCVal = xdr.ScVal.scvU32(inIdx);
    const outIdxSCVal = xdr.ScVal.scvU32(outIdx);

    const keypair = Keypair.fromSecret(userSecretKey);
    // Load the user account information from Soroban server
    const account = await sorobanServer.getAccount(keypair.publicKey());

    const contract = new Contract(poolAddress);

    // Build the swap transaction
    const tx = new TransactionBuilder(account, {
        fee: BASE_FEE,
        networkPassphrase: Networks.PUBLIC,
    })
        // Append the invoke_contract_function operation for swap
        .addOperation(
            contract.call(
                "swap",
                xdr.ScVal.scvAddress(
                    Address.fromString(keypair.publicKey()).toScAddress(),
                ),
                inIdxSCVal,
                outIdxSCVal,
                amount,
                minimumOut,
            ),
        )
        .setTimeout(TimeoutInfinite)
        .build();

    // Prepare and sign the transaction
    const preparedTx = await sorobanServer.prepareTransaction(tx);
    preparedTx.sign(keypair);

    // Submit the transaction to the Horizon server
    const result = await horizonServer.submitTransaction(preparedTx);

    // Parse the transaction metadata to extract results
    const meta = (await sorobanServer.getTransaction(result.id)).resultMetaXdr;
    const returnValue = meta.value().sorobanMeta().returnValue();

    // Extract swapped amount
    const outValue = u128ToInt(returnValue.value());

    console.log("Swap successful!");
    console.log(`Received token out: ${outValue / 1e7}`);
}

// ==========================
// Entry Point
// ==========================
executeSwap();

```

</details>


# Deposit liquidity

Provide liquidity to an Aquarius pool with the official SDK — discover the pool, deposit with a simulation-derived guard, and read the minted shares.

Deposit tokens into a pool and receive pool share tokens in return. The SDK discovers the pool, orders the tokens, derives the minted-shares guard from a simulation of your exact deposit, and returns what actually happened on-chain.

{% hint style="info" %}
This page uses the [official SDKs](/developers/integrating-with-aquarius#official-sdks). For the raw router signature — `deposit(user, tokens, pool_index, desired_amounts, min_shares)` — and other languages, see [Router & pool contracts](/developers/reference/router-and-pool-contracts).
{% endhint %}

### 1. Set up the client

The account needs a balance of every token it deposits. Everything below works identically on [testnet](/developers/testing-on-testnet) with `network="testnet"` and the testnet AQUA issuer.

{% tabs %}
{% tab title="Python" %}

```python
from stellar_sdk import Keypair
from aquarius import AquariusClient, Asset, XLM

AQUA = Asset.classic("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA")

aqua = AquariusClient(network="mainnet", signer=Keypair.from_secret("S..."))
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
import { Keypair } from "@stellar/stellar-sdk";
import { AquariusClient, Asset, XLM } from "@aquariusdefi/sdk";

const AQUA = Asset.classic("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA");

const aqua = new AquariusClient({ network: "mainnet", signer: Keypair.fromSecret("S...") });
```

{% endtab %}
{% endtabs %}

### 2. Find the pool

A pair can have several pools — different types and fee tiers. `pools_for_pair` returns all of them, sorted by type and fee:

{% tabs %}
{% tab title="Python" %}

```python
pools = aqua.pools_for_pair(XLM, AQUA)
# [Pool(type='volatile', fee_bps=10, ...), Pool(type='volatile', fee_bps=30, ...), ...]

pool = next(p for p in pools if p.type == "volatile" and p.fee_bps == 30)
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const pools = await aqua.pools.forPair(XLM, AQUA);
// [Pool { type: 'volatile', feeBps: 10, ... }, Pool { type: 'volatile', feeBps: 30, ... }, ...]

const pool = pools.find(p => p.type === "volatile" && p.feeBps === 30);
```

{% endtab %}
{% endtabs %}

### 3. Deposit

Amounts are keyed by asset, in base units (stroops); the SDK handles the contract's token ordering. Volatile pools take the two tokens at the current reserve ratio — anything beyond that ratio stays in your account:

{% tabs %}
{% tab title="Python" %}

```python
result = pool.deposit({XLM: 50_0000000, AQUA: 2500_0000000}, slippage=0.01)

print(f"deposited: {result.amounts}")   # base units actually taken, in sorted-token order
print(f"shares minted: {result.shares}")
print(f"transaction: {result.tx_hash}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const result = await pool.deposit({
  amounts: [[XLM, 50_0000000n], [AQUA, 2500_0000000n]],
  slippage: 0.01,
});

console.log(`deposited: ${result.amounts}`);   // base units actually taken, in sorted-token order
console.log(`shares minted: ${result.shares}`);
console.log(`transaction: ${result.txHash}`);
```

{% endtab %}
{% endtabs %}

The `slippage` guard works like a swap's: the SDK simulates your exact deposit, takes the estimated shares, and submits with a minimum reduced by `slippage` (default 1%). If pool state moves past that guard between simulation and inclusion, the transaction fails with a `SlippageError` instead of minting fewer shares than you saw.

{% hint style="warning" %}
`deposit` refuses concentrated pools: a router deposit into one silently opens a **full-range position**, which is rarely what a liquidity provider wants. Manage concentrated positions through the pool contract — see [Concentrated liquidity](/developers/concentrated-liquidity).
{% endhint %}

### Next steps

* [Withdraw liquidity](/developers/code-examples/withdraw-liquidity) — burn the shares for the underlying tokens.
* [Claim LP rewards](/developers/code-examples/claim-lp-rewards) — collect the AQUA the position accrues.
* [Get pools info](/developers/code-examples/get-pools-info) — reserves, share totals, and positions, no signer required.


# Withdraw liquidity

Withdraw liquidity from an Aquarius pool with the official SDK — burn pool shares for the underlying tokens, guarded by per-token minimums.

Burn pool share tokens and receive the underlying tokens back. The SDK reads your share balance, derives per-token minimums from a simulation of your exact withdrawal, and returns the amounts you received.

This page continues from the client setup in [Deposit liquidity](/developers/code-examples/deposit-liquidity). For the raw router signature — `withdraw(user, tokens, pool_index, share_amount, min_amounts)` — see [Router & pool contracts](/developers/reference/router-and-pool-contracts).

### 1. Read your share balance

{% tabs %}
{% tab title="Python" %}

```python
pool = next(p for p in aqua.pools_for_pair(XLM, AQUA) if p.type == "volatile" and p.fee_bps == 30)

shares = pool.share_balance()   # the signer's shares; pass an address to check another account
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const pool = (await aqua.pools.forPair(XLM, AQUA)).find(p => p.type === "volatile" && p.feeBps === 30);

const shares = await pool.shareBalance();   // the signer's shares; pass an address to check another account
```

{% endtab %}
{% endtabs %}

To find every pool where an account holds shares, use `positions()` — see [Get pools info](/developers/code-examples/get-pools-info#positions).

### 2. Withdraw

Pass the share amount to burn — all of it, or any part:

{% tabs %}
{% tab title="Python" %}

```python
result = pool.withdraw(shares, slippage=0.01)

print(f"received: {result.amounts}")    # base units returned, in sorted-token order
print(f"transaction: {result.tx_hash}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const result = await pool.withdraw({ shares, slippage: 0.01 });

console.log(`received: ${result.amounts}`);    // base units returned, in sorted-token order
console.log(`transaction: ${result.txHash}`);
```

{% endtab %}
{% endtabs %}

The `slippage` guard mirrors the deposit's: the SDK simulates the exact withdrawal, takes the estimated per-token amounts, and submits with minimums reduced by `slippage` (default 1%). Withdrawing does not claim accrued rewards — that is a separate call, and the rewards keep waiting for you either way: [Claim LP rewards](/developers/code-examples/claim-lp-rewards).

{% hint style="info" %}
The receiving account must hold a trustline for every classic asset the pool returns. Withdrawing a position you deposited from the same account always satisfies this; a missing trustline fails with a `NoTrustlineError` naming the fix.
{% endhint %}


# Get pools info

Read Aquarius pool data with the official SDK — pools for a pair, reserves, share balances, and an account's positions. No signer required.

Every read on this page works without a signer — construct the client with a network alone. Pool data is also displayed in the app, on each pool's page.

{% hint style="info" %}
This page uses the [official SDKs](/developers/integrating-with-aquarius#official-sdks). For the raw read functions — `get_pools`, `get_info`, `get_reserves`, and the rest — see [Router & pool contracts](/developers/reference/router-and-pool-contracts); the REST alternative is the [Backend API](/developers/reference/backend-api).
{% endhint %}

### Pools for a pair

{% tabs %}
{% tab title="Python" %}

```python
from aquarius import AquariusClient, Asset, XLM

AQUA = Asset.classic("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA")

aqua = AquariusClient(network="mainnet")   # reads need no signer

for pool in aqua.pools_for_pair(XLM, AQUA):
    print(pool.type, pool.fee_bps, pool.address, pool.token_labels)
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
import { AquariusClient, Asset, XLM } from "@aquariusdefi/sdk";

const AQUA = Asset.classic("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA");

const aqua = new AquariusClient({ network: "mainnet" });   // reads need no signer

for (const pool of await aqua.pools.forPair(XLM, AQUA)) {
  console.log(pool.type, pool.feeBps, pool.address, pool.tokenLabels);
}
```

{% endtab %}
{% endtabs %}

Each `Pool` carries:

| Attribute                      | Meaning                                                  |
| ------------------------------ | -------------------------------------------------------- |
| `type`                         | `volatile`, `stable`, or `concentrated`                  |
| `fee_bps` / `feeBps`           | Swap fee in basis points (30 = 0.3%)                     |
| `address`                      | The pool's contract address                              |
| `pool_hash` / `poolHash`       | The pool hash identifying it inside the router           |
| `tokens`                       | Token contract IDs, in the contract's sorted order       |
| `token_labels` / `tokenLabels` | Human-readable names, for example `native`, `AQUA:GBNZ…` |

### Reserves and share totals

{% tabs %}
{% tab title="Python" %}

```python
pool = aqua.pools_for_pair(XLM, AQUA)[0]

pool.reserves()               # base units per token, in sorted-token order
pool.total_shares()           # total pool share supply
pool.share_balance("G...")    # one account's shares
pool.pending_rewards("G...")  # one account's accrued AQUA, in stroops
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const pool = (await aqua.pools.forPair(XLM, AQUA))[0];

await pool.reserves();               // base units per token, in sorted-token order
await pool.totalShares();            // total pool share supply
await pool.shareBalance("G...");     // one account's shares
await pool.pendingRewards("G...");   // one account's accrued AQUA, in stroops
```

{% endtab %}
{% endtabs %}

### Positions

Every pool where an account holds shares, in one call:

{% tabs %}
{% tab title="Python" %}

```python
for position in aqua.positions("G..."):
    print(position.pool.type, position.pool.fee_bps, position.shares)
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
for (const position of await aqua.positions("G...")) {
  console.log(position.pool.type, position.pool.feeBps, position.shares);
}
```

{% endtab %}
{% endtabs %}

Positions are backed by the indexer: a deposit that confirmed moments ago can take a few seconds to appear. Share amounts are read from the chain.

### The REST alternative

For dashboards and indexers that want pool data without touching the chain, the [Backend API](/developers/reference/backend-api) serves the same information over REST — pool lists with type and fee, an account's pools, and volume statistics. The full machine-readable spec is at [amm-api.aqua.network/api/schema/redoc](https://amm-api.aqua.network/api/schema/redoc/).


# Claim LP rewards

Claim accrued AQUA rewards for a liquidity position with the official SDK — check the pending amount, then collect it.

Liquidity in a [reward zone](/voting-and-rewards/aquarius-amm-rewards) pool accrues AQUA every second. Check what a position has accrued, then claim it.

This page continues from the client setup in [Deposit liquidity](/developers/code-examples/deposit-liquidity). For the raw router signature — `claim(user, tokens, pool_index)` — see [Router & pool contracts](/developers/reference/router-and-pool-contracts).

### 1. Check the pending amount

Reading needs no signer — pass any address:

{% tabs %}
{% tab title="Python" %}

```python
pool = next(p for p in aqua.pools_for_pair(XLM, AQUA) if p.type == "volatile" and p.fee_bps == 30)

pending = pool.pending_rewards()   # the signer's accrual, in stroops of AQUA
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const pool = (await aqua.pools.forPair(XLM, AQUA)).find(p => p.type === "volatile" && p.feeBps === 30);

const pending = await pool.pendingRewards();   // the signer's accrual, in stroops of AQUA
```

{% endtab %}
{% endtabs %}

### 2. Claim

{% tabs %}
{% tab title="Python" %}

```python
result = pool.claim_rewards()

print(f"claimed: {result.amount} stroops of AQUA")
print(f"transaction: {result.tx_hash}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const result = await pool.claimRewards();

console.log(`claimed: ${result.amount} stroops of AQUA`);
console.log(`transaction: ${result.txHash}`);
```

{% endtab %}
{% endtabs %}

The claimed AQUA lands in the signer's account; the position itself is untouched and keeps accruing. A pool outside the reward zone accrues nothing — claiming there returns 0.

{% hint style="info" %}
Claiming also refreshes your [reward boost](/aqua-and-ice/ice-boosts-how-to-maximize-lp-rewards) — worth doing periodically while other providers move in and out of the pool.
{% endhint %}


# Add fees to swap

Integrators have the option to introduce a fee for each executed swap.

Enable any Aquarius integrator to take a configurable commission on each swap—without mixing classic Stellar payments and Soroban calls in one transaction.

### Core features

* **Configurable Fee Fraction:** Specify the maximum fee allowed by the contract (for example, 2%), then set the fee amount for each individual swap for maximum flexibility.
* **Fee Destination:** Route collected fees into a single “fee destination” address of your choice.
* **Uniform Denomination:** Swap fees can be converted on claim into one target asset (for example, XLM, USDC, AQUA), so you aren’t left holding a basket of tokens.
* **Out‑of‑the‑Box Wrapper:** A standalone Soroban contract sits atop our Aquarius AMM Router. Call it instead of directly calling the router, and it handles fee deduction and forwarding for you.
* **Secure & Flexible:** Operators can claim raw fees or immediately swap them via predefined routes.

### Setup instructions

**1. Deploy a Fee Collector Smart Contract**

To begin collecting swap fees, deploy your custom fee collector smart contract. This contract stores core parameters such as:

* Fee destination address
* Maximum allowed fee
* Operator address (authorized to claim fees)

**2. Execute Swaps Using the Fee Collector**

Once deployed, the fee collector smart contract allows you to execute swaps similarly to regular Aquarius swaps, with the added capability to specify and collect fees.

**3. Claim Collected Fees**

At any time, the designated operator can invoke the claim method on the smart contract to transfer accumulated fees to the specified destination address.

Continue reading below for more detailed explanations of each step.


# Deploying a new fee collector

Deploy your own fee collector contract and configure its fee parameters.

To receive fees from swaps, deploy your own fee collector instance through the Provider Swap Fee Factory. With the SDK it is one call — the signer becomes the collector's `operator`:

{% hint style="info" %}
This page uses the [official SDKs](/developers/integrating-with-aquarius) (0.5.0 or later). The factory address lives in [Addresses and networks](/developers/reference/addresses-and-networks); the [contract-level flow](#contract-level-flow-any-language) below covers other languages.
{% endhint %}

{% tabs %}
{% tab title="Python" %}

```python
from stellar_sdk import Keypair
from aquarius import AquariusClient

aqua = AquariusClient(network="mainnet", signer=Keypair.from_secret("S..."))

collector = aqua.deploy_fee_collector(
    max_fee_fraction=100,        # per-swap fee_fraction cap: 100 / 10_000 -> up to 1%
    fee_denominator=10_000,      # basis points; raise for finer fee resolution
    fee_destination="G...",      # optional — defaults to the operator (the signer)
)
print(f"collector deployed: {collector.address}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
import { Keypair } from "@stellar/stellar-sdk";
import { AquariusClient } from "@aquariusdefi/sdk";

const aqua = new AquariusClient({ network: "mainnet", signer: Keypair.fromSecret("S...") });

const collector = await aqua.deployFeeCollector({
  maxFeeFraction: 100,        // per-swap feeFraction cap: 100 / 10000 -> up to 1%
  feeDenominator: 10_000,     // basis points; raise for finer fee resolution
  feeDestination: "G...",     // optional — defaults to the operator (the signer)
});
console.log(`collector deployed: ${collector.address}`);
```

{% endtab %}
{% endtabs %}

Parameters, fixed at deploy time:

| Parameter          | Description                                                                                                                                        |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `operator`         | The signer. The only address that can [claim fees](/developers/code-examples/add-fees-to-swap/claiming-and-swapping-accumulated-fees).             |
| `fee_destination`  | Where every claim pays out. Cannot be changed later — not even by the operator.                                                                    |
| `max_fee_fraction` | Upper bound for the per-swap `fee_fraction` this collector accepts.                                                                                |
| `fee_denominator`  | The fraction's denominator. 10,000 (basis points) works well; record it — the contract has no getter, and every `ProviderFeeConfig` must match it. |

The operator and the fee destination can be the same address; separate addresses are safer. The destination needs trustlines for any asset the operator claims raw — or convert claims into one asset with [`claim_and_swap`](/developers/code-examples/add-fees-to-swap/claiming-and-swapping-accumulated-fees).

Verify the deployment with the collector's read methods:

{% tabs %}
{% tab title="Python" %}

```python
print(collector.fee_destination())
print(collector.max_fee_fraction())
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
console.log(await collector.feeDestination());
console.log(await collector.maxFeeFraction());
```

{% endtab %}
{% endtabs %}

Charge fees on swaps with `collector.fee_config(fee_fraction)` — see [Executing swaps with provider fees](/developers/code-examples/add-fees-to-swap/executing-swaps-with-provider-fees).

### Contract-level flow (any language)

The factory function behind `deploy_fee_collector`:

* **Function:**\
  `deploy_swap_fee_contract(operator: Address, fee_destination: Address, max_swap_fee_fraction: u32, swap_fee_fraction_denominator: u32) -> Address`
* **Returns:**\
  Address of the newly deployed **ProviderSwapFeeCollector**.

{% tabs %}
{% tab title="Python" %}

```python
import binascii

from stellar_sdk import Keypair, Network, scval, Server, SorobanServer, StrKey, TransactionBuilder, xdr


def main():
    # =========================================
    # Configuration & Setup
    # =========================================
    # Operator account to be assigned as the fee contract operator
    operator_secret_key = "SA..........."
    operator_keypair = Keypair.from_secret(operator_secret_key)

    # Address to receive the swap fees
    fee_destination_address = "GC..........."

    # Factory contract for provider swap fee contracts (already deployed on network)
    provider_fee_factory_contract_id = "CA4Q2T6FRAFYJYSMDJV7F6B7RL5PS6QS2UOZHBMCT2KSMGQRAAKP2MKO"

    # Maximum allowed swap fee in basis points
    #   (depends on denominator, e.g. 100 bps with 10000 denominator = 1% fee)
    max_swap_fee_bps = 100    # 100 bps = 1% = 100 / 10000
    swap_fee_bps_denominator = 10000

    # Soroban and Horizon server endpoints (adjust for your network)
    soroban_server = SorobanServer("https://mainnet.sorobanrpc.com")
    horizon_server = Server("https://horizon.stellar.org/")
    network_passphrase = Network.PUBLIC_NETWORK_PASSPHRASE

    # =========================================
    # Build Transaction to Deploy Provider Swap Fee Contract
    # =========================================
    # This transaction calls:
    #   deploy_swap_fee_contract(
    #       operator: Address,
    #       fee_destination: Address,
    #       swap_fee_bps: u32,
    #       swap_fee_fraction_denominator: u32,
    #   )
    # on the provider fee factory contract; it returns the address of the newly deployed fee contract.

    # Load operator account from Horizon (must have sufficient XLM)
    source_account = horizon_server.load_account(operator_keypair.public_key)

    # Build transaction
    tx = (
        TransactionBuilder(
            source_account=source_account,
            network_passphrase=network_passphrase,
            base_fee=10000
        )
        .set_timeout(300)
        .append_invoke_contract_function_op(
            contract_id=provider_fee_factory_contract_id,
            function_name="deploy_swap_fee_contract",
            parameters=[
                # operator address (as an SCVal address)
                scval.to_address(operator_keypair.public_key),
                # fee destination address (as an SCVal address)
                scval.to_address(fee_destination_address),
                # max swap fee in basis points (as a uint32)
                scval.to_uint32(max_swap_fee_bps),
                # swap fee bps denominator (as a uint32)
                scval.to_uint32(swap_fee_bps_denominator),
            ]
        )
        .build()
    )

    prepared_tx = soroban_server.prepare_transaction(tx)

    # Sign the transaction with the operator keypair
    prepared_tx.sign(operator_keypair)

    print("Submitting transaction to Horizon...")
    horizon_response = horizon_server.submit_transaction(prepared_tx)
    if not horizon_response['successful']:
        print("Transaction submission failed.")
        return

    response = soroban_server.get_transaction(horizon_response['hash'])
    if not response.result_meta_xdr:
        print("No meta in response.")
        return

    meta = xdr.TransactionMeta.from_xdr(response.result_meta_xdr)
    if meta.v3 and (meta.v4 if meta.v == 4 else meta.v3).soroban_meta:
        address = StrKey.encode_contract(
            binascii.unhexlify((meta.v4 if meta.v == 4 else meta.v3).soroban_meta.return_value.address.contract_id.hash.hex()))
        print("Deploy successful!")
        print(f"Deployed address: {address}")
    else:
        print("No result returned by the contract.")


if __name__ == "__main__":
    main()

```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const {
    Keypair,
    Networks,
    Horizon,
    TransactionBuilder,
    rpc,
    nativeToScVal,
    Address,
    Operation,
    StrKey,
} = require('@stellar/stellar-sdk');

async function main() {
    // =========================================
    // Configuration & Setup
    // =========================================
    
    // Operator account to be assigned as the fee contract operator
    const operatorSecretKey = 'SA...........';
    // Address to receive the swap fees
    const feeDestinationAddress = 'GC...........';
    // Factory contract for provider swap fee contracts (already deployed on network)
    const providerFeeFactoryContractId = 'CA4Q2T6FRAFYJYSMDJV7F6B7RL5PS6QS2UOZHBMCT2KSMGQRAAKP2MKO';
    // Maximum allowed swap fee in basis points
    //   (depends on denominator, e.g. 100 bps with 10000 denominator = 1% fee)
    const maxSwapFeeBps = 100;
    const swapFeeBpsDenominator = 10000;

    // Soroban and Horizon server endpoints (adjust for your network)
    const sorobanServer = new rpc.Server('https://mainnet.sorobanrpc.com');
    const horizonServer = new Horizon.Server('https://horizon.stellar.org');
    const networkPassphrase = Networks.PUBLIC;

    const operatorKeypair = Keypair.fromSecret(operatorSecretKey);

    // =========================================
    // Load operator account
    // =========================================
    const account = await horizonServer.loadAccount(operatorKeypair.publicKey());

    // =========================================
    // Build transaction
    // =========================================
    
    // This transaction calls:
    //   deploy_swap_fee_contract(
    //       operator: Address,
    //       fee_destination: Address,
    //       swap_fee_bps: u32,
    //       swap_fee_fraction_denominator: u32,
    //   )
    // on the provider fee factory contract; it returns the address of the newly deployed fee contract.

    // Build transaction
    const tx = new TransactionBuilder(account, {
        fee: '10000',
        networkPassphrase,
    })
        .setTimeout(300)
        .addOperation(
            Operation.invokeContractFunction({
                contract: providerFeeFactoryContractId,
                function: 'deploy_swap_fee_contract',
                args: [
                    nativeToScVal(new Address(operatorKeypair.publicKey())),
                    nativeToScVal(new Address(feeDestinationAddress)),
                    nativeToScVal(maxSwapFeeBps, { type: 'u32' }),
                    nativeToScVal(swapFeeBpsDenominator, { type: 'u32' }),
                ]
            }),
        )
        .build();

    const preparedTx = await sorobanServer.prepareTransaction(tx);

    // Sign the transaction with the operator keypair
    preparedTx.sign(operatorKeypair);

    console.log('Submitting transaction to Horizon...');
    try {
        const txResponse = await horizonServer.submitTransaction(preparedTx);

        if (!txResponse.successful) {
            console.error('Transaction failed.');
            return;
        }

        const txStatus = await sorobanServer.getTransaction(txResponse.hash);
        if (!txStatus?.resultMetaXdr) {
            console.log('No meta in response.');
            return;
        }

        const resultValue = txStatus.returnValue;

        if (resultValue.value().value()) {
            const contractId = StrKey.encodeContract(resultValue.value().value());
            console.log('Deploy successful!');
            console.log(`Deployed address: ${contractId}`);
        } else {
            console.log('No result returned by the contract.');
        }
    } catch (err) {
        console.error('Transaction error:', err.response?.data || err.message);
    }
}

main();


```

{% endtab %}
{% endtabs %}


# Executing swaps with provider fees

Execute swaps through your fee collector, charging your provider fee on each one.

Route a swap through your deployed fee collector and keep a fraction of the output. Pass a `ProviderFeeConfig` to any quote or swap: the SDK keeps the fee arithmetic exact (ceiling rounding, both amount modes), targets the collector with client-built authorization, and the collector pulls the tokens, takes your cut from the output, and drives the router itself.

{% hint style="info" %}
This page uses the [official SDKs](/developers/integrating-with-aquarius) (0.5.0 or later). The [contract-level flow](#contract-level-flow-any-language) below covers other languages. Deploy a collector first — see [Deploying a new fee collector](/developers/code-examples/add-fees-to-swap/deploying-a-new-fee-collector).
{% endhint %}

### Swap with your fee

The fee is `fee_fraction / fee_denominator` of the swap's output: with the denominator 10,000, `fee_fraction=10` is 0.1% and `fee_fraction=30` is 0.3%. The denominator must match the value your collector was deployed with, and `fee_fraction` must stay at or below its `max_swap_fee_fraction`:

{% tabs %}
{% tab title="Python" %}

```python
from stellar_sdk import Keypair
from aquarius import AquariusClient, Asset, ProviderFeeConfig, XLM

AQUA = Asset.classic("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA")

aqua = AquariusClient(network="mainnet", signer=Keypair.from_secret("S..."))

fee = ProviderFeeConfig(
    contract_id="C...",     # your deployed collector
    fee_fraction=10,        # 10 / 10_000 -> 0.1% of the output
    fee_denominator=10_000,
)

receipt = aqua.swap(XLM, AQUA, amount_in=10_0000000, provider_fee=fee, retries=3)
print(f"user received: {receipt.amount_out}")   # net of your fee
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
import { Keypair } from "@stellar/stellar-sdk";
import { AquariusClient, Asset, XLM } from "@aquariusdefi/sdk";

const AQUA = Asset.classic("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA");

const aqua = new AquariusClient({ network: "mainnet", signer: Keypair.fromSecret("S...") });

const fee = {
  contractId: "C...",     // your deployed collector
  feeFraction: 10,        // 10 / 10000 -> 0.1% of the output
  feeDenominator: 10_000,
};

const receipt = await aqua.swap({ from: XLM, to: AQUA, amountIn: 10_0000000n, providerFee: fee, retries: 3 });
console.log(`user received: ${receipt.amountOut}`);   // net of your fee
```

{% endtab %}
{% endtabs %}

Both amount modes work. With `amount_out` the SDK quotes the gross amount the route must produce so the user still receives exactly the requested output after your fee.

If you attached to the collector with `aqua.fee_collector(...)`, `fee_config()` builds the same configuration with the contract id filled in — see [Claiming & swapping accumulated fees](/developers/code-examples/add-fees-to-swap/claiming-and-swapping-accumulated-fees).

### Send the output to a third party

Set `recipient` to deliver the net output to another address — it does not sign anything. If the recipient lacks a trustline for the output asset, the error names the recipient, not your signer:

{% tabs %}
{% tab title="Python" %}

```python
fee = ProviderFeeConfig(
    contract_id="C...",
    fee_fraction=10,
    fee_denominator=10_000,
    recipient="G...",       # receives the net output; no signature required
)

receipt = aqua.swap(XLM, AQUA, amount_in=10_0000000, provider_fee=fee)
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const fee = {
  contractId: "C...",
  feeFraction: 10,
  feeDenominator: 10_000,
  recipient: "G...",       // receives the net output; no signature required
};

const receipt = await aqua.swap({ from: XLM, to: AQUA, amountIn: 10_0000000n, providerFee: fee });
```

{% endtab %}
{% endtabs %}

Your cut accumulates on the collector contract in the output tokens of the swaps. Collect it with [Claiming & swapping accumulated fees](/developers/code-examples/add-fees-to-swap/claiming-and-swapping-accumulated-fees).

### Contract-level flow (any language)

The complete flow against the collector contract directly — request a route from the Find Path API, build the transaction, and call the collector's `swap_chained` (or `swap_chained_to` with a recipient):

{% tabs %}
{% tab title="Python" %}

```python
from decimal import Decimal

import requests
from stellar_sdk import Asset, Keypair, Network, scval, Server, SorobanServer, TransactionBuilder
from stellar_sdk.xdr import SCVal, TransactionMeta, UInt128Parts

# =========================================
# Configuration & Setup
# =========================================

# This account must have at least 3 XLM and a trustline to AQUA.
user_secret_key = "SA..........."
keypair = Keypair.from_secret(user_secret_key)

# Input and output tokens
token_in = Asset.native()  # XLM
token_out = Asset("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA")

# If True, the swap behaves like strict-send: the amount of the sending asset is fixed.
# If False, the swap behaves like strict-receive: the amount of the receiving asset is fixed.
is_send = True
# Amount of 1 XLM or 1 AQUA in stroops (depending on is_send)
amount = 1_0000000
slippage = Decimal("0.005")  # .5% slippage
provider_fee = Decimal("0.003")  # .3% provider fee
provider_fee_bps_denominator = 10000  # 100 bps = 1% = 100 / 10000

# swap provider contract for AMM swaps on mainnet
# IMPORTANT: Replace with your deployed contract ID. This contract address is only for test purposes.
provider_swap_contract_id = "CDJCVXFIT2UIVNLC22OWCHABTWKXUSYYLPKKBLJW67ISMIWX56YJBGLS"

# Soroban and Horizon servers
soroban_server = SorobanServer("https://mainnet.sorobanrpc.com")
horizon_server = Server("https://horizon.stellar.org/")
network = Network.PUBLIC_NETWORK_PASSPHRASE

# AQUA AMM API endpoint
base_api = 'https://amm-api.aqua.network/api/external/v2'


# =========================================
# Utility Function
# =========================================

def u128_to_int(value: UInt128Parts) -> int:
    """Convert Uint128Parts to Python int."""
    return (value.hi.uint64 << 64) + value.lo.uint64


# =========================================
# Functions
# =========================================

def find_swap_path(base_api: str, token_in_address: str, token_out_address: str, amount: int, is_send: bool) -> (int, str):
    """
    Call the Find Path API to retrieve the swap chain and estimated amount.
    """
    print("Requesting swap path from AMM API...")
    data = {
        'token_in_address': token_in_address,
        'token_out_address': token_out_address,
        'amount': amount,
        'slippage': str(slippage),
        'provider_fee': str(provider_fee),
    }
    endpoint = '/find-path/' if is_send else '/find-path-strict-receive/'
    response = requests.post(f'{base_api}{endpoint}', json=data)
    swap_result = response.json()
    print(swap_result)
    """
        {
          'success': True,
          'swap_chain_xdr': 'AAAAEAAAAAEAAAABAAAAEAAAAAEAAAADAAAAEAAAAAEAAAACAAAAEgAAAAEltPzYWa7C+mNIQ4xImzw8EMmLbSG+T9PLMMtolT75dwAAABIAAAABKIUvaMGYSI40b7EhLtUCkFN2HMJPRTOS41OYIBsIJecAAAANAAAAILLgL8/KbJb4rVy9hOd4Snd7NtnJaiRZQCxPRYRiqrfwAAAAEgAAAAEohS9owZhIjjRvsSEu1QKQU3Ycwk9FM5LjU5ggGwgl5w==',
          'pools': [
            'CDE57N6XTUPBKYYDGQMXX7E7SLNOLFY3JEQB4MULSMR2AKTSAENGX2HC'
          ],
          'tokens': [
            'native',
            'AQUA:GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA'
          ],
          'amount': 3627808902,
          'amount_with_fee': 3627808002,
        }
    """

    if not swap_result.get('success', False):
        raise Exception("Failed to retrieve swap path from the API.")

    print("Swap path retrieved. Estimated amount:", swap_result['amount'])
    print("Estimated amount with fee:", swap_result['amount_with_fee'])
    return int(swap_result['amount_with_fee']), swap_result['swap_chain_xdr']


def execute_swap(
        network: str,
        soroban_rpc_server: SorobanServer,
        horizon_server: Server,
        keypair: Keypair,
        router_contract_id: str,
        token_in_address: str,
        amount: int,
        amount_with_slippage: int,
        swap_path: str,
        provider_fee_bps: int,
        is_send: bool,
) -> int:
    """
    Executes the chained swap transaction on Soroban and returns the final amount out.
    """
    print("Preparing and building swap transaction...")
    source_account = horizon_server.load_account(keypair.public_key)

    function_name = 'swap_chained' if is_send else 'swap_chained_strict_receive'

    # Build the transaction to invoke `swap_chained`
    tx = (
        TransactionBuilder(
            source_account=source_account,
            network_passphrase=network,
            base_fee=10000
        )
        .set_timeout(300)
        .append_invoke_contract_function_op(
            contract_id=router_contract_id,
            function_name=function_name,
            parameters=[
                scval.to_address(keypair.public_key),
                SCVal.from_xdr(swap_path),
                scval.to_address(token_in_address),
                scval.to_uint128(amount),
                scval.to_uint128(amount_with_slippage),
                scval.to_uint32(provider_fee_bps),
            ],
        )
        .build()
    )

    # Prepare transaction to get Soroban-specific data (footprint, etc.)
    print("Preparing transaction on Soroban...")
    prepared_tx = soroban_rpc_server.prepare_transaction(tx)

    # Sign the prepared transaction
    print("Signing transaction...")
    prepared_tx.sign(keypair)

    # Submit the transaction to Horizon
    print("Submitting transaction to Horizon...")
    submit_response = horizon_server.submit_transaction(prepared_tx)

    if not submit_response.get('successful', False):
        raise Exception("Transaction failed: " + str(submit_response))

    print("Transaction submitted successfully. Fetching result...")

    # Get the transaction result from Soroban server to access Soroban metadata
    tx_info = soroban_server.get_transaction(submit_response['id'])
    if not tx_info or not tx_info.result_meta_xdr:
        raise Exception("No transaction metadata found.")

    # Extract the result from the Soroban metadata
    transaction_meta = TransactionMeta.from_xdr(tx_info.result_meta_xdr)
    return_val = (transaction_meta.v4 if transaction_meta.v == 4 else transaction_meta.v3).soroban_meta.return_value
    final_amount = u128_to_int(return_val.u128)
    print("Swap executed successfully.")
    return final_amount


# =========================================
# Entry Point
# =========================================

print("Starting swap process...")
print(f"Swapping {token_in.code} for {token_out.code}...")

# 1. Find the swap path and estimated output
amount_with_slippage, swap_path_xdr = find_swap_path(
    base_api,
    token_in.contract_id(network),
    token_out.contract_id(network),
    amount,
    is_send,
)

# 2. Execute the swap
amount = execute_swap(
    network,
    soroban_server,
    horizon_server,
    keypair,
    provider_swap_contract_id,
    token_in.contract_id(network),
    amount,
    amount_with_slippage,
    swap_path_xdr,
    int(provider_fee * provider_fee_bps_denominator),  # Convert to basis points
    is_send,
)

print("Swap completed successfully!")

if is_send:
    print(f"Amount out: {amount / 10 ** 7} {token_out.code}")  # If it's strict-send, show output amount
else:
    print(f"Amount in: {amount / 10 ** 7} {token_in.code}")  # If it's strict-receive, show input amount

```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const {
    Horizon,
    Keypair,
    Asset,
    Networks,
    TransactionBuilder,
    xdr,
    nativeToScVal,
    Address,
    rpc,
    Operation,
} = require('@stellar/stellar-sdk');

// =========================================
// Configuration & Setup
// =========================================
// This account must have at least 3 XLM and a trustline to AQUA.
const userSecretKey = 'SA...........';
const keypair = Keypair.fromSecret(userSecretKey);

// Assets
const tokenIn = Asset.native();
const tokenOut = new Asset('AQUA', 'GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA');


// If true, the swap behaves like strict-send: the amount of the sending asset is fixed.
// If false, the swap behaves like strict-receive: the amount of the receiving asset is fixed.
const isSend = true;
// Amount of 1 XLM or 1 AQUA in stroops (depending on is_send)
const amount = 1_0000000;
const slippage = 0.005;   // 0.5%
const providerFee = 0.003; // 0.3%
const providerFeeBpsDenominator = 10000;

const providerSwapContractId = 'CDJCVXFIT2UIVNLC22OWCHABTWKXUSYYLPKKBLJW67ISMIWX56YJBGLS';

const sorobanServer = new rpc.Server('https://mainnet.sorobanrpc.com');
const horizonServer = new Horizon.Server('https://horizon.stellar.org');
const networkPassphrase = Networks.PUBLIC;

const baseApi = 'https://amm-api.aqua.network/api/external/v2';

// =========================================
// Utilities
// =========================================
function u128ToInt(value) {
    /**
     * Converts UInt128Parts from Stellar's XDR to a JavaScript number.
     *
     * @param {Object} value - UInt128Parts object from Stellar SDK, with `hi` and `lo` properties.
     * @returns {number|null} Corresponding JavaScript number, or null if the number is too large.
     */
    const result = (BigInt(value.hi()._value) << 64n) + BigInt(value.lo()._value);

    // Check if the result is within the safe integer range for JavaScript numbers
    if (result <= BigInt(Number.MAX_SAFE_INTEGER)) {
        return Number(result);
    } else {
        console.warn("Value exceeds JavaScript's safe integer range");
        return null;
    }
}

// =========================================
// API: Find Swap Path
// =========================================
// Call the Find Path API to retrieve the swap chain and estimated amount.
async function findSwapPath(tokenInAddress, tokenOutAddress, amount, isSend) {
    const data = {
        token_in_address: tokenInAddress,
        token_out_address: tokenOutAddress,
        amount: amount,
        slippage: slippage.toString(),
        provider_fee: providerFee.toString()
    };
    const endpoint = isSend ? '/find-path/' : '/find-path-strict-receive/';
    const response = await fetch(`${baseApi}${endpoint}`, {
        method: 'POST',
        body: JSON.stringify(data),
        headers: { 'Content-Type': 'application/json' }
    });

    const swapResult = await response.json();

    if (!swapResult.success) {
        throw new Error('Failed to retrieve swap path from the API.');
    }

    console.log('Swap path retrieved. Estimated amount:', swapResult.amount / 1e7);
    return {
        amountWithFee: parseInt(swapResult.amount_with_fee),
        swapPathXdr: swapResult.swap_chain_xdr
    };
}

// =========================================
// Soroban: Execute Swap Transaction
// =========================================
// Executes the chained swap transaction on Soroban and returns the final amount out.
async function executeSwap({
    keypair,
    routerContractId,
    tokenInAddress,
    amount,
    amountWithSlippage,
    swapPathXdr,
    providerFeeBps,
    isSend
}) {
    const account = await horizonServer.loadAccount(keypair.publicKey());
    const functionName = isSend ? 'swap_chained' : 'swap_chained_strict_receive';

    // Build the transaction to invoke `swap_chained`
    const tx = new TransactionBuilder(account, {
        fee: '10000',
        networkPassphrase
    })
        .setTimeout(300)
        .addOperation(
            Operation.invokeContractFunction({
                contract: routerContractId,
                function: functionName,
                args: [
                    nativeToScVal(new Address(keypair.publicKey())),
                    xdr.ScVal.fromXDR(swapPathXdr, 'base64'),
                    nativeToScVal(new Address(tokenInAddress)),
                    nativeToScVal(BigInt(amount), { type: 'u128' }),
                    nativeToScVal(BigInt(amountWithSlippage), { type: 'u128' }),
                    nativeToScVal(providerFeeBps, { type: 'u32' })
                ],
            })
        )
        .build();

    // Prepare transaction to get Soroban-specific data (footprint, etc.)
    const preparedTx = await sorobanServer.prepareTransaction(tx);
    // Sign the prepared transaction
    preparedTx.sign(keypair);

    // Submit the transaction to Horizon
    const submitResponse = await horizonServer.submitTransaction(preparedTx);

    if (!submitResponse.successful) {
        throw new Error('Transaction failed: ' + JSON.stringify(submitResponse));
    }

    // Get the transaction result from Soroban server to access Soroban metadata
    const txResult = await sorobanServer.getTransaction(submitResponse.id);
    const meta = txResult.resultMetaXdr;

    if (!meta) {
        throw new Error('No metadata returned from transaction.');
    }

    // Extract the result from the Soroban metadata
    const returnVal = meta.value().sorobanMeta().returnValue();
    const u128 = returnVal.value();

    return u128ToInt(u128);
}

// =========================================
// Main
// =========================================
(async () => {
    try {
        console.log(`Swapping ${tokenIn.code} for ${tokenOut.code}...`);

        const tokenInAddress = tokenIn.contractId(networkPassphrase);
        const tokenOutAddress= tokenOut.contractId(networkPassphrase);

        // 1. Get path & expected output with fees/slippage
        const { amountWithFee, swapPathXdr } = await findSwapPath(
            tokenInAddress,
            tokenOutAddress,
            amount,
            isSend
        );

        // 2. Execute Soroban swap
        const providerFeeBps = Math.round(providerFee * providerFeeBpsDenominator);
        const finalAmount = await executeSwap({
            keypair,
            routerContractId: providerSwapContractId,
            tokenInAddress,
            amount,
            amountWithSlippage: amountWithFee,
            swapPathXdr,
            providerFeeBps,
            isSend
        });

        console.log('Swap completed!');
        if (isSend) {
            console.log(`Amount out: ${finalAmount / 1e7} ${tokenOut.code}`);
        } else {
            console.log(`Amount in: ${finalAmount / 1e7} ${tokenIn.code}`);
        }
    } catch (err) {
        console.error('❌ Swap failed:', err.message || err);
    }
})();

```

{% endtab %}
{% endtabs %}


# Claiming & swapping accumulated fees

Claim the fees your integration has accumulated, or convert them into one target asset.

Your fee accumulates on the collector as plain token balances — the fee comes out of each swap's output token, so a collector serving many pairs holds many tokens. Claims are per token, only the collector's `operator` can call them, and every claim pays out to the `fee_destination` fixed at deploy time.

{% hint style="info" %}
This page uses the [official SDKs](/developers/integrating-with-aquarius) (0.5.0 or later). The [contract-level flow](#contract-level-flow-any-language) below covers other languages.
{% endhint %}

### Read what has accrued

Reads need no signer. `balance` returns 0 for a token the collector has never held:

{% tabs %}
{% tab title="Python" %}

```python
from aquarius import AquariusClient, Asset, XLM

AQUA = Asset.classic("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA")

aqua = AquariusClient(network="mainnet")
collector = aqua.fee_collector("C...")   # your deployed collector

print(collector.fee_destination())       # where claims pay out
print(collector.balance(AQUA))           # accrued fees in AQUA, base units
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
import { AquariusClient, Asset, XLM } from "@aquariusdefi/sdk";

const AQUA = Asset.classic("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA");

const aqua = new AquariusClient({ network: "mainnet" });
const collector = aqua.feeCollector("C...");   // your deployed collector

console.log(await collector.feeDestination()); // where claims pay out
console.log(await collector.balance(AQUA));    // accrued fees in AQUA, base units
```

{% endtab %}
{% endtabs %}

### Claim and convert into one asset

`claim_and_swap` claims a token's entire accrued balance, swaps it through the router along a fresh quoted route, and sends the output token to the fee destination. The minimum-out guard comes from the quote, so a moving market fails the claim instead of underdelivering. The signer must be the operator:

{% tabs %}
{% tab title="Python" %}

```python
from stellar_sdk import Keypair

USDC = Asset.classic("USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN")

aqua = AquariusClient(network="mainnet", signer=Keypair.from_secret("S..."))
collector = aqua.fee_collector("C...")

claimed = collector.claim_and_swap(AQUA, USDC, slippage=0.01)
print(f"claimed {claimed.amount_in} AQUA -> {claimed.amount_out} USDC")
print(f"transaction: {claimed.tx_hash}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
import { Keypair } from "@stellar/stellar-sdk";

const USDC = Asset.classic("USDC", "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN");

const aqua = new AquariusClient({ network: "mainnet", signer: Keypair.fromSecret("S...") });
const collector = aqua.feeCollector("C...");

const claimed = await collector.claimAndSwap(AQUA, USDC, { slippage: 0.01 });
console.log(`claimed ${claimed.amountIn} AQUA -> ${claimed.amountOut} USDC`);
console.log(`transaction: ${claimed.txHash}`);
```

{% endtab %}
{% endtabs %}

### Claim raw

`claim` sweeps one token's balance to the fee destination without conversion. For classic assets the destination needs a trustline for that token:

{% tabs %}
{% tab title="Python" %}

```python
claimed = collector.claim(AQUA)
print(f"claimed {claimed.amount_out} AQUA  {claimed.tx_hash}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const claimed = await collector.claim(AQUA);
console.log(`claimed ${claimed.amountOut} AQUA  ${claimed.txHash}`);
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Claims are operator-gated by the contract: `claim_fees` and `claim_fees_and_swap` require the operator's signature and reject every other caller. The payout address is equally fixed — the operator chooses the token and the route, never the destination.
{% endhint %}

### Contract-level flow (any language)

The collector functions behind `claim_and_swap` and `claim`, called directly:

#### Claim and immediately swap

* **Function:** `claim_fees_and_swap(e, operator: Address, swaps_chain: Vec<…>, token: Address, out_min: u128) -> u128`
* **Effect:**
  1. Claims all of `token`.
  2. Swaps via your router along `swaps_chain`.
  3. Sends resulting tokens to `fee_destination`.
* **Returns:**\
  Final output amount in the target asset.

{% tabs %}
{% tab title="Python" %}

```python
import requests
from stellar_sdk import Asset, Keypair, Network, scval, Server, SorobanServer, TransactionBuilder
from stellar_sdk.xdr import Int128Parts, SCVal, TransactionMeta, UInt128Parts

# =========================================
# Configuration & Setup
# =========================================

# swap provider contract operator
operator_secret_key = "SA..........."
keypair = Keypair.from_secret(operator_secret_key)

# Input and output tokens
token_to_claim = Asset("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA")
token_out = Asset.native()  # XLM
slippage = "0.005"  # 0.5% slippage

# swap provider contract for AMM swaps on mainnet
# IMPORTANT: Replace with your deployed contract ID. This contract address is only for test purposes.
provider_swap_contract_id = "CDJCVXFIT2UIVNLC22OWCHABTWKXUSYYLPKKBLJW67ISMIWX56YJBGLS"

# Soroban and Horizon servers
soroban_server = SorobanServer("https://mainnet.sorobanrpc.com")
horizon_server = Server("https://horizon.stellar.org/")
network = Network.PUBLIC_NETWORK_PASSPHRASE

# AQUA AMM API endpoint
base_api = 'https://amm-api.aqua.network/api/external/v2'


# =========================================
# Utility Function
# =========================================

def u128_to_int(value: UInt128Parts) -> int:
    """Convert Uint128Parts to Python int."""
    return (value.hi.uint64 << 64) + value.lo.uint64


def i128_to_int(value: Int128Parts) -> int:
    """Convert int128Parts to Python int."""
    return (value.hi.int64 << 64) + value.lo.uint64


# =========================================
# Functions
# =========================================

def find_swap_path(base_api: str, token_in_address: str, token_out_address: str, amount: int, is_send: bool) -> (int, str):
    """
    Call the Find Path API to retrieve the swap chain and estimated amount.
    """
    print("Requesting swap path from AMM API...")
    data = {
        'token_in_address': token_in_address,
        'token_out_address': token_out_address,
        'amount': amount,
        'slippage': slippage,
    }
    endpoint = '/find-path/' if is_send else '/find-path-strict-receive/'
    response = requests.post(f'{base_api}{endpoint}', json=data)
    swap_result = response.json()
    print(swap_result)
    """
        {
          'success': True,
          'swap_chain_xdr': 'AAAAEAAAAAEAAAABAAAAEAAAAAEAAAADAAAAEAAAAAEAAAACAAAAEgAAAAEltPzYWa7C+mNIQ4xImzw8EMmLbSG+T9PLMMtolT75dwAAABIAAAABKIUvaMGYSI40b7EhLtUCkFN2HMJPRTOS41OYIBsIJecAAAANAAAAILLgL8/KbJb4rVy9hOd4Snd7NtnJaiRZQCxPRYRiqrfwAAAAEgAAAAEohS9owZhIjjRvsSEu1QKQU3Ycwk9FM5LjU5ggGwgl5w==',
          'pools': [
            'CDE57N6XTUPBKYYDGQMXX7E7SLNOLFY3JEQB4MULSMR2AKTSAENGX2HC'
          ],
          'tokens': [
            'native',
            'AQUA:GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA'
          ],
          'amount': 3627808902,
          'amount_with_fee': 3627808002,
        }
    """

    if not swap_result.get('success', False):
        raise Exception("Failed to retrieve swap path from the API.")

    print("Swap path retrieved. Estimated amount:", swap_result['amount'])
    print("Estimated amount with fee:", swap_result['amount_with_fee'])
    return int(swap_result['amount_with_fee']), swap_result['swap_chain_xdr']


def get_contract_balance(
        network: str,
        soroban_rpc_server: SorobanServer,
        keypair: Keypair,
        contract_address: str,
        token_address: str,
) -> int:
    """
    Retrieves the balance of a specific token for the given account.
    """
    tx = (
        TransactionBuilder(
            source_account=soroban_rpc_server.load_account(keypair.public_key),
            network_passphrase=network,
            base_fee=10000
        )
        .set_timeout(300)
        .append_invoke_contract_function_op(
            contract_id=token_address,
            function_name="balance",
            parameters=[
                scval.to_address(contract_address),
            ],
        )
        .build()
    )
    simulation = soroban_rpc_server.simulate_transaction(tx)
    return i128_to_int(SCVal.from_xdr(simulation.results[0].xdr).i128)


def execute_claim_with_swap(
        network: str,
        soroban_rpc_server: SorobanServer,
        horizon_server: Server,
        keypair: Keypair,
        contract_id: str,
        token_in_address: str,
        amount_with_slippage: int,
        swap_path: str,
) -> int:
    """
    Executes the chained swap transaction on Soroban and returns the final amount out.
    """
    print("Preparing and building swap transaction...")
    source_account = horizon_server.load_account(keypair.public_key)

    # Build the transaction to invoke `swap_chained`
    tx = (
        TransactionBuilder(
            source_account=source_account,
            network_passphrase=network,
            base_fee=10000
        )
        .set_timeout(300)
        .append_invoke_contract_function_op(
            contract_id=contract_id,
            function_name='claim_fees_and_swap',
            parameters=[
                scval.to_address(keypair.public_key),
                SCVal.from_xdr(swap_path),
                scval.to_address(token_in_address),
                scval.to_uint128(amount_with_slippage),
            ],
        )
        .build()
    )

    # Prepare transaction to get Soroban-specific data (footprint, etc.)
    print("Preparing transaction on Soroban...")
    prepared_tx = soroban_rpc_server.prepare_transaction(tx)

    # Sign the prepared transaction
    print("Signing transaction...")
    prepared_tx.sign(keypair)

    # Submit the transaction to Horizon
    print("Submitting transaction to Horizon...")
    submit_response = horizon_server.submit_transaction(prepared_tx)

    if not submit_response.get('successful', False):
        raise Exception("Transaction failed: " + str(submit_response))

    print("Transaction submitted successfully. Fetching result...")

    # Get the transaction result from Soroban server to access Soroban metadata
    tx_info = soroban_server.get_transaction(submit_response['id'])
    if not tx_info or not tx_info.result_meta_xdr:
        raise Exception("No transaction metadata found.")

    # Extract the result from the Soroban metadata
    transaction_meta = TransactionMeta.from_xdr(tx_info.result_meta_xdr)
    return_val = (transaction_meta.v4 if transaction_meta.v == 4 else transaction_meta.v3).soroban_meta.return_value
    final_amount = u128_to_int(return_val.u128)
    print("Swap executed successfully.")
    return final_amount


# =========================================
# Entry Point
# =========================================

print("Starting claim process...")
print(f"Swapping {token_to_claim.code} for {token_out.code}...")

# 0. Get the balance of the token to claim
amount = get_contract_balance(
    network,
    soroban_server,
    keypair,
    provider_swap_contract_id,
    token_to_claim.contract_id(network),
)

# 1. Find the swap path and estimated output
amount_with_slippage, swap_path_xdr = find_swap_path(
    base_api,
    token_to_claim.contract_id(network),
    token_out.contract_id(network),
    amount,
    True,
)

# 2. Execute the claim
amount = execute_claim_with_swap(
    network,
    soroban_server,
    horizon_server,
    keypair,
    provider_swap_contract_id,
    token_to_claim.contract_id(network),
    amount_with_slippage,
    swap_path_xdr,
)

print("Claim with swap completed successfully!")
print(f"Amount out: {amount / 10 ** 7} {token_out.code}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const {
    Horizon,
    Keypair,
    Asset,
    Networks,
    TransactionBuilder,
    nativeToScVal,
    Address,
    Operation,
    xdr,
    rpc
} = require('@stellar/stellar-sdk');

// =========================================
// Configuration
// =========================================

// swap provider contract operator
const operatorSecretKey = 'SA...........'; // Replace with actual secret
const keypair = Keypair.fromSecret(operatorSecretKey);
const publicKey = keypair.publicKey();

// Input and output tokens
const tokenToClaim = new Asset('AQUA', 'GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA');
const tokenOut = Asset.native(); // XLM
const slippage = '0.005';


// swap provider contract for AMM swaps on mainnet
// IMPORTANT: Replace with your deployed contract ID. This contract address is only for test purposes.
const providerSwapContractId = 'CDJCVXFIT2UIVNLC22OWCHABTWKXUSYYLPKKBLJW67ISMIWX56YJBGLS';

// Soroban and Horizon servers
const sorobanServer = new rpc.Server('https://mainnet.sorobanrpc.com');
const horizonServer = new Horizon.Server('https://horizon.stellar.org');
const networkPassphrase = Networks.PUBLIC;

// AQUA AMM API endpoint
const baseApi = 'https://amm-api.aqua.network/api/external/v2';

// =========================================
// Helpers
// =========================================

function u128ToInt(value) {
    const result = (BigInt(value.hi()._value) << 64n) + BigInt(value.lo()._value);
    if (result <= BigInt(Number.MAX_SAFE_INTEGER)) {
        return Number(result);
    } else {
        console.warn("Value exceeds JS safe integer range");
        return result;
    }
}

// Retrieves the balance of a specific token for the given account.
async function getContractTokenBalance(tokenAddress) {
    const sourceAccount = await horizonServer.loadAccount(publicKey);
    const tx = new TransactionBuilder(sourceAccount, {
        fee: '10000',
        networkPassphrase
    })
        .setTimeout(300)
        .addOperation(
            Operation.invokeContractFunction({
                contract: tokenAddress,
                function: 'balance',
                args: [nativeToScVal(new Address(providerSwapContractId))],
            })
        )
        .build();

    const sim = await sorobanServer.simulateTransaction(tx);
    return u128ToInt(sim.result.retval.value());
}

// Call the Find Path API to retrieve the swap chain and estimated amount.
async function findSwapPath(tokenIn, tokenOut, amount) {
    const endpoint = `${baseApi}/find-path/`;
    const response = await fetch(endpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            token_in_address: tokenIn,
            token_out_address: tokenOut,
            amount,
            slippage
        })
    });

    const data = await response.json();
    if (!data.success) throw new Error('Failed to retrieve swap path');
    return {
        amountWithFee: data.amount_with_fee,
        swapPathXDR: data.swap_chain_xdr
    };
}

// Executes the chained swap transaction on Soroban and returns the final amount out.
async function executeClaimAndSwap(tokenIn, amountWithSlippage, swapPathXDR) {
    const account = await horizonServer.loadAccount(publicKey);
    
    // Build the transaction to invoke `swap_chained`
    const tx = new TransactionBuilder(account, {
        fee: '10000',
        networkPassphrase
    })
        .setTimeout(300)
        .addOperation(
            Operation.invokeContractFunction({
                contract: providerSwapContractId,
                function: 'claim_fees_and_swap',
                args: [
                    nativeToScVal(new Address(publicKey)),
                    xdr.ScVal.fromXDR(swapPathXDR, 'base64'),
                    nativeToScVal(new Address(tokenIn)),
                    nativeToScVal(amountWithSlippage, { type: 'u128' })
                ]
            })
        )
        .build();

    // Prepare transaction to get Soroban-specific data (footprint, etc.)
    const preparedTx = await sorobanServer.prepareTransaction(tx);
    // Sign the prepared transaction
    preparedTx.sign(keypair);

    // Submit the transaction to Horizon
    const submitResponse = await horizonServer.submitTransaction(preparedTx);
    if (!submitResponse.successful) {
        throw new Error('Transaction submission failed: ' + JSON.stringify(submitResponse));
    }

    // Get the transaction result from Soroban server to access Soroban metadata
    const txInfo = await sorobanServer.getTransaction(submitResponse.hash);
    const metaXdr = txInfo.resultMetaXdr;
    if (!metaXdr) throw new Error('No metadata');

    // Extract the result from the Soroban metadata
    const returnVal = metaXdr.value().sorobanMeta().returnValue();
    return u128ToInt(returnVal.u128());
}

// =========================================
// Main
// =========================================

(async () => {
    try {
        console.log(`Swapping ${tokenToClaim.code} for ${tokenOut.code}...`);

        const tokenInAddress = tokenToClaim.contractId(networkPassphrase);
        const tokenOutAddress = tokenOut.contractId(networkPassphrase);

        // 1. Get the balance of the token to claim
        const balance = await getContractTokenBalance(tokenInAddress);
        console.log(`Token balance to claim: ${balance / 1e7} ${tokenToClaim.code}`);

        // 2. Find the swap path and estimated output
        const { amountWithFee, swapPathXDR } = await findSwapPath(tokenInAddress, tokenOutAddress, balance);
        console.log(`Swap path found. Final amount with slippage: ${amountWithFee / 1e7}`);

        // 3. Execute the claim
        const resultAmount = await executeClaimAndSwap(tokenInAddress, amountWithFee, swapPathXDR);
        console.log(`Claim + swap successful. Amount received: ${resultAmount / 1e7} ${tokenOut.code}`);
    } catch (err) {
        console.error('Error:', err.message || err);
    }
})();
```

{% endtab %}
{% endtabs %}

#### Claim raw fees

* **Function:** `claim_fees(e, operator: Address, token: Address) -> u128`
* **Effect:** Transfers entire `token` balance from the contract to `fee_destination`.

{% tabs %}
{% tab title="Python" %}

```python
from stellar_sdk import Asset, Keypair, Network, scval, Server, SorobanServer, TransactionBuilder
from stellar_sdk.xdr import Int128Parts, TransactionMeta, UInt128Parts

# =========================================
# Configuration & Setup
# =========================================

# swap provider contract operator
operator_secret_key = "SA..........."
keypair = Keypair.from_secret(operator_secret_key)

# Input and output tokens
token_to_claim = Asset.native()

# swap provider contract for AMM swaps on mainnet
# IMPORTANT: Replace with your deployed contract ID. This contract address is only for test purposes.
provider_swap_contract_id = "CDJCVXFIT2UIVNLC22OWCHABTWKXUSYYLPKKBLJW67ISMIWX56YJBGLS"

# Soroban and Horizon servers
soroban_server = SorobanServer("https://mainnet.sorobanrpc.com")
horizon_server = Server("https://horizon.stellar.org/")
network = Network.PUBLIC_NETWORK_PASSPHRASE


# =========================================
# Utility Function
# =========================================

def u128_to_int(value: UInt128Parts) -> int:
    """Convert Uint128Parts to Python int."""
    return (value.hi.uint64 << 64) + value.lo.uint64


def i128_to_int(value: Int128Parts) -> int:
    """Convert int128Parts to Python int."""
    return (value.hi.int64 << 64) + value.lo.uint64


# =========================================
# Entry Point
# =========================================

print("Starting claim process...")
print(f"Claiming {token_to_claim.code}...")

tx = (
    TransactionBuilder(
        source_account=horizon_server.load_account(keypair.public_key),
        network_passphrase=network,
        base_fee=10000
    )
    .set_timeout(300)
    .append_invoke_contract_function_op(
        contract_id=provider_swap_contract_id,
        function_name='claim_fees',
        parameters=[
            scval.to_address(keypair.public_key),
            scval.to_address(token_to_claim.contract_id(network)),
        ],
    )
    .build()
)

# Prepare transaction to get Soroban-specific data (footprint, etc.)
print("Preparing transaction on Soroban...")
prepared_tx = soroban_server.prepare_transaction(tx)

# Sign the prepared transaction
print("Signing transaction...")
prepared_tx.sign(keypair)

# Submit the transaction to Horizon
print("Submitting transaction to Horizon...")
submit_response = horizon_server.submit_transaction(prepared_tx)

if not submit_response.get('successful', False):
    raise Exception("Transaction failed: " + str(submit_response))

print("Transaction submitted successfully. Fetching result...")

# Get the transaction result from Soroban server to access Soroban metadata
tx_info = soroban_server.get_transaction(submit_response['id'])
if not tx_info or not tx_info.result_meta_xdr:
    raise Exception("No transaction metadata found.")

# Extract the result from the Soroban metadata
transaction_meta = TransactionMeta.from_xdr(tx_info.result_meta_xdr)
return_val = (transaction_meta.v4 if transaction_meta.v == 4 else transaction_meta.v3).soroban_meta.return_value
final_amount = u128_to_int(return_val.u128)

print("Claim completed successfully!")
print(f"Amount out: {final_amount / 10 ** 7} {token_to_claim.code}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const {
    Horizon,
    Keypair,
    Asset,
    Networks,
    TransactionBuilder,
    nativeToScVal,
    Address,
    Operation,
    xdr,
    rpc
} = require('@stellar/stellar-sdk');

// =========================================
// Config & Setup
// =========================================

const operatorSecretKey = 'SA...........'; // your real key
const keypair = Keypair.fromSecret(operatorSecretKey);

const tokenToClaim = Asset.native();

// swap provider contract for AMM swaps on mainnet
// IMPORTANT: Replace with your deployed contract ID. This contract address is only for test purposes.
const providerSwapContractId = 'CDJCVXFIT2UIVNLC22OWCHABTWKXUSYYLPKKBLJW67ISMIWX56YJBGLS';

const sorobanServer = new rpc.Server('https://mainnet.sorobanrpc.com');
const horizonServer = new Horizon.Server('https://horizon.stellar.org');
const networkPassphrase = Networks.PUBLIC;

// =========================================
// Helpers
// =========================================

function u128ToInt(value) {
    const result = (BigInt(value.hi()._value) << 64n) + BigInt(value.lo()._value);
    if (result <= BigInt(Number.MAX_SAFE_INTEGER)) {
        return Number(result);
    } else {
        console.warn("Value exceeds JavaScript's safe integer range");
        return null;
    }
}

// =========================================
// Main
// =========================================

(async () => {
    try {
        console.log(`Claiming ${tokenToClaim.code} fees...`);

        const account = await horizonServer.loadAccount(keypair.publicKey());

        // Build transaction
        const tx = new TransactionBuilder(account, {
            fee: '10000',
            networkPassphrase
        })
            .setTimeout(300)
            .addOperation(
                Operation.invokeContractFunction({
                    contract: providerSwapContractId,
                    function: 'claim_fees',
                    args: [
                        nativeToScVal(new Address(keypair.publicKey())),
                        nativeToScVal(new Address(tokenToClaim.contractId(networkPassphrase)))
                    ],
                })
            )
            .build();

        // Prepare transaction to get Soroban-specific data (footprint, etc.)
        console.log('Preparing transaction...');
        const preparedTx = await sorobanServer.prepareTransaction(tx);

        // Sign the prepared transaction
        console.log('Signing...');
        preparedTx.sign(keypair);

        // Submit the transaction to Horizon
        console.log('Submitting...');
        const submitResponse = await horizonServer.submitTransaction(preparedTx);

        if (!submitResponse.successful) {
            throw new Error('Transaction failed: ' + JSON.stringify(submitResponse));
        }

        console.log('Transaction submitted. Fetching result...');

        // Get the transaction result from Soroban server to access Soroban metadata
        const txInfo = await sorobanServer.getTransaction(submitResponse.hash);
        const metaXdr = txInfo.resultMetaXdr;

        if (!metaXdr) {
            throw new Error('No result metadata.');
        }

        // Extract the result from the Soroban metadata
        const returnVal = metaXdr.value().sorobanMeta().returnValue();
        const u128 = returnVal.u128();
        const finalAmount = u128ToInt(u128);

        console.log('Claim successful!');
        console.log(`Amount claimed: ${finalAmount / 1e7} ${tokenToClaim.code}`);
    } catch (err) {
        console.error('Error:', err.message || err);
    }
})();

```

{% endtab %}
{% endtabs %}


# Concentrated liquidity

How Aquarius concentrated liquidity pools work under the hood — ticks, prices, positions, fees and rewards. A guide for developers managing positions with code instead of the UI.

Aquarius concentrated liquidity pools are inspired by the Uniswap V3 primitive: instead of spreading liquidity across the whole price curve, each liquidity provider chooses a **price range** where their capital is active. This section explains the on-chain model and the smart contract interface so you can create, monitor and manage positions programmatically.

If you are looking for the web interface guide instead, see [Manage concentrated liquidity pool positions](/user-guides/pools/manage-concentrated-liquidity-pool-positions).

{% hint style="info" %}
Concentrated liquidity pools are still under audit — a [Halborn audit](/security/audits) has been ongoing since June 2026. Manage exposure accordingly.
{% endhint %}

### Architecture at a glance

Two contracts matter for a position manager:

* **The AMM router** — the same entry point used by all Aquarius pools ([addresses here](/developers/code-examples/prerequisites-and-basics#constants)). Use it to discover pools (`get_pools`, `get_info`), create new concentrated pools (`init_concentrated_pool`) and execute swaps (`swap`, `swap_chained`).
* **The pool contract itself** — each concentrated pool is a separate contract. All position management (`deposit_position`, `withdraw_position`, fee claiming, state getters) is done by calling the pool contract **directly**, not through the router.

A concentrated pool always holds exactly **two tokens**. Token order matters everywhere in the interface: `token0` and `token1` are the pool tokens sorted by contract ID, the same ordering used across Aquarius ([sorting helper](/developers/code-examples/prerequisites-and-basics#order-tokens-ids)). `get_tokens` on the pool returns the sorted pair.

### Prices and ticks

The pool tracks its current price as a square root in Q64.96 fixed-point format, exactly like Uniswap V3:

* `sqrt_price_x96 = sqrt(price) * 2^96`, where **price is the amount of `token1` per one unit of `token0`, in raw (stroop) units**. Since Stellar assets use 7 decimals on both sides, the raw ratio equals the human-readable price for classic asset pairs.
* Every price maps to a **tick**: `price(tick) = 1.0001^tick`. Tick `0` means a price of exactly 1.0, positive ticks mean `token0` is worth more `token1`, negative ticks less. One tick is a 0.01% price step.
* Ticks are bounded by `MIN_TICK = -887272` and `MAX_TICK = 887272`, covering the price range `[1.0001^-887272, 1.0001^887272]` — effectively any price.

The current price state is returned by `get_slot0` as `{ sqrt_price_x96, tick }`. To convert between prices and ticks off-chain:

```
tick  = floor( log(price) / log(1.0001) )
price = 1.0001 ^ tick
```

### Fee tiers and tick spacing

Positions cannot start or end at arbitrary ticks — the boundaries must be multiples of the pool's **tick spacing**, which is fixed at pool creation and derived from the fee tier:

| Fee tier | `fee` value | Tick spacing | Boundary granularity |
| -------- | ----------- | ------------ | -------------------- |
| 0.1%     | `10`        | `20`         | \~0.2% price steps   |
| 0.3%     | `30`        | `60`         | \~0.6% price steps   |
| 1.0%     | `100`       | `200`        | \~2% price steps     |

`fee` is expressed in basis points of 1/10000: `30` means 0.3% charged on every swap. Query a pool's parameters with `get_fee_fraction` and `get_tick_spacing`, or both at once via `get_info` (returns `pool_type: "concentrated"`, `fee` and `tick_spacing`).

Up to three concentrated pools (one per fee tier) can exist for the same token pair. New pools are created through the router's `init_concentrated_pool(user, tokens, fee)` — tick spacing is set automatically from the fee tier, and a pool creation payment may be charged in AQUA, same as for other pool types.

### Positions

A position is identified by the triple **(owner address, `tick_lower`, `tick_upper`)** — there are no position NFTs or numeric position IDs. Consequences of this model:

* Depositing again into the same range **adds liquidity to the existing position** rather than creating a new one.
* One account can hold up to **20 positions per pool**. Ranges must satisfy `tick_lower < tick_upper`, both multiples of the tick spacing, within the `MIN_TICK`/`MAX_TICK` bounds.
* All positions of an account can be listed on-chain with `get_user_position_snapshot(user)`, which returns the tick ranges and the total liquidity. Details of a single position are available via `get_position(owner, tick_lower, tick_upper)`.

A position holds an abstract **liquidity** amount (the `L` of the constant-product formula, not a token amount). How much of each token a given liquidity amount represents depends on where the current price sits relative to the range:

* **Price below the range** — the position is 100% `token0` (waiting for the price to rise into the range).
* **Price inside the range** — the position holds both tokens; the ratio shifts continuously as the price moves.
* **Price above the range** — the position is 100% `token1`.

You can therefore open one-sided positions deliberately: a range entirely above the current price requires only `token0`, entirely below — only `token1`.

When depositing, the amounts you pass are **maximums you authorize**, not exact spends: the contract computes the largest liquidity purchasable at the current price, takes what it needs and refunds the remainder within the same transaction. The actual amounts spent are returned by the call.

The **router-compatible `deposit`/`withdraw`** functions also work on concentrated pools: they open and close a **full-range position** (the widest range allowed by the tick spacing), making the pool behave like a classic volatile pool. This is what keeps concentrated pools compatible with generic Aquarius integrations.

{% hint style="warning" %}
The **first deposit into an empty pool sets the initial price** from the ratio of the two deposited amounts, and must therefore include both tokens. Double-check your amounts: a wrong initial price is an immediate arbitrage gift to the next trader.
{% endhint %}

### Swap fees

Swap fees accrue to positions whose range contains the trade's price path, proportionally to their share of active liquidity. Fees are tracked per position and do not compound. A protocol share of the swap fee (query `get_protocol_fee_fraction`) is diverted to the protocol treasury before LP distribution.

One extra source of LP income is specific to concentrated pools: if an exact-input swap exhausts the pool's initialized liquidity before consuming its whole input, the unswapped remainder is not refunded to the trader — it is distributed as additional fees to the positions covering the last active tick (see the [swaps section of the reference](/developers/concentrated-liquidity/contract-interface-reference#pool-contract-swaps)). This rewards LPs who extend liquidity coverage to the edges.

Collecting fees:

* `claim_position_fees(owner, tick_lower, tick_upper)` — collects accrued fees of one position.
* `claim_all_position_fees(owner)` — collects across all your positions in the pool.
* `withdraw_position` automatically pays out the position's **entire accrued fee balance** together with the withdrawn principal — even on a partial withdrawal. There is no way to withdraw principal while leaving fees unclaimed.

Pending amounts can be checked without a transaction via `get_position_fees` / `get_all_position_fees` (simulation-only calls).

### AQUA rewards

Concentrated pools participate in the standard Aquarius [AMM rewards](/voting-and-rewards/aquarius-amm-rewards) system, with one crucial difference: **a position earns rewards only while the current tick is inside its range** — the check is `tick_lower <= current_tick < tick_upper`, a binary in-range test. An out-of-range position earns nothing until the price returns.

Rewards accrue to the owner across all their in-range positions and are claimed per pool with `claim(user)` (also available through the router as `claim(user, tokens, pool_index)`). Estimate pending rewards with `get_user_reward(user)`. ICE boosts apply to concentrated positions the same way they do for other pools.

{% hint style="warning" %}
Reward accounting is **checkpoint-based**. Your reward weight — including the in-range check — is evaluated at the moment you interact with the pool (deposit, withdraw, claim) and stays fixed until your next interaction; the pool does not re-evaluate it as the price moves. In practice: a position that drifted out of range keeps accruing rewards at its last-checkpointed weight until your next action, and a position that came back into range earns nothing until an action re-snapshots it. If you manage positions actively, call `claim` (or perform any position action) after the price crosses your range boundary to bring your reward weight up to date.

This checkpoint behavior is a property of the current contract version and **may change in future versions** — don't build long-term assumptions on it.
{% endhint %}

### Typical position lifecycle

1. Find the pool: router `get_pools(tokens)` → filter by `get_info` `pool_type == "concentrated"`.
2. Read the current state: `get_slot0`, `get_tick_spacing`.
3. Choose a price range, convert to ticks, align to tick spacing.
4. Simulate `estimate_deposit_position(tick_lower, tick_upper, desired_amounts)` to see the actual amounts and liquidity.
5. Call `deposit_position(...)` with a `min_liquidity` slippage guard.
6. Monitor: `get_slot0().tick` vs your range; rebalance by withdrawing and re-depositing when the price escapes.
7. Collect: `claim_position_fees` for swap fees, `claim` for AQUA rewards.
8. Exit: `withdraw_position(...)` with `min_amounts` slippage guards.

Continue with the [Contract interface reference](/developers/concentrated-liquidity/contract-interface-reference) for exact signatures, or jump straight to the [code examples](/developers/concentrated-liquidity/managing-positions-code-examples).


# Contract interface reference

Full reference of the concentrated liquidity pool smart contract interface: position management, state getters, fees, swaps and rewards.

This page is the canonical public reference for the concentrated liquidity pool contract interface. Functions are grouped by where they live: the **router** (shared Aquarius entry point) and the **pool contract** (one per concentrated pool). Signatures are given in Rust/Soroban notation; the `e: Env` argument is implicit in every Soroban contract call and is not passed by the client.

Read-only getters and `estimate_*` functions don't change state — call them through transaction **simulation**, no signature or fees required. Estimates enforce the same validation as the real calls: on invalid input the simulation **fails with the corresponding error code** (for example, `PositionNotFound`, `AllCoinsRequired`, `OutMinNotSatisfied` when amounts round to zero liquidity) instead of returning a value — handle simulation errors accordingly.

## Router functions

The router address is listed in [Prerequisites & basics](/developers/code-examples/prerequisites-and-basics#constants). Token vectors must always be [sorted by contract ID](/developers/code-examples/prerequisites-and-basics#order-tokens-ids).

### init\_concentrated\_pool

Creates a concentrated pool for a token pair, or returns the existing one for that fee tier.

```rust
fn init_concentrated_pool(
    e: Env,
    user: Address,
    tokens: Vec<Address>,
    fee: u32,
) -> (BytesN<32>, Address);
```

* `user` — the account creating the pool (pays the pool creation fee in AQUA if one is configured).
* `tokens` — the two token addresses, sorted.
* `fee` — one of `10` (0.1%), `30` (0.3%), `100` (1.0%). Tick spacing is derived automatically: `20`, `60` or `200` respectively.

Returns the pool index hash and the pool contract address.

### Pool discovery

```rust
fn get_pools(e: Env, tokens: Vec<Address>) -> Map<BytesN<32>, Address>;
fn get_info(e: Env, tokens: Vec<Address>, pool_index: BytesN<32>) -> Map<Symbol, Val>;
fn get_pool(e: Env, tokens: Vec<Address>, pool_index: BytesN<32>) -> Address;
```

`get_pools` returns all pools for a pair (all types). Concentrated pools report `pool_type: "concentrated"` in `get_info`, along with `fee` and `tick_spacing`. See the [Get pools info example](/developers/code-examples/get-pools-info).

### Router-level deposit, withdraw, swaps, rewards

The generic router functions documented in [Router & pool contracts](/developers/reference/router-and-pool-contracts) — `deposit`, `withdraw`, `swap_chained`, `claim` — all work with concentrated pools. A router `deposit` into a concentrated pool opens a **full-range position** (see [position model](/developers/concentrated-liquidity#positions)). For ranged positions, call the pool contract directly.

## Pool contract: position management

### deposit\_position

Opens a new position or adds liquidity to an existing one with the same range.

```rust
fn deposit_position(
    e: Env,
    sender: Address,
    tick_lower: i32,
    tick_upper: i32,
    desired_amounts: Vec<u128>,
    min_liquidity: u128,
) -> (Vec<u128>, u128);
```

* `sender` — position owner; authorizes the token transfers.
* `tick_lower`, `tick_upper` — range boundaries; must be multiples of the pool's tick spacing, `tick_lower < tick_upper`, within `±887272`.
* `desired_amounts` — `[amount0, amount1]` in raw units (token order = sorted order). These are **maximums you authorize**, not exact spends: the contract computes the maximum liquidity purchasable at the current price, pulls what it needs and refunds the excess within the same transaction.
* `min_liquidity` — slippage guard on **liquidity units** (not token amounts): the transaction fails with `OutMinNotSatisfied` if the minted liquidity is below this value.

Returns `(actual_amounts, minted_liquidity)` — the amounts actually spent and the liquidity minted.

Special cases:

* If the current price is **outside the range**, only one token is required — pass `0` for the other. Which one depends on the side: a range entirely **above** the current price takes only `token0`, entirely **below** — only `token1`. Passing only the wrong-side token yields zero liquidity and fails with `OutMinNotSatisfied`.
* The **first deposit into an empty pool initializes the pool price** from `amount1 / amount0`. It must include both tokens (`AllCoinsRequired` otherwise), and the tick derived from that ratio must fall inside the deposit range (`TickOutOfBounds` otherwise).

### estimate\_deposit\_position

Simulation counterpart of `deposit_position` — same math, no transfers.

```rust
fn estimate_deposit_position(
    e: Env,
    tick_lower: i32,
    tick_upper: i32,
    desired_amounts: Vec<u128>,
) -> (Vec<u128>, u128);
```

### withdraw\_position

Burns liquidity from a position and transfers principal **plus all accrued swap fees** to the owner. This applies to **partial withdrawals too**: any `withdraw_position` call, regardless of `amount`, always sweeps the position's entire accrued fee balance along with the withdrawn principal — there is no way to withdraw principal while leaving fees unclaimed. Withdrawing the full liquidity closes the position.

```rust
fn withdraw_position(
    e: Env,
    owner: Address,
    tick_lower: i32,
    tick_upper: i32,
    amount: u128,
    min_amounts: Vec<u128>,
) -> Vec<u128>;
```

* `amount` — liquidity to burn (up to the position's total; see `get_position`).
* `min_amounts` — `[min0, min1]` slippage guards checked against the total payout (principal + fees).

Returns the paid-out token amounts. Withdrawals are always available — this function has no kill-switch.

### estimate\_withdraw\_position

```rust
fn estimate_withdraw_position(
    e: Env,
    owner: Address,
    tick_lower: i32,
    tick_upper: i32,
    amount: u128,
) -> Vec<u128>;
```

Like `withdraw_position`, the returned amounts **include the position's accrued fees** on top of the principal.

## Pool contract: swap fees

```rust
// Pending fees of one position (simulate, read-only)
fn get_position_fees(e: Env, owner: Address, tick_lower: i32, tick_upper: i32) -> Vec<u128>;
// Pending fees across all owner's positions
fn get_all_position_fees(e: Env, owner: Address) -> Vec<u128>;

// Collect fees of one position
fn claim_position_fees(e: Env, owner: Address, tick_lower: i32, tick_upper: i32) -> Vec<u128>;
// Collect fees of all owner's positions
fn claim_all_position_fees(e: Env, owner: Address) -> Vec<u128>;
```

All return `[amount0, amount1]`. Claiming emits a `claim_fees` event with the owner and token addresses as topics.

## Pool contract: state getters

```rust
// Current price state: sqrt_price_x96 (Q64.96) and current tick
fn get_slot0(e: Env) -> Slot0;
// Tick spacing of this pool (20 / 60 / 200)
fn get_tick_spacing(e: Env) -> i32;
// Liquidity currently active at the current tick
fn get_active_liquidity(e: Env) -> u128;

// Single position details
fn get_position(e: Env, recipient: Address, tick_lower: i32, tick_upper: i32) -> PositionData;
// All positions of a user: tick ranges + total raw liquidity
fn get_user_position_snapshot(e: Env, user: Address) -> UserPositionSnapshot;

// Tick-level data (advanced: building liquidity distribution charts)
fn get_tick(e: Env, tick: i32) -> TickInfo;
fn get_ticks_batch(e: Env, ticks: Vec<i32>) -> Vec<TickInfo>;
// Bounds of initialized ticks; min > max means no initialized ticks
fn get_tick_bounds(e: Env) -> (i32, i32);
// Tick corresponding to the price ratio amount1/amount0
fn tick_from_amounts(e: Env, amount0: u128, amount1: u128) -> i32;

// Cumulative swap fees per unit of liquidity since pool creation, Q128 fixed-point
fn get_fee_growth_global_0_x128(e: Env) -> U256;
fn get_fee_growth_global_1_x128(e: Env) -> U256;

// Bitmap index of initialized ticks (see below)
fn get_chunk_bitmap(e: Env, word_pos: i32) -> U256;
fn get_chunk_bitmap_batch(e: Env, start_word: i32, count: u32) -> Vec<U256>;

// Generic pool info (also available via router)
fn get_info(e: Env) -> Map<Symbol, Val>;   // pool_type, fee, tick_spacing
fn get_tokens(e: Env) -> Vec<Address>;     // [token0, token1] sorted
fn get_reserves(e: Env) -> Vec<u128>;      // LP reserves, excludes protocol fees
fn get_fee_fraction(e: Env) -> u32;
fn get_protocol_fee_fraction(e: Env) -> u32;
// Total liquidity across all positions; user's total across their positions
fn get_total_shares(e: Env) -> u128;
fn get_user_shares(e: Env, user: Address) -> u128;
```

Relevant types:

```rust
struct Slot0 {
    sqrt_price_x96: U256, // sqrt(token1/token0 price) * 2^96
    tick: i32,            // floor(log_1.0001(price))
}

struct PositionData {
    fee_growth_inside_0_last_x128: U256, // internal fee accounting
    fee_growth_inside_1_last_x128: U256,
    liquidity: u128,                     // position's liquidity
    tokens_owed_0: u128,                 // fees settled but not yet collected
    tokens_owed_1: u128,
}

struct UserPositionSnapshot {
    ranges: Vec<PositionRange>, // { tick_lower: i32, tick_upper: i32 }
    raw_liquidity: u128,        // sum of all position liquidity
    weighted_liquidity: u128,   // boosted working liquidity — the user's reward
                                // weight as of their last checkpoint (see rewards)
}
```

{% hint style="info" %}
`tokens_owed_*` in `get_position` only reflects fees settled during past interactions with the position. For the live pending total, use `get_position_fees`.
{% endhint %}

{% hint style="warning" %}
**Missing-position behavior differs between getters.** `get_position`, `get_position_fees` and `claim_position_fees` **fail with `PositionNotFound`** when called for a range the owner has no position in (including one you have already fully withdrawn). The aggregate views — `get_user_position_snapshot` and `get_all_position_fees` — safely return empty/zero results for a user with no positions.
{% endhint %}

#### Enumerating initialized ticks

To reconstruct the pool's liquidity distribution (for example, for a chart), you need every **initialized tick** — a tick referenced by at least one position boundary (`liquidity_gross > 0` in `TickInfo`). Two strategies:

* **Simple:** call `get_tick_bounds` and scan `get_ticks_batch` over multiples of the tick spacing between the bounds. Fine for pools with a modest active range.
* **Sparse (bitmap):** ticks are indexed in a two-level bitmap. Ticks are grouped into **chunks of 16**: `chunk_pos = floor((tick / tick_spacing) / 16)`. Each bit of a 256-bit **word** flags a chunk containing at least one initialized tick: `word_pos = chunk_pos >> 8`, bit index `chunk_pos & 255` (bit 0 = least significant). Scan words with `get_chunk_bitmap_batch`, then read only the flagged chunks' ticks via `get_ticks_batch`.

Given active liquidity at the current tick (`get_active_liquidity`) and the `liquidity_net` deltas of initialized ticks, liquidity at any tick is reconstructed by walking outward from the current tick and applying the deltas: add `liquidity_net` when crossing a tick left-to-right, subtract when right-to-left.

## Pool contract: swaps

Concentrated pools implement the standard Aquarius pool swap interface, so direct-pool swaps work exactly like the [Executing swaps through specific pool](/developers/code-examples/executing-swaps-through-specific-pool) example:

```rust
fn swap(e: Env, user: Address, in_idx: u32, out_idx: u32, in_amount: u128, out_min: u128) -> u128;
fn estimate_swap(e: Env, in_idx: u32, out_idx: u32, in_amount: u128) -> u128;
fn swap_strict_receive(e: Env, user: Address, in_idx: u32, out_idx: u32, out_amount: u128, in_max: u128) -> u128;
fn estimate_swap_strict_receive(e: Env, in_idx: u32, out_idx: u32, out_amount: u128) -> u128;
```

`in_idx`/`out_idx` are the token indices in the sorted pair (0 or 1).

{% hint style="warning" %}
**Partial-fill behavior differs between the two swap modes.** An exact-input `swap` that runs out of initialized liquidity before consuming the whole input does **not refund the unswapped remainder**: the remainder is treated like a swap fee — the protocol share is diverted to the treasury and the rest is distributed to the liquidity positions that covered the last active tick of the swap path. This is intentional: a refund leg could itself fail mid-transaction (for example, a missing trustline on the refund path of a chained swap), so the contract guarantees a simple invariant instead — **exactly `in_amount` is consumed, and at least `out_min` is received**, or the transaction fails. Always set a meaningful `out_min` and check `estimate_swap` first. Edge cases: a swap that cannot move any liquidity at all reverts with `InsufficientLiquidity` rather than consuming the input, and an exact-output `swap_strict_receive` that cannot produce `out_amount` fails with `InsufficientLiquidity` as well.

Multi-hop routes (`swap_chained`) through concentrated pools are also heavier computationally than through other pool types and can hit Soroban per-transaction limits sooner.
{% endhint %}

## Pool contract: AQUA rewards

```rust
// Pending reward estimate
fn get_user_reward(e: Env, user: Address) -> u128;
// Detailed rewards state (keys include to_claim, tps, exp_at, boost, working_balance)
fn get_rewards_info(e: Env, user: Address) -> Map<Symbol, i128>;
// Claim accrued rewards
fn claim(e: Env, user: Address) -> u128;

// Preview the (unboosted, boosted) working balance a deposit would produce —
// useful for estimating the ICE boost effect before depositing
fn estimate_working_balance(
    e: Env, user: Address, tick_lower: i32, tick_upper: i32, new_liquidity: u128,
) -> (u128, u128);

// Rewards participation flag (opted in by default)
fn get_rewards_state(e: Env, user: Address) -> bool;
fn set_rewards_state(e: Env, user: Address, state: bool);
```

Remember: positions accrue rewards **only while in range** — `tick_lower <= current_tick < tick_upper` — and the in-range check is **checkpoint-based**: the reward weight is snapshotted when the owner interacts with the pool (deposit, withdraw, claim) and is not re-evaluated between interactions as the price moves. A position that left its range keeps accruing at the stale weight until the owner's next action, and one that re-entered its range earns nothing until then — perform an action (for example, `claim`) after the price crosses a range boundary to re-sync. Note: this checkpoint behavior is specific to the current contract version and may change in future versions.

The same operations are available through the router with `(tokens, pool_index)` arguments. Calling `claim` with nothing accrued succeeds and pays zero — it is a no-op, not an error. Reward claiming can be temporarily paused by protocol admins in emergencies; principal withdrawal can not.

## Events

Events emitted by the pool contract, for indexers and monitoring (topics listed first, then the data payload):

| Event                | Topics (after the event name)     | Data                                          |
| -------------------- | --------------------------------- | --------------------------------------------- |
| `deposit_liquidity`  | `token0`, `token1`                | `liquidity`, `amount0`, `amount1`             |
| `withdraw_liquidity` | `token0`, `token1`                | `liquidity`, `amount0`, `amount1`             |
| `position_update`    | `user`                            | `tick_lower`, `tick_upper`, `liquidity_delta` |
| `pool_state`         | —                                 | `sqrt_price_x96`, `tick`, `active_liquidity`  |
| `claim_fees`         | `owner`, `token0`, `token1`       | `amount0`, `amount1`                          |
| `claim_reward`       | `reward_token`, `user`            | `amount`                                      |
| `trade`              | `token_in`, `token_out`, `trader` | `sold_amount`, `bought_amount`, `fee`         |
| `update_reserves`    | —                                 | `reserve0`, `reserve1`                        |

Correlation notes for indexers: a swap emits `trade`, `update_reserves` and `pool_state` together; `withdraw_position` additionally emits `claim_fees` when it auto-sweeps the position's accrued fees.

## Common errors

| Code   | Error                     | Meaning                                                                                                                                               |
| ------ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `201`  | PoolAlreadyInitialized    | Pool init attempted twice                                                                                                                             |
| `205`  | DepositKilled             | Deposits are paused by the protocol                                                                                                                   |
| `2002` | TokensNotSorted           | Token vector not sorted by contract ID                                                                                                                |
| `2101` | InvalidTickRange          | Malformed tick range                                                                                                                                  |
| `2107` | TickOutOfBounds           | Tick outside allowed bounds (for example, first-deposit price outside the deposit range)                                                              |
| `2109` | TickNotSpacedCorrectly    | Boundary is not a multiple of the tick spacing                                                                                                        |
| `2110` | TickLowerNotLessThanUpper | `tick_lower >= tick_upper`                                                                                                                            |
| `2111` | TickLowerTooLow           | Below `MIN_TICK`                                                                                                                                      |
| `2112` | TickUpperTooHigh          | Above `MAX_TICK`                                                                                                                                      |
| `2118` | PositionNotFound          | No position for (owner, range)                                                                                                                        |
| `2119` | TooManyPositions          | More than 20 positions per user per pool                                                                                                              |
| `2121` | InsufficientLiquidity     | Withdraw amount exceeds position liquidity; or the pool cannot fill a swap (exact-output cannot produce `out_amount`, exact-input cannot move at all) |

Generic validation errors (zero amounts, slippage guards like `OutMinNotSatisfied`) are shared with other Aquarius pool types.


# Managing positions: code examples

Code examples for the full concentrated liquidity position lifecycle: reading pool state, opening a position, monitoring it, claiming fees and rewards, and withdrawing.

This page walks through the full lifecycle of a concentrated liquidity position with the [official SDKs](/developers/integrating-with-aquarius) (0.3.0 or later). Amounts and liquidity always come from the contract's own estimators — the SDK converts prices to ticks with exact integer math and derives every slippage guard from a quote, never from client-side arithmetic.

{% hint style="info" %}
The [contract-level scripts](#contract-level-scripts-any-language) at the bottom cover other languages; per-function signatures live in the [contract interface reference](/developers/concentrated-liquidity/contract-interface-reference). The concentrated surface is beta: the contracts are under an external audit.
{% endhint %}

### Step 1. Set up and find a concentrated pool

{% tabs %}
{% tab title="Python" %}

```python
from stellar_sdk import Keypair
from aquarius import AquariusClient, Asset, XLM

AQUA = Asset.classic("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA")

aqua = AquariusClient(network="mainnet", signer=Keypair.from_secret("S..."))

pool = next(p for p in aqua.pools_for_pair(XLM, AQUA) if p.type == "concentrated")

print(pool.slot0())          # Slot0(sqrt_price_x96=..., tick=...)
print(pool.tick_spacing())   # 20 (0.1% pool), 60 (0.3%) or 200 (1.0%)
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
import { Keypair } from "@stellar/stellar-sdk";
import { AquariusClient, Asset, XLM } from "@aquariusdefi/sdk";

const AQUA = Asset.classic("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA");

const aqua = new AquariusClient({ network: "mainnet", signer: Keypair.fromSecret("S...") });

const pool = (await aqua.pools.forPair(XLM, AQUA)).find(p => p.type === "concentrated");

console.log(await pool.slot0());         // { sqrtPriceX96: ..., tick: ... }
console.log(await pool.tickSpacing());   // 20 (0.1% pool), 60 (0.3%) or 200 (1.0%)
```

{% endtab %}
{% endtabs %}

### Step 2. Convert your price range to ticks

Prices are token1 per token0 in base units. `tick_from_price` is integer-exact and identical across both SDKs; `snap_tick` aligns a tick to the pool's spacing. Passing `price_range` to the estimator in the next step performs both conversions for you:

{% tabs %}
{% tab title="Python" %}

```python
from aquarius import price_at_tick, snap_tick, tick_from_price

spacing = pool.tick_spacing()
tick = pool.current_tick()

# Range: 5% below to 5% above the current price
lower = snap_tick(tick_from_price(price_at_tick(tick) * 0.95), spacing)
upper = snap_tick(tick_from_price(price_at_tick(tick) * 1.05), spacing, up=True)
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
import { priceAtTick, snapTick, tickFromPrice } from "@aquariusdefi/sdk";

const spacing = await pool.tickSpacing();
const tick = await pool.currentTick();

// Range: 5% below to 5% above the current price
const lower = snapTick(tickFromPrice(priceAtTick(tick) * 0.95), spacing);
const upper = snapTick(tickFromPrice(priceAtTick(tick) * 1.05), spacing, { up: true });
```

{% endtab %}
{% endtabs %}

### Step 3. Estimate and open the position

Deposits are two-phase: `estimate_position_deposit` quotes through the contract's own estimator, then `execute` runs the deposit with a minimum-liquidity guard derived from that quote. The contract pulls the desired amounts and refunds whatever the range does not need:

{% tabs %}
{% tab title="Python" %}

```python
est = pool.estimate_position_deposit(
    {XLM: 10_0000000, AQUA: 5000_0000000},
    tick_range=(lower, upper),          # or price_range=(low, high)
)
print(f"will use: {est.amounts}, minting ~{est.liquidity} liquidity")

position = est.execute(slippage=0.01)   # min-liquidity guard from the estimate
print(f"opened with {position.liquidity} liquidity  {position.tx_hash}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const est = await pool.estimatePositionDeposit({
  amounts: [[XLM, 10_0000000n], [AQUA, 5000_0000000n]],
  tickRange: [lower, upper],            // or priceRange: [low, high]
});
console.log(`will use: ${est.amounts}, minting ~${est.liquidity} liquidity`);

const position = await est.execute({ slippage: 0.01 });   // min-liquidity guard from the estimate
console.log(`opened with ${position.liquidity} liquidity  ${position.txHash}`);
```

{% endtab %}
{% endtabs %}

A position is the key `(owner, tick_lower, tick_upper)` — no NFTs, merged on re-deposit, at most 20 ranges per account.

### Step 4. List your positions and check if they are in range

`position_range_status` matches the contract's active-liquidity rule exactly: the range is half-open `[tick_lower, tick_upper)`, so a position sitting at `tick_upper` is already out and earning nothing. "Below" means the price is under the range and the position sits entirely in the first sorted token; "above" — entirely in the second:

{% tabs %}
{% tab title="Python" %}

```python
for tick_lower, tick_upper in pool.position_ranges():
    status = pool.position_range_status(tick_lower, tick_upper)
    print(f"[{tick_lower}, {tick_upper}): {status.status}")   # in_range | below | above

value = pool.position_value(lower, upper)
print(f"principal: {value.principal}, fees: {value.fees}, total: {value.total}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
for (const [tickLower, tickUpper] of await pool.positionRanges()) {
  const status = await pool.positionRangeStatus(tickLower, tickUpper);
  console.log(`[${tickLower}, ${tickUpper}): ${status.status}`);   // in_range | below | above
}

const value = await pool.positionValue(lower, upper);
console.log(`principal: ${value.principal}, fees: ${value.fees}, total: ${value.total}`);
```

{% endtab %}
{% endtabs %}

### Step 5. Check and claim swap fees

Fees accrue per position while it is in range. Withdrawals auto-claim the position's fees, so explicit claims are for harvesting without touching the principal:

{% tabs %}
{% tab title="Python" %}

```python
print(pool.position_fees(lower, upper))     # accrued, unclaimed, in sorted-token order

claimed = pool.claim_position_fees(lower, upper)
print(f"claimed: {claimed.amounts}  {claimed.tx_hash}")

# Or across all of the signer's positions in this pool:
pool.claim_all_position_fees()
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
console.log(await pool.positionFees(lower, upper));   // accrued, unclaimed, in sorted-token order

const claimed = await pool.claimPositionFees(lower, upper);
console.log(`claimed: ${claimed.amounts}  ${claimed.txHash}`);

// Or across all of the signer's positions in this pool:
await pool.claimAllPositionFees();
```

{% endtab %}
{% endtabs %}

### Step 6. Claim AQUA rewards

Reward accrual and claims work the same as for every other pool type:

{% tabs %}
{% tab title="Python" %}

```python
print(pool.pending_rewards())        # accrued AQUA, stroops

claim = pool.claim_rewards()
print(f"claimed {claim.amount} AQUA  {claim.tx_hash}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
console.log(await pool.pendingRewards());   // accrued AQUA, stroops

const claim = await pool.claimRewards();
console.log(`claimed ${claim.amount} AQUA  ${claim.txHash}`);
```

{% endtab %}
{% endtabs %}

### Step 7. Withdraw the position

Partial or full — pass the liquidity to remove, or nothing for the whole position. Per-token minimums come from a simulation of this exact withdrawal, reduced by `slippage`; the payout includes the position's accrued fees:

{% tabs %}
{% tab title="Python" %}

```python
half = pool.withdraw_position(lower, upper, position.liquidity // 2)

closed = pool.withdraw_position(lower, upper)     # full close
print(f"received: {closed.amounts}  {closed.tx_hash}")
```

{% endtab %}

{% tab title="JavaScript" %}

```javascript
const half = await pool.withdrawPosition({ tickLower: lower, tickUpper: upper, liquidity: position.liquidity / 2n });

const closed = await pool.withdrawPosition({ tickLower: lower, tickUpper: upper });   // full close
console.log(`received: ${closed.amounts}  ${closed.txHash}`);
```

{% endtab %}
{% endtabs %}

## Contract-level scripts (any language)

The scripts below run the same lifecycle against the pool contract directly, with the `simulate`/`invoke` helpers spelled out — the reference for languages without an official SDK. Provide the secret key of a funded account and the address of a concentrated pool.

<details>

<summary>Copy the full code Python</summary>

```python
# concentrated_position.py

import math
import time

from stellar_sdk import Address, Keypair, Network, scval, SorobanServer, TransactionBuilder, xdr
from stellar_sdk.soroban_rpc import GetTransactionStatus
from stellar_sdk.xdr import UInt128Parts

# ==========================
# Configuration Variables
# ==========================

SOROBAN_SERVER_RPC = "https://mainnet.sorobanrpc.com"
NETWORK_PASSPHRASE = Network.PUBLIC_NETWORK_PASSPHRASE

# User's secret key (ensure this is kept secure)
USER_SECRET_KEY = "S..."
# Address of the concentrated liquidity pool contract
POOL_ADDRESS = "C..."

server = SorobanServer(SOROBAN_SERVER_RPC)
keypair = Keypair.from_secret(USER_SECRET_KEY)


# ==========================
# Utility Functions
# ==========================

def u128_to_int(value: UInt128Parts) -> int:
   """Converts UInt128Parts from Stellar's XDR to a Python integer."""
   return (value.hi.uint64 << 64) + value.lo.uint64


def scval_map(scv: xdr.SCVal) -> dict:
   """Converts an ScVal map (contract struct) to a Python dict of ScVals."""
   return {entry.key.sym.sc_symbol.decode(): entry.val for entry in scv.map.sc_map}


def price_to_tick(price: float, tick_spacing: int) -> int:
   """Converts a price to the nearest valid (spacing-aligned) tick below it."""
   tick = math.floor(math.log(price) / math.log(1.0001))
   return tick - tick % tick_spacing


def build_tx(function_name: str, parameters: list):
   return (
       TransactionBuilder(
           source_account=server.load_account(keypair.public_key),
           network_passphrase=NETWORK_PASSPHRASE,
           base_fee=1000000,
       )
       .set_timeout(3600)
       .append_invoke_contract_function_op(
           contract_id=POOL_ADDRESS,
           function_name=function_name,
           parameters=parameters,
       )
       .build()
   )


def simulate(function_name: str, parameters: list = None) -> xdr.SCVal:
   """Read-only contract call executed through simulation. No signature needed."""
   simulation = server.simulate_transaction(build_tx(function_name, parameters or []))
   if simulation.error:
       raise RuntimeError(f"{function_name} simulation failed: {simulation.error}")
   return xdr.SCVal.from_xdr(simulation.results[0].xdr)


def invoke(function_name: str, parameters: list) -> xdr.SCVal:
   """State-changing contract call: prepare, sign, send and wait for the result."""
   prepared_tx = server.prepare_transaction(build_tx(function_name, parameters))
   prepared_tx.sign(keypair)
   send_response = server.send_transaction(prepared_tx)

   # Poll until the transaction is included in a ledger
   while True:
       tx_response = server.get_transaction(send_response.hash)
       if tx_response.status != GetTransactionStatus.NOT_FOUND:
           break
       time.sleep(2)

   if tx_response.status != GetTransactionStatus.SUCCESS:
       raise RuntimeError(f"{function_name} failed: {tx_response.result_xdr}")

   transaction_meta = xdr.TransactionMeta.from_xdr(tx_response.result_meta_xdr)
   return (transaction_meta.v4 if transaction_meta.v == 4 else transaction_meta.v3).soroban_meta.return_value


# ==========================
# Position Lifecycle
# ==========================

def run():
   # Step 1. Read the pool state
   slot0 = scval_map(simulate("get_slot0"))
   current_tick = slot0["tick"].i32.int32
   tick_spacing = simulate("get_tick_spacing").i32.int32
   current_price = 1.0001 ** current_tick
   print(f"tick={current_tick} spacing={tick_spacing} price={current_price:.7f}")

   # Step 2. Convert the target price range (±5%) to ticks
   tick_lower = price_to_tick(current_price * 0.95, tick_spacing)
   tick_upper = price_to_tick(current_price * 1.05, tick_spacing) + tick_spacing
   print(f"range: [{tick_lower}, {tick_upper}]")

   # Step 3. Estimate and open the position
   desired_amounts = [100_0000000, 100_0000000]  # raw amounts, sorted token order
   est = simulate("estimate_deposit_position", [
       scval.to_int32(tick_lower),
       scval.to_int32(tick_upper),
       scval.to_vec([scval.to_uint128(a) for a in desired_amounts]),
   ])
   est_liquidity = u128_to_int(est.vec.sc_vec[1].u128)

   result = invoke("deposit_position", [
       scval.to_address(keypair.public_key),
       scval.to_int32(tick_lower),
       scval.to_int32(tick_upper),
       scval.to_vec([scval.to_uint128(a) for a in desired_amounts]),
       scval.to_uint128(est_liquidity * 99 // 100),  # 1% slippage guard
   ])
   deposited = [u128_to_int(v.u128) for v in result.vec.sc_vec[0].vec.sc_vec]
   liquidity = u128_to_int(result.vec.sc_vec[1].u128)
   print(f"deposited {deposited}, received liquidity {liquidity}")

   # Step 4. List positions and check range status
   snapshot = scval_map(simulate("get_user_position_snapshot",
                                 [scval.to_address(keypair.public_key)]))
   for entry in snapshot["ranges"].vec.sc_vec:
       rng = scval_map(entry)
       lower = rng["tick_lower"].i32.int32
       upper = rng["tick_upper"].i32.int32
       status = "IN RANGE" if lower <= current_tick < upper else "OUT OF RANGE"
       print(f"position [{lower}, {upper}]: {status}")

   # Step 5. Check pending swap fees (claim with "claim_position_fees" when non-zero)
   fees = simulate("get_position_fees", [
       scval.to_address(keypair.public_key),
       scval.to_int32(tick_lower),
       scval.to_int32(tick_upper),
   ])
   print("pending fees:", [u128_to_int(v.u128) for v in fees.vec.sc_vec])

   # Step 6. Check pending AQUA rewards (claim with "claim" when non-zero)
   reward = u128_to_int(
       simulate("get_user_reward", [scval.to_address(keypair.public_key)]).u128
   )
   print(f"pending AQUA reward: {reward / 1e7}")

   # Step 7. Withdraw the whole position (principal + accrued fees)
   est = simulate("estimate_withdraw_position", [
       scval.to_address(keypair.public_key),
       scval.to_int32(tick_lower),
       scval.to_int32(tick_upper),
       scval.to_uint128(liquidity),
   ])
   min_amounts = [u128_to_int(v.u128) * 99 // 100 for v in est.vec.sc_vec]

   withdrawn = invoke("withdraw_position", [
       scval.to_address(keypair.public_key),
       scval.to_int32(tick_lower),
       scval.to_int32(tick_upper),
       scval.to_uint128(liquidity),
       scval.to_vec([scval.to_uint128(a) for a in min_amounts]),
   ])
   print("withdrawn:", [u128_to_int(v.u128) for v in withdrawn.vec.sc_vec])


if __name__ == "__main__":
   run()
```

</details>

<details>

<summary>Copy the full code JavaScript</summary>

```javascript
// concentrated_position.js
const StellarSdk = require('@stellar/stellar-sdk');
const {
    Address,
    Contract,
    Keypair,
    Networks,
    TransactionBuilder,
    TimeoutInfinite,
    BASE_FEE,
    nativeToScVal,
    scValToNative,
    rpc,
    xdr,
} = StellarSdk;

// ==========================
// Configuration Variables
// ==========================

const sorobanServerUrl = 'https://mainnet.sorobanrpc.com';

// User's secret key (ensure this is kept secure)
const userSecretKey = 'S...';
// Address of the concentrated liquidity pool contract
const poolAddress = 'C...';

const server = new rpc.Server(sorobanServerUrl);
const keypair = Keypair.fromSecret(userSecretKey);

// ==========================
// Utility Functions
// ==========================

function priceToTick(price, tickSpacing) {
    /** Converts a price to the nearest valid (spacing-aligned) tick below it. */
    const tick = Math.floor(Math.log(price) / Math.log(1.0001));
    return tick - ((tick % tickSpacing) + tickSpacing) % tickSpacing;
}

async function buildTx(functionName, parameters) {
    const account = await server.getAccount(keypair.publicKey());
    const contract = new Contract(poolAddress);
    return new TransactionBuilder(account, {
        fee: BASE_FEE,
        networkPassphrase: Networks.PUBLIC,
    })
        .addOperation(contract.call(functionName, ...parameters))
        .setTimeout(TimeoutInfinite)
        .build();
}

async function simulate(functionName, parameters = []) {
    /** Read-only contract call executed through simulation. No signature needed. */
    const tx = await buildTx(functionName, parameters);
    const result = await server.simulateTransaction(tx);
    if (!result.result) {
        throw new Error(`${functionName} simulation failed`);
    }
    return scValToNative(result.result.retval);
}

async function invoke(functionName, parameters) {
    /** State-changing contract call: prepare, sign, send and wait for the result. */
    const tx = await buildTx(functionName, parameters);
    const preparedTx = await server.prepareTransaction(tx);
    preparedTx.sign(keypair);

    const sendResponse = await server.sendTransaction(preparedTx);

    // Poll until the transaction is included in a ledger
    let txResponse = await server.getTransaction(sendResponse.hash);
    while (txResponse.status === 'NOT_FOUND') {
        await new Promise(resolve => setTimeout(resolve, 2000));
        txResponse = await server.getTransaction(sendResponse.hash);
    }

    if (txResponse.status !== 'SUCCESS') {
        throw new Error(`${functionName} failed: ${txResponse.status}`);
    }
    return scValToNative(txResponse.returnValue);
}

// ==========================
// Position Lifecycle
// ==========================

async function run() {
    // Step 1. Read the pool state
    const slot0 = await simulate('get_slot0');
    const currentTick = slot0.tick;
    const tickSpacing = await simulate('get_tick_spacing');
    const currentPrice = Math.pow(1.0001, currentTick);
    console.log(`tick=${currentTick} spacing=${tickSpacing} price=${currentPrice}`);

    // Step 2. Convert the target price range (±5%) to ticks
    const tickLower = priceToTick(currentPrice * 0.95, tickSpacing);
    const tickUpper = priceToTick(currentPrice * 1.05, tickSpacing) + tickSpacing;
    console.log(`range: [${tickLower}, ${tickUpper}]`);

    const user = new Address(keypair.publicKey()).toScVal();
    const i32 = value => nativeToScVal(value, { type: 'i32' });
    const u128 = value => nativeToScVal(value, { type: 'u128' });
    const u128vec = values => xdr.ScVal.scvVec(values.map(u128));

    // Step 3. Estimate and open the position
    const desiredAmounts = [1000000000n, 1000000000n]; // raw amounts, sorted token order
    const [, estLiquidity] = await simulate('estimate_deposit_position',
        [i32(tickLower), i32(tickUpper), u128vec(desiredAmounts)]);

    const [deposited, liquidity] = await invoke('deposit_position', [
        user,
        i32(tickLower),
        i32(tickUpper),
        u128vec(desiredAmounts),
        u128(estLiquidity * 99n / 100n), // 1% slippage guard
    ]);
    console.log(`deposited ${deposited}, received liquidity ${liquidity}`);

    // Step 4. List positions and check range status
    const snapshot = await simulate('get_user_position_snapshot', [user]);
    for (const { tick_lower, tick_upper } of snapshot.ranges) {
        const inRange = tick_lower <= currentTick && currentTick < tick_upper;
        console.log(`position [${tick_lower}, ${tick_upper}]: ${inRange ? 'IN RANGE' : 'OUT OF RANGE'}`);
    }

    // Step 5. Check pending swap fees (claim with "claim_position_fees" when non-zero)
    const pendingFees = await simulate('get_position_fees',
        [user, i32(tickLower), i32(tickUpper)]);
    console.log(`pending fees: ${pendingFees}`);

    // Step 6. Check pending AQUA rewards (claim with "claim" when non-zero)
    const reward = await simulate('get_user_reward', [user]);
    console.log(`pending AQUA reward: ${Number(reward) / 1e7}`);

    // Step 7. Withdraw the whole position (principal + accrued fees)
    const estAmounts = await simulate('estimate_withdraw_position',
        [user, i32(tickLower), i32(tickUpper), u128(liquidity)]);
    const minAmounts = estAmounts.map(amount => amount * 99n / 100n);

    const withdrawn = await invoke('withdraw_position',
        [user, i32(tickLower), i32(tickUpper), u128(liquidity), u128vec(minAmounts)]);
    console.log(`withdrawn: ${withdrawn}`);
}

run();
```

</details>


# Testing on testnet

Test your Aquarius integration on Stellar testnet before touching real funds.

Aquarius runs a full deployment on Stellar **testnet** — the AMM router, pools, and a dedicated instance of the backend API. Everything in these docs works identically on testnet; only the constants change.

### Setup

* **Constants** — the testnet router, API base URL, and RPC/Horizon endpoints are listed in [Addresses & networks](/developers/reference/addresses-and-networks). The [Quickstart](/developers/quickstart) is pre-configured for testnet.
* **Funding accounts** — testnet XLM is free from friendbot: `https://friendbot.stellar.org?addr=<PUBLIC_KEY>` funds any new account with 10,000 XLM.
* **Test assets** — testnet versions of AQUA, USDC, USDT and others are issued by the shared testnet issuer `GAHPYWLK6YRN7CVYZOO4H3VDRZ7PVF5UJGLZCSPAEIKJE2XSWF5LAGER`. Add a trustline, then acquire them by swapping testnet XLM through the AMM.

### What to expect

* **Thin liquidity.** Testnet pools hold test liquidity — quotes and slippage won't match mainnet. Use testnet to verify your integration logic, not your pricing assumptions.
* **You can create pools.** Pool creation is permissionless on testnet too, so you can set up exactly the pool configuration your tests need.

### Testnet resets

Stellar's testnet is [reset by SDF 2–4 times per year](https://developers.stellar.org/docs/learn/fundamentals/networks#testnet) (17:00 UTC, announced at least two weeks in advance) — the ledger is restored to genesis: all accounts, trustlines, claimable balances, and contract state are wiped. The network passphrase stays the same.

After each reset, Aquarius rebuilds its testnet stack. What survives and what doesn't:

| Survives a reset                                                                                                                                         | Wiped by a reset                                                          |
| -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| **Contract addresses** (router and core contracts) — redeployed deterministically: pinned deployer seed + fixed salts + the unchanged network passphrase | **All accounts, balances, and trustlines** — including your test accounts |
| **Test-asset identities** (`AQUA:GAHP…`) — the issuer keys are kept and re-provisioned                                                                   | **Claimable balances** — votes and locks                                  |
| **Endpoints** — Horizon, Soroban RPC, the testnet API URL, the network passphrase                                                                        | **Pools and their liquidity** — recreated during the rebuild              |
|                                                                                                                                                          | **Backend-indexed data** — re-indexed after the rebuild                   |

{% hint style="warning" %}
The rebuild takes time — right after a reset, expect failures until the stack is back, even though the addresses you're calling are unchanged.
{% endhint %}

If your integration suddenly fails against testnet:

1. Check whether a reset just happened (announced in advance by SDF).
2. Re-fund your test accounts via friendbot and re-create trustlines.
3. If it still fails, the rebuild may not be finished — verify against [Addresses & networks](/developers/reference/addresses-and-networks) or ask in [Discord](https://discord.gg/sgzFscHp4C).


# Reference

Lookup material for Aquarius integrators — API, error codes, and addresses.

Quick-lookup pages for facts you'll return to while building:

{% content-ref url="/pages/oCdlEXPmWdWaaDft6ZHj" %}
[Router & pool contracts](/developers/reference/router-and-pool-contracts)
{% endcontent-ref %}

{% content-ref url="/pages/q4G96jiWcJROPLpbCRuj" %}
[Backend API](/developers/reference/backend-api)
{% endcontent-ref %}

{% content-ref url="/pages/rsfDvqUxvqg0v5IcUGGT" %}
[Error codes](/developers/reference/error-codes)
{% endcontent-ref %}

{% content-ref url="/pages/t5chaM7U6p52rPfg4dTJ" %}
[Addresses & networks](/developers/reference/addresses-and-networks)
{% endcontent-ref %}

For the concentrated-liquidity contract interfaces, see the [Concentrated liquidity contract reference](/developers/concentrated-liquidity/contract-interface-reference).


# Router & pool contracts

Function reference for the Aquarius AMM router and direct pool contract calls

The **router** is the single entry point for all AMM functionality: swaps, deposits, withdrawals, reward claims, and pool discovery. Router addresses for mainnet and testnet are listed in [Addresses & Networks](/developers/reference/addresses-and-networks).

### Read functions

Read-only functions don't change state — call them through transaction **simulation**; no signature or fees are required.

#### Get pools

The `get_pools` function returns all pools that exist for a set of tokens, as a map of pool hash → pool contract address:

```rust
fn get_pools(e: Env, tokens: Vec<Address>) -> Map<BytesN<32>, Address>;
```

| Parameter | Description           |
| --------- | --------------------- |
| `tokens`  | Ordered tokens vector |

See the [Get pools info example](/developers/code-examples/get-pools-info) for usage in Python and JavaScript.

#### Get info

The `get_info` function returns the parameters of a specific pool — pool type, fee, and type-specific values (for example, `a` for stable pools):

```rust
fn get_info(e: Env, tokens: Vec<Address>, pool_index: BytesN<32>) -> Map<Symbol, Val>;
```

| Parameter    | Description                 |
| ------------ | --------------------------- |
| `tokens`     | Ordered tokens vector       |
| `pool_index` | Pool hash (see `get_pools`) |

### Write functions

#### Deposit

The `deposit` function increases pool liquidity by exchanging tokens for pool share tokens:

```rust
fn deposit(
  e: Env,
  user: Address,
  tokens: Vec<Address>,
  pool_index: BytesN<32>,
  desired_amounts: Vec<u128>,
  min_shares: u128,
) -> (Vec<u128>, u128);
```

| Parameter         | Description                                    |
| ----------------- | ---------------------------------------------- |
| `user`            | The address of the user executing the deposit  |
| `tokens`          | Ordered tokens vector                          |
| `pool_index`      | Pool hash (see `get_pools`)                    |
| `desired_amounts` | Vector of desired amounts to deposit           |
| `min_shares`      | Minimum amount of shares to receive on deposit |

**Returns:** the actual amounts of deposited tokens and the minted shares amount.

#### Withdraw

The `withdraw` function removes liquidity from a pool by exchanging pool shares for pool tokens:

```rust
fn withdraw(
  e: Env,
  user: Address,
  tokens: Vec<Address>,
  pool_index: BytesN<32>,
  share_amount: u128,
  min_amounts: Vec<u128>,
) -> Vec<u128>;
```

| Parameter      | Description                                      |
| -------------- | ------------------------------------------------ |
| `user`         | The address of the user executing the withdrawal |
| `tokens`       | Ordered tokens vector                            |
| `pool_index`   | Pool hash (see `get_pools`)                      |
| `share_amount` | Amount of shares to withdraw                     |
| `min_amounts`  | Vector of minimum amounts to withdraw            |

**Returns:** the actual amounts of withdrawn tokens.

#### Swap chained

The `swap_chained` function executes a chain of token swaps to exchange an input token for an output token, with the **input amount fixed** (strict-send):

```rust
fn swap_chained(
    e: Env,
    user: Address,
    swaps_chain: Vec<(Vec<Address>, BytesN<32>, Address)>,
    token_in: Address,
    in_amount: u128,
    out_min: u128,
) -> u128
```

| Parameter     | Description                                                                                                                                                                                                             |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `user`        | The address of the user executing the swaps                                                                                                                                                                             |
| `swaps_chain` | The series of swaps to execute. No need to build it manually — the find-path API returns it as an XDR-encoded `SCVal`. Each element is a tuple of the pool's token vector, the pool index hash, and the token to obtain |
| `token_in`    | The address of the input token to be swapped                                                                                                                                                                            |
| `in_amount`   | The amount of the input token to be swapped                                                                                                                                                                             |
| `out_min`     | The minimum amount of the output token to be received                                                                                                                                                                   |

**Returns:** the amount of the output token received after all swaps have been executed.

#### Swap chained strict receive

The `swap_chained_strict_receive` function is the strict-receive counterpart of `swap_chained`: the **output amount is fixed**, and the function spends no more of the input token than the specified maximum:

```rust
fn swap_chained_strict_receive(
    e: Env,
    user: Address,
    swaps_chain: Vec<(Vec<Address>, BytesN<32>, Address)>,
    token_in: Address,
    out_amount: u128,
    max_in: u128,
) -> u128
```

Parameters mirror `swap_chained`, except:

| Parameter    | Description                                                  |
| ------------ | ------------------------------------------------------------ |
| `out_amount` | The exact amount of the output token to receive              |
| `max_in`     | The maximum amount of the input token you authorize to spend |

**Returns:** the amount of the input token actually spent. See [Executing swaps through optimal path](/developers/code-examples/executing-swaps-through-optimal-path) for a complete example covering both modes.

{% hint style="warning" %}
**Chain length limits:** strict-send chains support up to **4 pools**, strict-receive chains up to **3** — the find-path API enforces these caps. Longer chains raise no contract validation error; the transaction fails simulation with a `BudgetExceed` error due to Soroban limitations.
{% endhint %}

#### Claim

The `claim` function collects accrued AQUA rewards for a liquidity provider:

```rust
fn claim(e: Env, user: Address, tokens: Vec<Address>, pool_index: BytesN<32>) -> u128;
```

**Returns:** the amount of AQUA claimed. See the [Claim LP rewards example](/developers/code-examples/claim-lp-rewards).

### Pool contract functions

Every liquidity pool is a separate contract that can also be called directly — useful for contract sub-invocations, as a direct call requires fewer resources than going through the router. `in_idx`/`out_idx` are the token indices in the pool's sorted token vector:

| Function                                                                                                | Description                                                      |
| ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `estimate_swap(in_idx: u32, out_idx: u32, in_amount: u128) -> u128`                                     | Estimate the output of an exact-input swap (call via simulation) |
| `swap(user: Address, in_idx: u32, out_idx: u32, in_amount: u128, out_min: u128) -> u128`                | Execute an exact-input swap against this pool only               |
| `estimate_swap_strict_receive(in_idx: u32, out_idx: u32, out_amount: u128) -> u128`                     | Estimate the input required for an exact-output swap             |
| `swap_strict_receive(user: Address, in_idx: u32, out_idx: u32, out_amount: u128, in_max: u128) -> u128` | Execute an exact-output swap, spending at most `in_max`          |
| `claim(user: Address) -> u128`                                                                          | Claim accrued AQUA rewards from this pool                        |

See [Executing swaps through specific pool](/developers/code-examples/executing-swaps-through-specific-pool) for a complete example.


# Backend API

The Aquarius backend API — path finding and pool data endpoints, versioning, and error behavior.

The backend API provides off-chain conveniences for integrators: route computation for swaps (returning ready-to-execute XDR) and indexed pool data. It is a free, public API — **no API key or authentication is currently required**. The full machine-readable spec is at [amm-api.aqua.network/api/schema/redoc](https://amm-api.aqua.network/api/schema/redoc/).

|         | Base URL                                               |
| ------- | ------------------------------------------------------ |
| Mainnet | `https://amm-api.aqua.network/api/external/v2`         |
| Testnet | `https://amm-api-testnet.aqua.network/api/external/v2` |

{% hint style="info" %}
**Versioning:** use `v2`. `v1` remains available and is identical except for one endpoint — `v2`'s `find-path-strict-receive` correctly accounts for [provider fees](/developers/code-examples/add-fees-to-swap) when computing the required input.
{% endhint %}

## Endpoints

| Method | Path                         | Purpose                                                  |
| ------ | ---------------------------- | -------------------------------------------------------- |
| `POST` | `/find-path/`                | Best route for an exact-**input** swap (strict-send)     |
| `POST` | `/find-path-strict-receive/` | Best route for an exact-**output** swap (strict-receive) |
| `GET`  | `/pools/`                    | List pools; filter with `?address__in=<addr1>,<addr2>`   |
| `GET`  | `/pools/user/<address>`      | Pools where the given account holds a position           |
| `GET`  | `/statistics/totals/`        | Daily time series: volume, TVL, LP fees, protocol fees   |
| `GET`  | `/statistics/all-time/`      | All-time aggregate volume and TVL                        |

List endpoints are paginated (10 items per page by default; use `?page=N`).

## Find-path request & response

Request body (both endpoints):

| Field               | Type              | Notes                                                                       |
| ------------------- | ----------------- | --------------------------------------------------------------------------- |
| `token_in_address`  | string            | Contract address of the token you send                                      |
| `token_out_address` | string            | Contract address of the token you receive                                   |
| `amount`            | string/number     | In stroops. Input amount for `find-path`, desired output for strict-receive |
| `slippage`          | decimal, optional | for example, `"0.01"` for 1%; default 0                                     |
| `provider_fee`      | decimal, optional | Your integrator fee, for example, `"0.003"`; default 0                      |
| `max_depth`         | int, optional     | Route length cap — up to 4 hops (strict-send) / 3 hops (strict-receive)     |

Response:

| Field                         | Notes                                                                                                  |
| ----------------------------- | ------------------------------------------------------------------------------------------------------ |
| `success`                     | **Always check this.** `false` means no route exists — other fields are zeroed                         |
| `swap_chain_xdr`              | XDR-encoded swap chain, passed directly to the router's `swap_chained` / `swap_chained_strict_receive` |
| `pools`                       | Pool contract addresses along the route                                                                |
| `tokens` / `tokens_addresses` | Assets along the route                                                                                 |
| `amount`                      | Expected output (strict-send) or required input (strict-receive), in stroops                           |
| `amount_with_fee`             | `amount` adjusted for the requested slippage and provider fee                                          |

Example:

```bash
curl -X POST "https://amm-api.aqua.network/api/external/v2/find-path/" \
  -H "Content-Type: application/json" \
  -d '{
    "token_in_address": "CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA",
    "token_out_address": "CDNVQW44C3HALYNVQ4SOBXY5EWYTGVYXX6JPESOLQDABJI5FC5LTRRUE",
    "amount": "100000000",
    "slippage": "0.01"
  }'
```

## Error behavior

* **No route found** → HTTP `200` with `success: false` and zeroed fields. Handle this case explicitly.
* **Invalid input** (malformed token address, bad JSON) → HTTP `400` with field-level validation messages.
* `/statistics/all-time/` → HTTP `404` if no statistics exist yet (relevant on fresh testnet deployments).

See the [swap guides](/developers/code-examples/executing-swaps-through-optimal-path) for the full route → execute flow, and [Error codes](/developers/reference/error-codes) for the on-chain errors that can follow.


# Error codes

Contract error codes returned by the Aquarius AMM router and pools, and how to handle them.

When a contract call fails, the simulation (or transaction) reports a numeric error code, for example, `Error(Contract, #2006)`. The codes below cover the errors an integrator is most likely to encounter.

## Swap & liquidity guards

The errors you should expect and handle in normal operation:

| Code   | Name                  | When it happens                                                                                                            | How to handle                                           |
| ------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `2006` | `OutMinNotSatisfied`  | Exact-input swap: output fell below your `out_min`. Also raised on deposits (`min_shares`) and withdrawals (`min_amounts`) | Re-quote and retry; consider a wider slippage tolerance |
| `2020` | `InMaxNotSatisfied`   | Exact-output swap: required input exceeds your `max_in`                                                                    | Re-quote and retry                                      |
| `2005` | `InMinNotSatisfied`   | Exact-output swap at pool level: output below minimum                                                                      | Re-quote and retry                                      |
| `2018` | `ZeroAmount`          | Zero passed as a swap/deposit amount                                                                                       | Validate input before calling                           |
| `2019` | `InsufficientBalance` | Account balance can't cover the transfer                                                                                   | Check balances before submitting                        |

For multi-hop swaps (`swap_chained`), the slippage guard is enforced **end-to-end** — intermediate hops are unconstrained; only the final output is checked against `out_min`.

## Routing & setup errors

Usually indicate a bug in the integration rather than market conditions:

| Code   | Name              | When it happens                                   | How to handle                                                                                |
| ------ | ----------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `301`  | `PoolNotFound`    | No pool exists for the given tokens + pool hash   | Re-discover pools via `get_pools(tokens)`                                                    |
| `302`  | `BadFee`          | Fee tier not in the allowed set for the pool type | Use 10 / 30 / 100 bps (volatile & concentrated)                                              |
| `307`  | `PathIsEmpty`     | `swap_chained` called with an empty swaps chain   | Always pass the XDR from the Find Path API                                                   |
| `2002` | `TokensNotSorted` | Token vector not sorted by contract address       | Sort with the `order_token_ids` [helper](/developers/code-examples/prerequisites-and-basics) |

## Pause states

Aquarius contracts have independent kill switches for emergency response. If operations are paused you'll see: `205 PoolDepositKilled`, `206 PoolSwapKilled`, `207 PoolClaimKilled` (standard pools), or `2139 GaugesClaimKilled` (concentrated pool incentives). These are not integration bugs — check [Discord](https://discord.gg/sgzFscHp4C) for status.

## Concentrated-pool specific

| Code   | Name                    | When it happens                              |
| ------ | ----------------------- | -------------------------------------------- |
| `2121` | `InsufficientLiquidity` | The swap exhausted all liquidity in the pool |

## API-level failures

The backend API signals differently — see [Backend API](/developers/reference/backend-api): no route is HTTP `200` + `success: false`; invalid input is HTTP `400`.

{% hint style="info" %}
**Parsing results:** since Stellar protocol 23, transaction metadata uses **version 4** (`TransactionMetaV4`). Code that reads `meta.v3.soroban_meta` directly will find `None` — use the version-aware pattern shown in the [code examples](/developers/code-examples), or `meta.value().sorobanMeta()` in JavaScript.
{% endhint %}


# Addresses & networks

Canonical contract addresses and endpoints for Aquarius on mainnet and testnet.

The single source of truth for Aquarius deployment addresses. All values are verified against the production application configuration.

## Mainnet

|                                    | Address / URL                                                                                                                                                          |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **AMM router**                     | [`CBQDHNBFBZYE4MKPWBSJOPIYLW4SFSXAXUTSXJN76GNKYVYPCKWC6QUK`](https://stellar.expert/explorer/public/contract/CBQDHNBFBZYE4MKPWBSJOPIYLW4SFSXAXUTSXJN76GNKYVYPCKWC6QUK) |
| **Provider-fee collector factory** | [`CA4Q2T6FRAFYJYSMDJV7F6B7RL5PS6QS2UOZHBMCT2KSMGQRAAKP2MKO`](https://stellar.expert/explorer/public/contract/CA4Q2T6FRAFYJYSMDJV7F6B7RL5PS6QS2UOZHBMCT2KSMGQRAAKP2MKO) |
| **AQUA asset**                     | `AQUA:GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA`                                                                                                        |
| **Backend API**                    | `https://amm-api.aqua.network/api/external/v2` ([spec](https://amm-api.aqua.network/api/schema/redoc/))                                                                |
| **Soroban RPC**                    | `https://mainnet.sorobanrpc.com`                                                                                                                                       |
| **Horizon**                        | `https://horizon.stellar.org`                                                                                                                                          |

## Testnet

|                                              | Address / URL                                              |
| -------------------------------------------- | ---------------------------------------------------------- |
| **AMM router**                               | `CBCFTQSPDBAIZ6R6PJQKSQWKNKWH2QIV3I4J72SHWBIK3ADRRAM5A6GD` |
| **Test assets issuer** (AQUA, USDC, USDT, …) | `GAHPYWLK6YRN7CVYZOO4H3VDRZ7PVF5UJGLZCSPAEIKJE2XSWF5LAGER` |
| **Backend API**                              | `https://amm-api-testnet.aqua.network/api/external/v2`     |
| **Soroban RPC**                              | `https://soroban-testnet.stellar.org:443`                  |
| **Horizon**                                  | `https://horizon-testnet.stellar.org`                      |
| **Friendbot** (account funding)              | `https://friendbot.stellar.org?addr=<PUBLIC_KEY>`          |

{% hint style="info" %}
**Pool addresses are not listed here** — pools are deployed permissionlessly and discovered at runtime: call `get_pools(tokens)` on the router, or query the [Backend API](/developers/reference/backend-api) `/pools/` endpoint.
{% endhint %}

{% hint style="info" %}
Stellar testnet is wiped 2–4 times per year, but Aquarius redeploys from a pinned deployer seed — the testnet addresses above are expected to persist across resets, and the test-asset issuer keeps its keys. State (pools, balances) is recreated each time; if a value here disagrees with the live deployment right after a reset, the rebuild may still be in progress — see [Testing on testnet](/developers/testing-on-testnet).
{% endhint %}


# Growing liquidity on Aquarius

The path to a liquid asset on Aquarius — Asset Registry whitelisting, pool creation, and the two liquidity tools, bribes and Pool Incentives, compared.

If you issue a token on Stellar, Aquarius is the toolset for making it **liquid** — so that people can buy, sell, and swap it with minimal slippage, and market makers and liquidity providers have a reason to support your markets.

Deep liquidity is what turns a token into a usable asset: tighter spreads, stabler prices, better swap routes across the network, and more confidence for holders and integrators. Aquarius gives projects two complementary tools to get there, both permissionless.

### The path to a liquid asset

1. **Get your asset whitelisted.** AQUA emissions and protocol-level incentives only flow to markets where every asset is approved in the [Asset Registry](/governance/asset-registry). Approval happens through a governance vote — the registry page describes the process and what information to include in your proposal.
2. **Make sure a pool exists.** Anyone can [create a liquidity pool](/user-guides/pools/creating-a-pool) for your asset on Aquarius — volatile, stable, or concentrated, whichever fits your market.
3. **Attract liquidity** with one or both of the tools below.

### Two tools, two angles

Both tools let you spend your own budget to grow liquidity — the difference is who you pay:

* [**Pool Incentives**](/for-projects/pool-incentives) reward **liquidity providers directly**. You fund a reward stream for a specific pool over a period you choose — immediate, precise, and independent of voting outcomes.
* [**Bribes**](/for-projects/what-are-bribes) reward **voters**. You attach a weekly reward to your market; ICE holders vote for it; the votes direct **AQUA emissions** from the protocol to your market's liquidity providers. Bribes have a multiplier quality — your budget attracts votes, and the votes unlock protocol rewards on top.

|                 | Pool Incentives                  | Bribes                                 |
| --------------- | -------------------------------- | -------------------------------------- |
| Who receives it | Liquidity providers in your pool | Voters on your market                  |
| What it unlocks | Direct extra yield in your pool  | Votes → AQUA emissions for your market |
| Minimum size    | 100,000 AQUA-equivalent per day  | 100,000 AQUA-equivalent per week       |
| Cadence         | Any period from 1 day            | Weekly rounds (collected Sundays)      |
| Reward token    | Any token traded on Aquarius     | Any Stellar asset                      |

They combine well: Pool Incentives deliver immediate yield, while bribes build the voting support that sustains AQUA emissions week over week.

### Get started

* [Pool Incentives](/for-projects/pool-incentives) → [Creating Pool Incentives](/user-guides/how-to-create-pool-incentives)
* [Bribes](/for-projects/what-are-bribes) → [Creating Bribes](/user-guides/how-to-create-bribes)


# Pool Incentives

Pool Incentives let projects and third parties fund extra reward streams for liquidity providers in specific Aquarius pools.

{% hint style="info" %}
Pool Incentives are one of two tools for projects growing liquidity on Aquarius — see [Growing Liquidity on Aquarius](/for-projects/for-projects) for the full picture, including how they compare to bribes.
{% endhint %}

## Overview

[Pool Incentives](https://aqua.network/incentives/) allow projects, issuers, DAOs, and ecosystem participants to distribute additional rewards to liquidity providers in specific Aquarius pools.

Unlike [AQUA emissions](/voting-and-rewards/aquarius-amm-rewards), which are funded by the protocol and allocated through governance voting, Pool Incentives are funded directly by third parties. This allows projects to independently incentivize liquidity and support trading activity in markets that are important to their ecosystem.

Pool Incentives operate alongside existing liquidity provider rewards and can be used to complement AQUA emissions or support pools that do not currently receive protocol emissions.

***

### Purpose

Deep and sustainable liquidity is essential for healthy markets. Pool Incentives provide a mechanism for asset issuers and ecosystem participants to encourage liquidity provision without relying on protocol-funded rewards.

Projects commonly use Pool Incentives to:

* Bootstrap liquidity for newly launched assets
* Increase liquidity depth in strategic markets
* Improve trading conditions by reducing slippage
* Encourage long-term liquidity provision
* Support ecosystem growth initiatives

Because incentives are funded directly by participants rather than the protocol, projects have full flexibility in deciding how much liquidity support they wish to provide.

***

### How Pool Incentives work

<figure><img src="/files/6n6pexz93OY9pWqvdNja" alt="Pool incentives diagram"><figcaption></figcaption></figure>

A Pool Incentive campaign distributes rewards to liquidity providers in a specific Aquarius pool over a predefined period of time.

Reward tokens are deposited into the incentive system and distributed automatically to eligible liquidity providers according to their share of liquidity in the incentivized pool. Unlike AQUA rewards, Pool Incentives are not affected by ICE boosts.

Key parameters:

* Minimum incentive size: the equivalent of **100,000 AQUA per day** at creation time, in any token traded on Aquarius
* Minimum duration: **1 day**
* Up to **4 reward tokens** per pool, with up to **10 scheduled incentives per token**

See the step-by-step guide: [How to create pool incentives](/user-guides/how-to-create-pool-incentives).

As long as a campaign remains active, liquidity providers can earn the additional rewards while continuing to receive normal pool trading fees.

Multiple incentive campaigns may exist simultaneously, allowing a pool to receive rewards from several independent sources.

***

### Relationship to AQUA emissions

Pool Incentives and AQUA emissions are separate incentive mechanisms.

AQUA emissions are governed by AQUA holders and distributed according to governance voting outcomes. Pool Incentives are created and funded directly by third parties.

Because the two systems operate independently, a pool may:

* Receive AQUA emissions
* Receive Pool Incentives
* Receive both simultaneously

Pool Incentives do not influence governance voting and do not increase a pool's eligibility for AQUA emissions.

Likewise, governance voting does not determine whether a Pool Incentive campaign can be created.

***

### Asset eligibility requirements

To maintain consistent standards across the protocol, Pool Incentives follow the same asset eligibility requirements as AQUA emissions.

A pool is eligible to receive Pool Incentives only if all assets contained within the pool are approved in the Asset Registry.

This requirement ensures that protocol-supported incentive programs are limited to assets that have undergone governance review and approval.

For more information, see [Asset Registry](/governance/asset-registry).

***

### Permissionless participation

Pool Incentives are designed to remain permissionless.

Any participant may create incentive campaigns for eligible pools. Projects are free to support their own markets, community members can sponsor liquidity programs, and ecosystem participants can direct incentives toward pools they believe provide value to the network.

This model allows market participants to express support for assets and trading pairs without requiring changes to AQUA governance or protocol emissions.

***

### Transparency

All Pool Incentive campaigns are publicly visible and tracked on-chain.

The Aquarius interface displays active incentives, reward assets, distribution rates, and eligibility status directly within the pools interface. This allows liquidity providers to evaluate available opportunities and understand the complete reward profile of a pool before providing liquidity.

Combined with governance-controlled asset eligibility, this creates a transparent and predictable framework for liquidity incentives across the Aquarius ecosystem.


# Bribes

Bribes reward voters who support a specific market, directing AQUA emissions toward it — funded by projects or by the protocol itself.

{% hint style="info" %}
Bribes are one of two tools for projects growing liquidity on Aquarius — see [Growing Liquidity on Aquarius](/for-projects/for-projects) for the full picture, including how bribes compare to Pool Incentives.
{% endhint %}

A **bribe** is a reward attached to a specific Stellar market on [aqua.network/vote](https://aqua.network/vote) to incentivize voters. Voters on bribed markets receive daily payouts for as long as the market stays incentivized.

There are two types of bribes:

* **Protocol bribes** — funded automatically from AMM trading fees, paid to voters on high-volume markets.
* **External bribes** — submitted by anyone to boost votes for a specific pair.

### Who are external bribes for?

Primarily project owners who want AQUA holders to vote for markets related to their token — but any user can create a bribe to influence markets in their interest. Bribed markets consistently attract more votes, since voters earn passive income for supporting them.

* Bribes can be assigned to any Stellar market.
* Since June 2026, AQUA emissions are limited to assets whitelisted in the [Asset Registry](/governance/asset-registry). Creating bribes for non-whitelisted markets remains possible, but won't lead to any AQUA rewards for that market.
* A market can carry bribes in multiple assets at once, though AQUA is the most commonly used token.
* Bribed markets are marked in the UI and collected under the **With Bribes** tab on [aqua.network/bribes](https://aqua.network/bribes).

### How bribes work

1. **Creation.** A claimable balance is created with a marker for the bribed market and the reward tokens used to pay voters.
2. **Collection.** New bribes can be created until 18:00 UTC on Sundays for the following week. At 19:00 UTC on Sundays, Aquarius collects and validates all bribes for the coming week.
3. **Distribution.** Approved bribes are distributed linearly from Monday to Sunday. Snapshots and payouts happen at random times each day to keep things fair — keep your votes on the bribed market around the clock, since adjusting them can mean missing a snapshot.

### Requirements and validity

* Minimum bribe size: **100,000 AQUA per week** (no maximum).
* On collection, Aquarius performs a validity check by purchasing 100,000 AQUA from the offered tokens via a path payment — this ensures bribe tokens hold real value. The purchased AQUA plus the remaining reward tokens are distributed to voters.
* Example: a bribe of 1,000 USDC with AQUA at $0.003 — at collection, 300 USDC converts into 100,000 AQUA, and voters on the market receive the 100,000 AQUA plus the remaining 700 USDC.
* Rejected bribes are refunded to the sender.

### Why use Aquarius bribes?

Bribes level the playing field: any Stellar market can be incentivized, with no infrastructure on your side.

* **No coding** — bribes are created directly from the Aquarius interface; vote tracking and distribution are handled by the protocol.
* **Open participation** — the process is permissionless for projects and individuals alike.
* **Guaranteed payouts** — distribution is automatic; voters who lock their AQUA receive the promised rewards.
* **Transparency** — all bribes are visible on [vote.aqua.network](https://vote.aqua.network), with payout details and schedules.
* **Flexibility** — bribes can be paid in any Stellar asset, including your own token, and a single market can carry several bribe assets at once.

***

* To create a bribe, follow the walkthrough: [Creating Bribes](/user-guides/how-to-create-bribes).
* To earn bribes as a voter, see [Voting for Markets](/user-guides/how-to-vote-for-markets-on-aquarius).


# The AQUA token

AQUA is the core token of Aquarius — used for liquidity voting, locking into ICE, earning rewards, and governance.

<figure><img src="/files/VNLbQWXlsrNJkvfJahsq" alt="The AQUA token symbol and Aquarius branding"><figcaption></figcaption></figure>

AQUA is the protocol token used for ICE locking, liquidity incentives, voting, and governance.

### ICE

Locking AQUA creates [ICE voting balances](/aqua-and-ice/ice-tokens-locking-aqua-and-getting-benefits), including upvoteICE for liquidity voting and governICE for governance.

Holding ICE can increase eligible SDEX and AMM AQUA rewards through the [ICE reward boost](/aqua-and-ice/ice-boosts-how-to-maximize-lp-rewards).

### SDEX & AMM rewards

Market makers and liquidity providers in eligible markets can receive [Stellar DEX (SDEX) rewards](/voting-and-rewards/sdex-rewards) and [Aquarius AMM rewards](/voting-and-rewards/aquarius-amm-rewards).

AQUA can also be used as a [bribe](/for-projects/what-are-bribes) reward asset or exchanged through an Aquarius [swap](/user-guides/swap).

### Liquidity voting

Lock AQUA into ICE, then assign upvoteICE to markets through [liquidity voting](/user-guides/how-to-vote-for-markets-on-aquarius). Votes affect how AQUA emissions are allocated among eligible markets.

### Aquarius Governance

governICE holders can vote on [Aquarius governance](/governance/aquarius-governance-community-led-decision-making) proposals.


# AQUAnomics

AQUA token supply, allocation, and how tokens enter circulation.

## Supply

100 billion AQUA is the total supply, issued once on the Stellar network. This is a hard cap — no more AQUA will ever be created.

> **Token details:**\
> *asset code: **AQUA***\
> *home domain: **aqua.network***\
> *issuer account:* [**GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA**](https://stellar.expert/explorer/public/account/GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA)

## Allocation

| Allocation                               | Share | AQUA           |
| ---------------------------------------- | ----- | -------------- |
| SDEX & AMM rewards                       | 50%   | 50,000,000,000 |
| Airdrops                                 | 20%   | 20,000,000,000 |
| Institutional investors (3-year vesting) | 10%   | 10,000,000,000 |
| Founders & team (3-year vesting)         | 10%   | 10,000,000,000 |
| Voting & staking rewards                 | 5%    | 5,000,000,000  |
| Advisors & partners (3-year vesting)     | 2.5%  | 2,500,000,000  |
| Emergency fund                           | 2.5%  | 2,500,000,000  |

<figure><img src="/files/XJuYbSXdY0KzcHTFUSm2" alt="AQUA allocation chart"><figcaption></figcaption></figure>

Every allocation lives in a public on-chain wallet — all of them are listed [below](#allocation-wallets).

## How AQUA enters circulation

* **Stellar DEX (SDEX) & AMM rewards** — half of the supply is distributed continuously to market makers and liquidity providers, on markets chosen through [liquidity voting](/voting-and-rewards/aquarius-voting).
* **Airdrops** — 20% of the supply was distributed in two [airdrops](/archive/archive), both concluded; unclaimed AQUA went to the community DAO fund. No further airdrops are planned.
* **Vesting allocations** — investors, team, and advisors receive their allocations on 3-year vesting schedules.
* **Voting & staking rewards** — fund programs such as the [Reward Program for Delegates](/aqua-and-ice/overview/reward-program-for-delegates).

## What AQUA is for

* **Directing emissions** — lock AQUA into ICE and [vote for markets](/user-guides/how-to-vote-for-markets-on-aquarius) to decide where SDEX & AMM rewards flow.
* **Locking into ICE** — [freeze AQUA](/aqua-and-ice/ice-tokens-locking-aqua-and-getting-benefits) to boost voting power and LP rewards, and to participate in [governance](/governance/aquarius-governance-community-led-decision-making).
* **Providing liquidity** — deposit AQUA into [pools](/user-guides/pools) to earn trading fees and rewards.

## Allocation wallets

Every wallet in the allocation is public and verifiable on-chain — click any address to inspect its balance and history on a block explorer.

### Rewards

| Wallet                                                                                                                                                      | Original holdings   | Purpose                                                                                                                                                                                                        |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`GBU44GPN…I4L22DOM`](https://stellar.expert/explorer/public/account/GBU44GPNHLW5GSGE4HDOEFLSSF5A6TF4BAPQ7ZWPB7YTZ2F4I4L22DOM) **Liquidity rewards**        | 50,000,000,000 AQUA | Funds [SDEX](/voting-and-rewards/sdex-rewards) & [AMM](/voting-and-rewards/aquarius-amm-rewards) rewards. AQUA is released in small chunks to the two distributor wallets below to minimize distribution risk. |
| [`GC5VEAWX…DGBVANBA`](https://stellar.expert/explorer/public/account/GC5VEAWX7C3GSTW7RUKJKMQWXYZFW5TH4NGI4ZQSF6LNLUYSDGBVANBA) **SDEX rewards distributor** | 0 AQUA              | Distributes SDEX rewards.                                                                                                                                                                                      |
| [`GC6ZWKVY…QTQFQANA`](https://stellar.expert/explorer/public/account/GC6ZWKVYRAUSCLMQYKDQZNTUNVDH2J5J6IELIWHMF7THRUQJQTQFQANA) **AMM rewards distributor**  | 0 AQUA              | Distributes AMM rewards.                                                                                                                                                                                       |
| [`GDLPCKVN…IEBZBTLT`](https://stellar.expert/explorer/public/account/GDLPCKVNQZ3337RDURRV6MCKZATJGPLVCKZQXXGQW6JTITW6IEBZBTLT) **Voting & staking rewards** | 5,000,000,000 AQUA  | Funds voting-related reward programs, including the [Reward Program for Delegates](/aqua-and-ice/overview/reward-program-for-delegates).                                                                       |

### Airdrops & community

| Wallet                                                                                                                                                | Original holdings   | Purpose                                                                                    |
| ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------ |
| [`GDWDKSV2…FUTBM3NC`](https://stellar.expert/explorer/public/account/GDWDKSV247DCZKSLTR3CSC5V6MJT7GFD6TBAN46BKWDNKI7OFUTBM3NC) **Initial airdrop**    | 5,000,000,000 AQUA  | Funded the [Initial Airdrop](/archive/archive/the-initial-airdrop) (concluded).            |
| [`GDFCYDQO…5CIWMYBH`](https://stellar.expert/explorer/public/account/GDFCYDQOVJ2OEWPLEGIRQVAM3VTOQ6JDNLJTDZP5S5OGTEHM5CIWMYBH) **Airdrop #2**         | 15,000,000,000 AQUA | Funded [Airdrop #2](/archive/archive/airdrop-2) (concluded).                               |
| [`GDB3GJCD…SL3MBOXU`](https://stellar.expert/explorer/public/account/GDB3GJCDWLAMVV7ZL6Q7BH3PGVMFWHA5KSRIY2NML3RXX35ESL3MBOXU) **Community DAO fund** | 0 AQUA              | Holds unclaimed airdrop AQUA. AQUA holders decide through governance where these funds go. |

### Vesting & reserves

| Wallet                                                                                                                                                 | Original holdings   | Purpose                                                                     |
| ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- | --------------------------------------------------------------------------- |
| [`GDRGJEPI…RNMMAMAO`](https://stellar.expert/explorer/public/account/GDRGJEPIHDKNRHYS4W7JEHPHW4VX4U3V6QDG573VFN47GURDRNMMAMAO) **Investors**           | 10,000,000,000 AQUA | Institutional investors; 3-year vesting.                                    |
| [`GC25K7RG…ICJZHW5G`](https://stellar.expert/explorer/public/account/GC25K7RGEWCPKBBBLOV4JSUDQ3AQWB6R4A24R7TDRAOGM77KICJZHW5G) **Founders & team**     | 10,000,000,000 AQUA | Founders, team, and future employees; 3-year vesting.                       |
| [`GAOBW72C…D46M65KS`](https://stellar.expert/explorer/public/account/GAOBW72CGZ3VTZJAC63L5NCFURWOYUI4YX6LRVVFNLZR3I7WD46M65KS) **Advisors & partners** | 2,500,000,000 AQUA  | Advisors and partners; 3-year vesting.                                      |
| [`GB3BDPP5…CBINIZFA`](https://stellar.expert/explorer/public/account/GB3BDPP5HOK5U7DGVKBPKKDZ6EWUIUEKFIZAE2ZMCCBJVG6TCBINIZFA) **Emergency fund**      | 2,500,000,000 AQUA  | Reserved for emergencies; also pays [bug bounties](/security/bug-bounties). |


# ICE tokens

Lock AQUA to receive ICE — non-transferable tokens that carry voting power and boost liquidity rewards.

ICE is locked AQUA. Locking converts AQUA into non-transferable ICE tokens that carry liquidity-voting power, governance-voting power, and a [reward boost](/aqua-and-ice/ice-boosts-how-to-maximize-lp-rewards) for liquidity providers. ICE cannot be sent, traded, or deposited anywhere.

<figure><img src="/files/w3ubfXziNjcyNoGV3nwE" alt="The ICE token symbol and Aquarius branding"><figcaption></figcaption></figure>

## How much ICE you get

The amount of ICE depends on how long you lock. The **lock multiplier** grows linearly with the time until unlock and caps at 10x at 1,095 days (3 years):

```
ICE = AQUA locked × (1 + 9 × min(days until unlock / 1095, 1))
```

| Lock duration   | Lock multiplier | 10,000 AQUA locked gives |
| --------------- | --------------- | ------------------------ |
| 3 years or more | 10x             | 100,000 ICE              |
| 2 years         | 7x              | 70,000 ICE               |
| 1 year          | 4x              | 40,000 ICE               |
| 6 months        | 2.5x            | 25,000 ICE               |

The lock multiplier is not the reward boost: locking determines how much ICE you hold, and the up-to-x2.5 [reward boost](/aqua-and-ice/ice-boosts-how-to-maximize-lp-rewards) is what that ICE earns you as a liquidity provider.

## How ICE melts

Your ICE balance is not fixed. The protocol recomputes the expected amount daily from the formula above, and as the unlock date approaches, `days until unlock` shrinks — so ICE decays linearly toward 1x the locked AQUA at the unlock date. Excess ICE is reclaimed from your wallet and from your votes in batches throughout the day.

For example: 10,000 AQUA locked for 2 years starts at 70,000 ICE, melts to 40,000 ICE with one year remaining, and reaches 10,000 ICE on the unlock date — when the AQUA itself becomes reclaimable.

To maintain voting power, extend your lock or lock additional AQUA before melting erodes your balance.

## Locking AQUA

Locking happens at [aqua.network/locker](https://aqua.network/locker) and is executed entirely on the Stellar blockchain: the locker creates a claimable balance that only your wallet can reclaim once the selected lock period expires. Until then, the locked AQUA cannot be retrieved. For the interface walkthrough, see [Locking AQUA into ICE](/user-guides/how-to-use-aqua-locker-tool-and-get-ice-tokens).

## The ICE token family

Locking mints three non-transferable tokens to your wallet:

| Token     | What it does                                                                                                                                                        |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ICE       | Tracks how much AQUA you have locked and determines the distribution of upvoteICE and governICE. No direct operational use.                                         |
| upvoteICE | Votes for markets at [aqua.network/vote](https://aqua.network/vote) — eligibility thresholds are covered in [Aquarius voting](/voting-and-rewards/aquarius-voting). |
| governICE | Votes on [governance proposals](https://aqua.network/governance).                                                                                                   |

{% hint style="info" %}
A fourth token, downvoteICE, was used for downvoting markets. It was deprecated in June 2026 and can still appear in older wallets.
{% endhint %}

Delegation mints two more tokens — dICE and gdICE — covered in [ICE delegation](/aqua-and-ice/overview/tokens-for-delegated-voting-dice-and-gdice).

## Why lock

* **Vote.** Liquidity voting and governance both run on ICE — locking is how AQUA holders get a say. upvoteICE votes can be withdrawn at any time, and governICE is retrievable as soon as a proposal concludes.
* **Vote everywhere at once.** The separate tokens let you participate in liquidity voting and governance simultaneously.
* **Earn more.** Holding ICE [boosts your AQUA rewards](/aqua-and-ice/ice-boosts-how-to-maximize-lp-rewards) as a liquidity provider, up to x2.5.


# ICE boosts

Holding ICE multiplies your AQUA rewards as a liquidity provider — up to x2.5, based on your ICE share versus your pool share.

Liquidity providers who hold ICE receive a **reward boost** on their AQUA rewards, up to x2.5. The boost is determined by two ratios: your share of the total ICE supply and your share of the pool. It is distinct from the up-to-10x [lock multiplier](/aqua-and-ice/ice-tokens-locking-aqua-and-getting-benefits), which determines how much ICE you receive for locking AQUA.

## How the boost is calculated

Aquarius uses the working-balance model known from Curve's veCRV boost:

```
boost = min(0.4 × deposit + 0.6 × pool_liquidity × your_ICE / total_ICE, deposit) / (0.4 × deposit)
```

You reach the full x2.5 when your share of the total ICE supply is at least as large as your share of the pool. A larger pool share with the same ICE lowers the boost toward 1x.

The inputs differ per reward system:

| Reward system              | `deposit`                                | `pool_liquidity`                                                                                             |
| -------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| AMM rewards                | Your LP tokens in the pool               | The pool's total LP tokens                                                                                   |
| Stellar DEX (SDEX) rewards | Your order-book liquidity for the market | Total order-book liquidity for the market, weighted by time on book, spread proximity, and offer fulfillment |

Worked example — suppose the total ICE supply is 50 billion:

* You hold 5 million ICE (0.01% of supply) and provide 0.01% of a pool's liquidity: full **x2.5** boost. A 10% base rewards APY becomes 25%.
* You double your pool share to 0.02% with the same ICE: the boost drops to **x1.75**.

The boost is recalculated hourly and shifts as other providers deposit and withdraw. Claiming rewards refreshes your boost.

## Where the boost appears in the app

The pool list shows each pool's minimum and maximum boost values:

<figure><img src="/files/aDWp450UwxdyKzFqMuzJ" alt="The pools list with minimum and maximum ICE boost values per pool"><figcaption></figcaption></figure>

The deposit flow shows how your boost changes before you confirm:

<figure><img src="/files/225zA0keLv2UokP1i1lI" alt="The deposit dialog showing the boost changing from 1.0 to 1.13"><figcaption></figcaption></figure>

Boosted rewards APY is outlined in purple, with the multiplier shown in blue chips:

<figure><img src="/files/PhZcgZCBOYCx0FfsXAXh" alt="A pool row with boosted rewards APY highlighted and the boost multiplier in a chip"><figcaption></figcaption></figure>


# ICE delegation

Aquarius Delegation makes it easier for ICE holders to stay active in the protocol without having to vote on every decision themselves.

Aquarius Delegation makes it easier for ICE holders to stay active in the protocol without having to vote on every decision themselves. By delegating, you let trusted community members (called **delegates**) use your voting power while you continue to receive all rewards and benefits.

Delegation exists in **two tracks**:

* **Market voting** → using *upvoteICE* to decide which liquidity pools earn AQUA emissions.
* **Governance voting** → using *governICE* to decide on protocol upgrades and DAO proposals.

This system helps keep Aquarius governance efficient, fair, and broadly representative — even if some holders don’t have the time or expertise to vote directly.

### Why delegation matters

Delegation was designed with three main goals in mind:

* **Better experience for holders** – you can support the protocol and earn rewards without active management.
* **Higher utilization of ICE** – less voting power left idle, more decisions reflect the community.
* **Opportunities for active users** – delegates who take on responsibility can earn reputation and, in some cases, extra incentives.

### Roles at a glance

* **Delegators** – ICE holders who assign their voting power to someone else. They keep ownership of their tokens and receive any rewards generated.
* **Delegates** – community members who accept voting power from others and cast votes on their behalf, according to their own strategy or stated commitments.

### Key features

* You can delegate *upvoteICE*, *governICE*, or both.
* You can choose the same delegate for both, or different ones.
* You can **split your voting power between multiple delegates**, giving each a portion of your ICE.
* Delegation is flexible — you can revoke or change it anytime (with a 24-hour cooldown).
* Rewards (like bribes in market votes) always flow back to the delegator, never the delegate.

To learn how to delegate ICE and vote with delegated ICE, please refer to the [interface guide](/aqua-and-ice/overview/user-guide-for-delegation).


# dICE & gdICE

dICE and gdICE represent delegated ICE for markets and governance. They let delegates vote on behalf of holders.

When ICE is delegated, it is represented by special non-transferable tokens inside the Aquarius system. These tokens allow delegates to vote on behalf of others without ever taking ownership of their ICE.

There are two kinds of delegated tokens:

***

### dICE – delegated upvoteICE

* **Purpose**: Market voting
* **What it represents**: The upvoteICE delegated by holders to a delegate.
* **How it’s used**: Delegates use their dICE to vote on which liquidity pools receive AQUA emissions.
* **Rewards**: Any bribes or incentives tied to market votes flow back to the original ICE holder (the delegator), not to the delegate.

***

### gdICE – delegated governICE

* **Purpose**: Governance voting
* **What it represents**: The governICE delegated by holders to a delegate.
* **How it’s used**: Delegates use their gdICE to vote on DAO proposals, parameter changes, or upgrades to the protocol.

***

### Shared properties

* **Non-transferable** – dICE and gdICE cannot be moved or traded; they only exist within Aquarius to reflect delegated power.
* **Minted/Burned automatically** – created when ICE is delegated and removed when delegation is withdrawn.
* **Delegate-only** – visible in the delegate’s balance and usable only for casting votes.
* **Control stays with the delegator** – delegators can revoke or reassign at any time (with a 24-hour cooldown).

***

### Why this design matters

* **Transparency** – anyone can see how much delegated power a delegate holds.
* **Security** – delegates never own the underlying ICE; they only get temporary voting tokens.
* **Flexibility** – by separating dICE and gdICE, delegators can assign different delegates for markets and for governance.

***

### Token addresses

* [dICE on Stellar Expert](https://stellar.expert/explorer/public/asset/dICE-GAXSGZ2JM3LNWOO4WRGADISNMWO4HQLG4QBGUZRKH5ZHL3EQBGX73ICE-1)
* [gdICE on Stellar Expert](https://stellar.expert/explorer/public/asset/gdICE-GAXSGZ2JM3LNWOO4WRGADISNMWO4HQLG4QBGUZRKH5ZHL3EQBGX73ICE)


# User guide for delegation

This guide walks you through the process of delegating ICE in the Aquarius app, from selecting a delegate to monitoring activity.

### Open delegation hub

Go to [aqua.network/delegate](https://aqua.network/delegate).\
You will see a list of suggested delegates.

Each **delegate card** displays:

* **Name** – the delegate’s chosen identifier
* **Description** – short intro or strategy statement
* **Recommended badge** – applied by the Aquarius team to highlight delegates with strong past contributions to Aquarius or the Stellar ecosystem
* **Voting Power** – total ICE delegated to this delegate
* **Trusted by** – number of unique accounts that have delegated ICE to them

<figure><img src="/files/QUjdSruxklsvp8YbLNAU" alt="Image"><figcaption></figcaption></figure>

Click on a delegate card to see more information, including:

* **Voting strategy** – how the delegate approaches market or governance decisions
* **Discord handle** (if provided)
* **Twitter/X profile** (if provided)
* **Vote distribution** – breakdown of votes across different markets
* **Unused voting power** – shown at the bottom of the list
* **DAO voting history** – record of how the delegate voted on past proposals

### Delegate to a selected delegate

To delegate ICE, click the **Delegate** button on the chosen delegate’s card.

\
A popup will appear where you can:

* **Choose the token type** – decide whether to delegate **upvoteICE** (market voting) or **governICE** (governance voting)
* **Enter the amount** to delegate (exact number)
* **Use the slider** to set a percentage of your balance (linked to the input field)
* **Confirm the selected delegate** in the dropdown list

You can repeat this process to split your upvoteICE and governICE across multiple delegates if you wish.

<figure><img src="/files/wrnTMNt1bX5A2CGOWeNb" alt="Delegate select"><figcaption></figcaption></figure>

#### Delegate to a custom delegate

Alternatively, you can delegate ICE to any Stellar address. Please ensure that the address belongs to someone who, to your knowledge, is capable of voting on your behalf.

<figure><img src="/files/aeq9WEjJrrxlD6wqOLYH" alt="Delegate custom"><figcaption></figcaption></figure>

### Monitor delegate activity

You can track delegate activity in the **My Delegates** section. This section displays all the delegates you’ve assigned ICE to.

Click on a delegate preview to:

* View their vote distribution
* Increase the delegated ICE amount
* Undelegate ICE

<figure><img src="/files/uYLT3wCWFtc1BX42Nqiw" alt="Delegate monitor activity"><figcaption></figcaption></figure>

### Undelegate ICE

* You can **revoke delegation** at any time.
* A 24-hour cooldown applies before you can reassign those tokens to a new delegate.
* If you change your mind, repeat the delegation flow with a new address.

During the first 24 hours after delegation the "Claim" option will remain unavailable, as shown in the example screenshot.

<figure><img src="/files/j8xQie2FffC9Tugb4M9s" alt="Undelegate ICE"><figcaption></figcaption></figure>

### For delegates: view delegators

If you are a new delegate receiving ICE for the first time, you’ll be prompted to **add a trustline** to accept [dICE and gdICE](/aqua-and-ice/overview/tokens-for-delegated-voting-dice-and-gdice).

<figure><img src="/files/KSeg1zvBbiqVZy6OVEWS" alt="Delegate view delegators"><figcaption></figcaption></figure>

Once the trustline is set, a new tab will appear: **ICE Delegated to Me**. Here, you’ll see all your delegators and the amounts of ICE each has delegated to you.

<figure><img src="/files/AFyyvNWMUtF3tVoLat9D" alt="Delegate delegators list"><figcaption></figcaption></figure>

### For delegates: voting with dICE and gdICE

If you are a delegate, voting works the same way as if you were using your own ICE.

* When you vote in markets or governance, you’ll see a **dropdown to select which voting power to use**.
* If you hold delegated tokens (dICE or gdICE), they will appear there alongside your own ICE.
* Select them and cast your vote as usual — the weight of all delegated tokens will count towards your choice.


# Bribes & delegation

Delegates can focus on bribes, ecosystem growth, or may stay inactive. Your rewards depend on their approach, so choose delegates whose strategy matches your priorities.

In Aquarius, market votes can be incentivized with **bribes** — extra rewards offered by projects, or by the protocol itself on highly traded markets, to attract ICE votes to specific pools.

When you delegate your **upvoteICE** to a delegate:

* **Bribes always flow back to you, the delegator**, never to the delegate.
* Your earnings depend on how your delegate chooses to vote.

There are **no bribes or rewards** connected to **governICE** delegation. Governance votes are about protocol decisions, not direct incentives.

***

### Why delegation helps

Bribes are not static — they change **week by week**. To maximize rewards on your own, you would need to check markets regularly and update votes on time. Delegation solves this by letting an active delegate handle these updates for you.

***

### Delegate strategies and impact

Different delegates may follow different approaches:

* **Bribe-focused** – aiming to maximize short-term rewards by voting on markets with the highest bribes.
* **Ecosystem-focused** – prioritizing long-term protocol health or liquidity depth, which may not always align with the top bribe markets.
* **Inactive delegates** – if a delegate does not use the delegated ICE, no bribes are earned.

As a delegator, your bribe income can vary widely depending on the delegate’s voting style. Review their activity and choose the delegate whose priorities align with yours.

***


# Reward program for delegates

To recognize active delegates and keep governance strong, Aquarius allocates a monthly reward pool of 10M AQUA to the most trusted voting delegates.

### How it works

* Each month, **10M AQUA** is distributed among the **top 20 whitelisted delegates** listed on [aqua.network/delegate](https://aqua.network/delegate).
* Rewards are calculated based on **voting power**, defined as the total amount of ICE delegated (dICE + gdICE).
* To keep the distribution balanced, we use a **square root formula**:

  `reward ∝ sqrt(voting_power)`

  In plain terms: the more voting power a delegate has, the larger their share of rewards — but the square root formula smooths the difference so smaller delegates get a fairer portion compared to a strictly proportional split.
* Voting power is tracked daily, and the **average monthly balance** is used for the final calculation.

***

### Eligibility

* Only **listed delegates** are eligible.
* Custom delegates or disqualified applicants do **not** qualify.
* Delegates must actively use their **dICE** for voting.
  * If more than **20% of dICE remains unused on a daily average**, the delegate is excluded from that month’s rewards.

***

### Distribution

* Rewards are transferred **directly to delegate wallets**.
* Payouts come from the **Voting Rewards Wallet** (separate from the DAO wallet):\
  [Voting Rewards Wallet on Stellar Expert](https://stellar.expert/explorer/public/account/GDLPCKVNQZ3337RDURRV6MCKZATJGPLVCKZQXXGQW6JTITW6IEBZBTLT)
* The **Signers Guild** processes payments monthly.
* At the beginning of each month:
  * Calculations are shared with all listed delegates.
  * Rewards are distributed by the **10th of the month**.

***

### Why this matters

This program:

* Encourages delegates to stay active and accountable.
* Ensures both small and large delegates are rewarded fairly.
* Strengthens the overall participation in Aquarius governance and market voting.


# Becoming a delegate

Delegation is permissionless, but only whitelisted delegates listed on aqua.network are visible in the app and eligible for monthly rewards.

Delegation in Aquarius is fully **permissionless** — any Stellar address can receive ICE delegations, and dICE or gdICE tokens will be automatically minted to that address when a delegator locks their ICE. This means you don’t need approval from the Aquarius team to act as a delegate.

However, only **whitelisted delegates listed on aqua.network** are featured in the app and eligible for the monthly reward program. The listing is curated so delegators can find trusted options, and so Aquarius can enforce a consistent set of rules for listed delegates.

***

### Two ways to become a delegate

#### 1. custom delegate (permissionless)

* Any Stellar address can accept ICE delegations.
* Delegators can manually enter your address in the delegation interface.
* You will not appear in the public delegate list on aqua.network.
* You are free to promote yourself independently or even build your own delegation hub or tools — such as dashboards, analytics, or other features — to support your community of delegators.
* Custom delegates are **not eligible** for the monthly AQUA reward program.
* **Tip:** Custom delegation can be useful if you manage multiple accounts and want to aggregate their voting power under a single address.

#### 2. whitelisted delegate (recommended)

* To be featured in the public delegate list on [aqua.network/delegate](https://aqua.network/delegate), you must apply.
* Apply here: [aqua.network/delegate/apply](https://aqua.network/delegate/apply).

Before submitting an application, applicants should:

* Review the [**Delegate Code of Conduct**](/aqua-and-ice/overview/delegate-code-of-conduct)
* Review the **Eligibility Criteria** (see below) to ensure they meet the standards expected from listed delegates

Applications are reviewed by the Aquarius team. Onboarding of new candidates and review of applications takes place roughly **once per month**. After each review cycle, the Aquarius team will announce a **summary of application outcomes in the Discord server**.

If approved:

1. The team will contact you to confirm participation.
2. Your profile will appear in the public list with your name, description, social links, voting history.
3. You will become eligible for the **Reward Program for Delegates**.

***

### Why apply?

* **Visibility** – appear in the delegation hub where ICE holders can find you.
* **Rewards** – whitelisted delegates qualify for the monthly AQUA reward program.

***

### Eligibility criteria (for whitelisted delegates)

1. **Discord requirement** – must have a Discord handle and be a member of the Aquarius server for at least **1 month**, with **5+ messages** posted.
2. **Voting history** – demonstrate participation in both DAO governance voting and liquidity (market) voting using the **same wallet** that will be listed as a delegate.
3. **ICE holdings** – hold at least some amount of ICE in the delegate wallet.
4. **Liquidity provision** – maintain at least some LP deposited in the Aquarius AMM to show active involvement.
5. **Agreement** – accept and follow the Delegate Code of Conduct.
6. **Clean record** – no history of prior violations of Aquarius rules (see *Prohibited Behaviour* in the [Code of Conduct](/aqua-and-ice/overview/delegate-code-of-conduct)).


# Delegate code of conduct

All listed delegates are expected to follow a set of principles and rules that ensure fairness, transparency, and accountability in the Aquarius ecosystem.

### Principles for delegates

1. **Act in good faith** — vote with the best interests of Aquarius and its community in mind.
2. **Be transparent** — disclose affiliations, conflicts of interest, and reasoning behind major votes.
3. **Stay accountable** — maintain active participation; if inactive, step aside or inform delegators.
4. **Respect the process** — follow governance rules, deadlines, and treat all members fairly.
5. **Protect integrity** — avoid manipulation, misinformation, or self-dealing that undermines trust.

***

### Community standards

* Maintain respectful behavior in the Aquarius Discord and other official channels.
* No harassment, hate speech, or toxic behavior.
* Participate constructively in governance and community discussions.

***

### Prohibited behavior

1. **Sybil activity** — running duplicate/fake delegates.
2. **Improper incentives** — offering payments or coercion to attract delegations.
3. **Misrepresentation** — providing false info in delegate profile (identity, history, affiliated projects).
4. **Malicious actions** — manipulative voting or other activity that could harm protocol security.
5. **Community misconduct** — harassment, hate speech, or other harmful actions on Discord or other official channels.

***

### Reporting violations

Community members may report suspected violations of the Code of Conduct through the **Aquarius Discord** or other official communication channels. Reports should include any evidence available (such as screenshots or transaction links) so the Aquarius team can investigate fairly.

***

### Enforcement and removal

* Delegates who violate the Code of Conduct may be **removed from the whitelist** and lose eligibility for the **Reward Program for Delegates**.
* The Aquarius team reviews violations during regular delegate review cycles.
* Delegates subject to removal will be informed of the decision and given the chance to **dispute or clarify** before final removal.
* Final decisions are made by the Aquarius team to protect the protocol and community.


# How Aquarius AMMs work

How Aquarius pools price swaps, what liquidity providers earn, the three pool types, and the risks to understand before depositing.

<figure><img src="/files/KxQRiyc8oL5K47X8Qp92" alt="Soroban AQUA"><figcaption></figcaption></figure>

Aquarius AMMs are **automated market makers built as Soroban smart contracts** on the Stellar network. Launched in July 2024, they let anyone swap Stellar assets, provide liquidity, and earn — with no order book and no counterparty: prices come from a formula over the tokens held in each pool.

### How a pool works

A liquidity pool is a smart contract holding two or more tokens. Anyone can trade against it: a swap sends one token into the pool and takes the other out, and the pool's pricing formula moves the price with every trade.

Liquidity providers (LPs) are the other side of the market:

* When you **deposit** tokens into a pool, you receive **LP share tokens** representing your proportion of the pool.
* Every swap pays a **trading fee**, which accrues to the pool — so the value behind each share grows with trading activity.
* When you **withdraw**, your shares are burned and you receive your proportion of the pool's tokens, including accumulated fees.

> The first deposit into an empty pool sets its initial exchange rate, so first depositors should cross-check market prices carefully — see [Deposit & withdraw liquidity](/user-guides/pools/deposit-and-withdraw-liquidity).

### Pool types

Aquarius offers three pool types, each with its own pricing formula:

**Volatile pools** follow the constant product formula (the Uniswap v2 model) and suit any pair of independently priced assets. The pool always holds two tokens, and the price adjusts continuously with supply and demand. Trading fee options at creation: **0.1%, 0.3%, or 1%**.

**Stable swap pools** are designed for assets that should trade close to **1:1** — stablecoins, or wrapped versions of the same asset (for example, ETH and yETH). The stable swap formula concentrates depth around the peg, giving much lower slippage than a volatile pool would for the same liquidity. Stable pools can hold up to three assets, with a customizable trading fee set at creation. Depositing volatile assets into a stable pool is dangerous — the formula assumes the peg and can drain value if prices diverge.

**Concentrated liquidity pools** follow the Uniswap v3 model: each LP chooses a **price range** where their capital is active, multiplying capital efficiency for those willing to manage positions. Fee tiers: 0.1%, 0.3%, or 1%. See the [position management guide](/user-guides/pools/manage-concentrated-liquidity-pool-positions) for the interface and the [developer reference](/developers/concentrated-liquidity) for the contract-level model.

{% hint style="info" %}
Concentrated liquidity pools are still under audit — a [Halborn audit](/security/audits) has been ongoing since June 2026. Manage exposure accordingly.
{% endhint %}

Anyone can [create a pool](/user-guides/pools/creating-a-pool) for any pair of supported tokens. Creating a pool costs **300,000 AQUA**, which discourages spam deployments.

### Where fees go

Trading fees primarily accrue to liquidity providers through the value of their shares. On high-volume markets, a share of AMM trading fees also funds [protocol bribes](/for-projects/what-are-bribes) — rewards that flow back to voters supporting those markets, closing the loop between trading activity and liquidity incentives.

### Earning as a liquidity provider

Trading fees are only the base layer. Aquarius pools can earn up to three additional reward streams:

* **AQUA rewards** — pools whose market is in the [reward zone](/voting-and-rewards/aquarius-voting) (voted above the threshold, all assets whitelisted in the [Asset Registry](/governance/asset-registry)) receive continuous AQUA emissions for their LPs.
* **ICE boost** — holders of [ICE](/aqua-and-ice/ice-boosts-how-to-maximize-lp-rewards) can multiply their AQUA rewards by up to **2.5×**, depending on their ICE balance relative to their pool share.
* **Pool Incentives** — third parties can attach [extra reward streams](/for-projects/pool-incentives) to specific pools, paid in any traded token.

### Risks to understand

* **Impermanent loss.** In a volatile pool, if the two assets' prices diverge after you deposit, withdrawing can return less value than holding the assets — the difference is called impermanent loss. Trading fees and rewards compensate for it; whether they compensate *enough* depends on the market. Stable pools largely avoid it while the peg holds; concentrated positions amplify both fee income and impermanent loss within their range.
* **Position management.** Concentrated positions earn nothing while the market price is outside their range and need active monitoring.
* **Token compatibility.** Aquarius supports standard Stellar and SEP-41 tokens with fixed supply and predictable transfers. Fee-on-transfer, rebasing, and deflationary tokens are not supported, and token contract migrations require manual action — see [System limitations](/amm-and-pools/system-limitations).

### For developers

The AMM is fully accessible on-chain: the [router contract](/developers/reference/router-and-pool-contracts) exposes swaps, deposits, withdrawals, and pool discovery, with runnable [code examples](/developers/code-examples) in Python and JavaScript and a [backend API](/developers/code-examples/get-pools-info) for pool data and swap routing.


# System limitations

Known limitations of the Aquarius AMM and how to work around them.

Like any AMM, Aquarius has boundaries on what token types and scenarios it can safely support. The pages in this section describe known limitations and how to work around them.


# Unsupported token types

Fee-on-transfer, rebasing, and deflationary tokens are not supported by the Aquarius AMM — why, and what happens if they enter a pool.

### Introduction

The Aquarius AMM on the Stellar network supports only standard SEP-41 compliant tokens, which maintain a fixed supply and predictable transfer behavior. Certain non-standard tokens — **fee-on-transfer, rebasing, and deflationary tokens** — are not supported, because their behavior is incompatible with the AMM's internal accounting and liquidity management.

### Unsupported token types

#### Fee-on-Transfer tokens

These tokens automatically deduct a fee on each transfer, so the recipient receives less than the amount sent.

Why they are unsupported: the AMM's balance tracking assumes the entire transferred amount is received. The deducted fee creates a mismatch between recorded and actual balances, which leads to inaccurate pool accounting.

#### Rebasing tokens

Rebasing tokens periodically adjust their total supply, increasing or decreasing balances for all holders based on preset conditions.

Why they are unsupported: the AMM expects token balances to remain stable unless explicitly changed by user actions. Rebasing introduces unexpected balance changes, leading to pricing errors and liquidity imbalances.

#### Deflationary tokens

Deflationary tokens burn a portion of each transfer, gradually reducing total supply over time.

Why they are unsupported: as with fee-on-transfer tokens, the received amount differs from the sent amount, which disrupts the AMM's balance calculations and causes inaccurate pricing and pool valuation errors.

### Impact of adding unsupported tokens

Adding fee-on-transfer, rebasing, or deflationary tokens to Aquarius liquidity pools can cause significant operational and financial issues:

* **Imbalanced pool accounting** — the AMM may fail to accurately track token balances, resulting in pricing errors and liquidity mismanagement.
* **Increased exploitation risk** — discrepancies create arbitrage opportunities where users can manipulate pool imbalances for unfair gains, harming liquidity providers.
* **Financial loss for LPs** — liquidity providers may suffer reduced returns due to inefficient liquidity allocation and inaccurate token pricing.

By ensuring that only compatible tokens are used, Aquarius maintains accurate accounting, fair liquidity management, and a secure trading environment.


# Token address migrations

Aquarius pools cannot follow a token contract to a new address — how liquidity providers handle token migrations.

### Introduction

In DeFi, token contracts occasionally migrate to new addresses due to upgrades, bug fixes, or protocol changes. While such migrations can be necessary, they present challenges for AMMs like Aquarius.

Aquarius liquidity pools are initialized with fixed token addresses and cannot automatically adapt when a token's contract migrates. This page describes the limitation and the recommended way to handle migrations.

### The limitation: fixed token addresses

When an Aquarius liquidity pool is created, the token addresses are hardcoded into the pool's structure. The pool's unique address is derived from a hash of these token addresses. This means:

* Token addresses cannot be changed once a pool is created.
* If a token migrates to a new contract address, the pool cannot recognize or switch to the new token.

#### Risks to liquidity providers

* **Locked value** — liquidity associated with a migrated token stays in the pool as the old token, which may lose value or utility after migration.
* **Trading disruptions** — pools with migrated tokens may see reduced liquidity and trading activity as funds are withdrawn.
* LPs who don't react promptly to a migration may suffer losses if their deposited tokens become obsolete.

Withdrawals always remain functional: an LP who deposited an old token and a second token can still withdraw both even after the migration.

### Handling a migration

When a token migration is detected or announced:

1. **Aquarius notifies LPs** — through UI prompts, email updates, or social media announcements. If a token is known to have frequent upgrades or planned migrations, users may see a warning before creating or adding liquidity to a pool.
2. **The affected pool can be paused** — the pool admin can pause the pool, preventing further deposits or trades involving the obsolete token.
3. **LPs withdraw liquidity manually** from the old pool.
4. **A new pool is created** with the updated token address, and LPs redeposit their liquidity there.

#### Possible future automation

To simplify the process, Aquarius may introduce a migration contract that automatically withdraws liquidity from the old pool, deposits the equivalent liquidity into the new pool, and issues new LP tokens — reducing manual effort for LPs.

#### Coordination with token issuers

The Aquarius team collaborates with token issuers to plan migration events in advance, notify LPs early with step-by-step instructions, and coordinate on strategies that minimize disruption and losses.

### Recommendations for liquidity providers

* **Stay informed** — monitor official channels of both the token issuer and Aquarius, and be aware of tokens likely to migrate due to frequent upgrades or protocol changes.
* **Act during migrations** — withdraw liquidity from the affected pool promptly, then redeposit into the new pool to continue earning rewards.
* **Assess migration risk before providing liquidity** — consider how likely a token is to migrate and plan ahead for possible redeployment.


# Aquarius voting

How ICE holders direct SDEX and AMM rewards by voting for markets — eligibility threshold, ranking, and the reward cap.

Aquarius lets the AQUA-holding community vote on where liquidity incentives should flow across the Stellar order book DEX and the Aquarius AMM. Voting power comes from [locking AQUA into ICE](/aqua-and-ice/ice-tokens-locking-aqua-and-getting-benefits): users place **ICE** votes on the markets they value most, and markets that gather enough votes become eligible for Stellar DEX (SDEX) and AMM rewards.

Learn more about the two reward streams:

* [SDEX Rewards](/voting-and-rewards/sdex-rewards) – distributed automatically every hour to liquidity providers on the Stellar order book.
* [AMM Rewards](/voting-and-rewards/aquarius-amm-rewards) – accrued every block by liquidity providers, claimed manually.

### What makes voting verifiable?

Voting happens entirely on the Stellar blockchain. Each voting transaction and its claimable balance are verifiable on-chain. Voters can later reclaim the ICE from a claimable balance to withdraw an allocation.

### How liquidity voting works

When you select market pairs and commit ICE to them, the voting platform generates transactions containing claimable balances. They are submitted to the Stellar network and processed in the next available ledger (typically within 5 seconds). The allocation remains active until you withdraw it. To change an allocation, reclaim its claimable balance and submit a new vote.

### The incentive loop

Votes, rewards, liquidity, and bribes form one circuit:

```mermaid
flowchart LR
    V["ICE holders"] -->|"vote for markets"| RZ["Reward zone markets<br>(0.5%+ of votes, assets whitelisted)"]
    RZ -->|"AQUA emissions"| LP["Liquidity providers<br>and market makers"]
    LP -->|"provide liquidity"| P["AMM pools and<br>SDEX order books"]
    P -->|"share of trading fees"| PB["Protocol bribes"]
    PB -->|"daily payouts"| V
    EB["External bribes<br>from projects"] -->|"daily payouts"| V
```

Voters direct emissions, emissions attract liquidity, liquidity generates trading fees, and a share of those fees flows back to voters as [protocol bribes](/for-projects/what-are-bribes) — while projects can join the circuit directly with external bribes.

### Reward distribution

For a market to qualify for SDEX and AMM rewards, it must receive at least **0.5% of all votes** cast in liquidity voting, and both assets must be whitelisted in the [Asset Registry](/governance/asset-registry). For example:

* If 1 billion upvoteICE is committed to liquidity voting, a market needs at least 5 million upvoteICE in votes to become eligible.
* At the 0.5% threshold, up to 200 markets can receive AQUA rewards at the same time.

Once eligible, a market's reward share follows its ranking: a market with 5% of total votes earns twice as much as one with 2.5%.

### Reward cap

A single market's daily reward share is capped at **10%**. With 7 million AQUA allocated daily to SDEX and AMM rewards, one market can receive at most 700,000 AQUA per day. Votes beyond the cap don't increase that market's rewards — the excess is not converted into emissions and is not redistributed to other markets. As a result, the total distributed per day can be below the daily allocation.

[Learn how to vote for markets in the step-by-step guide](/user-guides/how-to-vote-for-markets-on-aquarius).


# Asset flag restrictions

Assets with authorization or clawback flags are excluded from market voting and rewards unless governance grants an exception.

By default, some Stellar assets are restricted from participating in Aquarius market voting and rewards.

These assets have specific **flags** set by their issuers, which impose restrictions on holding, sending, and trading. In the case of clawback, issuers can even reclaim tokens from holders.

Assets that have any combination of the `AUTH_REQUIRED`, `AUTH_REVOCABLE`, or `AUTH_CLAWBACK_ENABLED` flags are banned from Aquarius market voting and rewards, as decided by the Aquarius governance process under [Proposal 85](https://aqua.network/governance/proposal/85/).

### Flag definitions

* `AUTH_REQUIRED` – the issuing account must approve other accounts before they can hold the asset.
* `AUTH_REVOCABLE` – the issuing account can revoke an asset's trustline, preventing accounts from accessing it.
* `AUTH_CLAWBACK_ENABLED` – the issuing account can claw back tokens without user consent, typically for regulatory compliance or terms-of-service violations.

### How to lift the flag restriction on an asset

Certain Stellar assets legitimately use these flags for legal and compliance reasons. Because of this, there is a way to request an exception and allow such assets to participate in Aquarius market voting and rewards.

1. Go to [Aquarius Governance](https://aqua.network/governance).
2. Create a governance discussion explaining why the restriction should be lifted. Your post should follow this format:
   * **Title:** Lift Flag Restrictions for *\[asset name]*
   * **Ticker:** *\[asset ticker]*
   * **Asset issuer account:** *\[issuing account]*
   * **Website:** *\[official link]*
   * **Justification:** explain what the asset does and why the flag restrictions should be removed.
3. Submit your discussion for community feedback. After one week, resubmit it as a formal proposal for a community vote.

If the community votes in favor, the Aquarius team will remove the restriction for that asset.


# Reward cycle

When votes are counted and rewards are paid — the continuous, hourly, daily, weekly, and monthly rhythms of Aquarius.

Aquarius runs on several overlapping cycles. Votes are counted continuously, reward allocations refresh daily, Stellar DEX (SDEX) rewards pay hourly, AMM rewards accrue every block, bribes run in weekly rounds, governance votes in weekly slots, and delegate rewards pay monthly. This page shows when each thing happens; the mechanics live on the linked pages.

## The rhythm at a glance

| When (UTC)              | What happens                                                                                                                                                                      |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Continuously            | Votes are tracked; [AMM rewards](/voting-and-rewards/aquarius-amm-rewards) accrue every block; [Pool Incentives](/for-projects/pool-incentives) stream over their campaign period |
| Every hour              | [SDEX rewards](/voting-and-rewards/sdex-rewards) are calculated shortly after the hour and paid out around half past                                                              |
| Once a day              | Reward allocations resync from the latest [vote standings](/voting-and-rewards/aquarius-voting) — at an unpredictable time within the day, so the update moment can't be gamed    |
| Once a day, random time | Each bribed market takes its payout snapshot and pays [bribes](/for-projects/what-are-bribes) to voters                                                                           |
| Sunday 18:00            | Deadline to submit bribes for the following week                                                                                                                                  |
| Sunday 19:00            | Bribes for the coming week are collected and validated                                                                                                                            |
| Monday–Sunday           | Approved bribes are distributed linearly, one payout per day                                                                                                                      |
| Monday–Sunday           | One governance or asset proposal votes per weekly [voting slot](/user-guides/how-to-use-aquarius-governance#proposal-queue-and-voting-slots), Monday 00:00:00 to Sunday 23:59:59  |
| Monthly                 | [Delegate rewards](/aqua-and-ice/overview/reward-program-for-delegates) are paid, computed from daily delegation snapshots                                                        |

## What this means in practice

* **Your vote doesn't move rewards instantly.** Votes are counted as soon as they confirm, but reward allocations refresh once a day — expect a market's rewards to reflect new votes within a day, not within minutes.
* **SDEX rewards arrive hourly, automatically** — your wallet needs an AQUA trustline at payout time, or the payout skips you.
* **AMM rewards accrue every block but are not pushed to you** — claim them in [My Aquarius](/user-guides/dashboard) or through the [claim function](/developers/code-examples/claim-lp-rewards).
* **Bribes snapshot at a random time each day.** Keep your votes on the bribed market around the clock and hold a trustline for the bribe asset — adjusting votes can mean missing that day's snapshot.
* **Bribes operate on whole weeks.** A bribe submitted before Sunday 18:00 UTC starts paying the following Monday; submitted after, it waits an extra week.


# SDEX rewards

How market makers earn hourly AQUA rewards for placing offers on the order books of reward zone markets — and how the reward algorithm weighs offers.

Stellar DEX (SDEX) rewards are earned by placing buy and sell offers on the order books of markets in the reward zone, using SDEX interfaces such as [StellarX](https://www.stellarx.com/) and [StellarTerm](https://stellarterm.com/). For AMM pool rewards, see [AMM Rewards](/voting-and-rewards/aquarius-amm-rewards).

{% hint style="info" %}
To receive rewards, your wallet must hold a trustline to AQUA — without it, rewards cannot be credited.
{% endhint %}

### How the algorithm weighs offers

The reward engine measures **liquidity provided, not volume traded**. Every hour, each offer you held on the order book accrues a weighted contribution:

* **Volume** — the size of the offer.
* **Time on the book** — an offer that sits the full hour counts fully; one that appears briefly counts proportionally.
* **Proximity to the spread** — weight falls off sharply as an offer moves away from the best price, so competitive quotes earn far more than distant ones.
* **Fulfillment** — offers that get filled (fully or partially) count more than offers that never trade.

Your hourly reward is your share of the market's total weighted contribution, paid from that market's slice of the daily allocation. This weighting model was adopted from the community-designed [SDEX v2 algorithm](/archive/archive/sdex-v2-proposal-and-algorithm), built to reward real market making and leave wash trading unpaid: recycling volume between your own accounts scores poorly on time and spread weight, while patient competitive quoting scores well.

### Eligibility and boosts

Markets qualify through [liquidity voting](/voting-and-rewards/aquarius-voting): at least 0.5% of votes with both assets whitelisted in the Asset Registry. Rewards are distributed automatically every hour — see the [reward cycle](/voting-and-rewards/reward-cycle) for exact timing. Holding ICE [boosts your SDEX rewards](/aqua-and-ice/ice-boosts-how-to-maximize-lp-rewards) up to x2.5, the same as for AMM rewards.

### Additional resources

* [Active market pairs for rewards](https://aqua.network/rewards)


# AMM rewards

How liquidity providers in Aquarius AMM pools earn AQUA rewards, and where to claim them.

AMM rewards are earned by depositing liquidity into Aquarius AMM pools. The size of the rewards depends on:

* the number of upvotes a market receives, and
* the amount of liquidity you provide to the pool relative to others.

> **Note:** AMM rewards go to liquidity providers of the Aquarius AMM — see [how to create pools](/user-guides/pools/creating-a-pool) and [deposit liquidity](/user-guides/pools/deposit-and-withdraw-liquidity).

### Markets vs. pools

* A **market** is a trading pair that AQUA and ICE holders vote for, such as XLM/AQUA.
* A **pool** is a specific Aquarius AMM liquidity pool for that market.

A market can have multiple related AMM pools as well as a corresponding pair on the Stellar DEX (SDEX). Total liquidity provider rewards for a market are shared across its related pools and the SDEX based on the total liquidity provided in each.

<figure><img src="/files/6XXoSVKjgH3Q1xbgdc45" alt="A market reward allocation shared among related AMM pools and the SDEX order book"><figcaption></figcaption></figure>

### Distribution and claiming

Rewards are paid in AQUA, so your wallet needs an **AQUA trustline** to receive them.

Rewards accrue every block but must be claimed manually — either in the **My Liquidity** tab under the pools list, or in **My Aquarius**.

### Additional resources

* [Active market pairs for rewards](https://aqua.network/rewards)
* The [voting guide](/user-guides/how-to-vote-for-markets-on-aquarius) for directing rewards to specific markets


# Aquarius Governance

How AQUA holders govern the protocol with governICE votes — the proposal lifecycle, quorum, and approval rules.

Aquarius governance lets governICE holders vote on general protocol proposals and Asset Registry proposals.

### How governance works

* Users vote on proposals using their **governICE** balance — received by [locking AQUA into ICE](/aqua-and-ice/ice-tokens-locking-aqua-and-getting-benefits). The tokens can be claimed back once the voting period ends.
* Governance voting does not offer rewards — it's purely for protocol decision-making.
* All active and past proposals live at [aqua.network/governance](https://aqua.network/governance).
* Governance decides protocol questions (general proposals) and [Asset Registry](/governance/asset-registry) listings and delistings (asset proposals) — both share the same rules and voting queue.

<figure><img src="/files/GpUblRXdgXOHVjnBa5WE" alt="The governance page listing active, upcoming, and completed proposals"><figcaption></figcaption></figure>

When a proposal is active, a vote counter appears in the top menu for quick access:

<figure><img src="/files/BMD4VwGAKlVSvgM9upzo" alt="The active proposal vote counter in the Aquarius navigation bar"><figcaption></figcaption></figure>

### Creating a proposal

Anyone can submit a proposal. Fees apply when creating, editing, and publishing it. The lifecycle is:

1. Once a proposal is created, a discussion thread opens in the Aquarius Discord for community feedback.
2. After the discussion period, the proposal is published into a weekly [voting slot](/user-guides/how-to-use-aquarius-governance#proposal-queue-and-voting-slots) — one proposal votes at a time, and slots can be booked weeks ahead.
3. To be valid, the proposal must reach quorum — the participation requirement and vote counting rules are defined in [Using Aquarius Governance](/user-guides/how-to-use-aquarius-governance#validity-and-quorum).
4. The proposal is approved if it finishes with more "For" than "Against" votes.

Learn more:

* [How to vote in governance](/user-guides/how-to-use-aquarius-governance)
* [Creating a proposal](/user-guides/how-to-use-aquarius-governance/how-to-create-a-proposal)


# Asset Registry

The governance-managed registry that determines which assets are eligible for AQUA emissions and protocol-level pool incentives.

## Overview

The [Asset Registry](https://aqua.network/asset-registry/) is an on-chain governance-managed registry that determines which assets are eligible to receive [AQUA emissions](/voting-and-rewards/aquarius-amm-rewards) and protocol-level [pool incentives](/for-projects/pool-incentives) on Aquarius.

The registry exists to improve the quality and safety of incentivized markets by ensuring that AQUA emissions are directed only toward assets that have been reviewed and approved by AQUA governance.

Asset eligibility is managed entirely through governance. Governance can approve new assets, revoke existing approvals, and determine which assets are eligible to participate in Aquarius incentive programs.

***

### Why the Asset Registry exists

Aquarius distributes AQUA emissions to liquidity providers based on governance voting.

This helps to:

* Improve the quality of incentivized markets
* Reduce the risk of malicious or fraudulent assets receiving emissions
* Protect liquidity providers from avoidable risks
* Ensure AQUA emissions are allocated to assets that have demonstrated community support and transparency

***

### How it works

<figure><img src="/files/XdczrBAFd5GOb25Oy2du" alt="Asset registry page"><figcaption></figcaption></figure>

The Asset Registry maintains a list of approved assets.

An asset becomes eligible only after a successful governance approval vote.

For a liquidity pool to receive AQUA emissions or [Pool Incentives](/for-projects/pool-incentives):

* Every asset in the pool must be approved in the Asset Registry
* Partial eligibility is not supported
* If a pool contains a single non-approved asset, the pool is not eligible for incentives

This rule applies to all pool types, including:

* Volatile pools
* Stable pools
* Multi-asset pools

***

### Default eligible assets

To ensure continuity for core ecosystem markets, the following assets are automatically eligible and do not require governance approval: **XLM, AQUA, USDC**.

These assets are considered foundational ecosystem assets and form the initial set of approved assets within the registry.

All other assets must complete the governance approval process before becoming eligible.

***

### Asset approval process

New assets are proposed through an **asset listing proposal**, created directly in the [Asset Registry](https://aqua.network/asset-registry/) section of the app. Identify the asset by its Stellar code and issuer, or by its Soroban contract address — the asset's existence is verified on-chain when the proposal is submitted.

The application form requires:

* Issuer information
* Token description
* Holder distribution
* Liquidity
* Trading volume
* Audit information
* Stellar asset flags
* Related projects
* Community references
* Existing Aquarius traction
* Issuer commitments

Asset proposals follow the same lifecycle, [fees](/user-guides/how-to-use-aquarius-governance#fees-and-rewards), and quorum as general governance proposals, and vote in the same [weekly voting slots](/user-guides/how-to-use-aquarius-governance#proposal-queue-and-voting-slots). One difference: the creator reward for approved proposals does not apply — an approved asset proposal earns no reward, while the creation and publication fees still apply. One proposal per asset at a time: while a proposal for an asset is pending or voting, a new one for the same asset is rejected.

Approval requires a successful governance vote.

Once approved, the asset is added to the Asset Registry and becomes eligible for AQUA emissions and Pool Incentives.

***

### Asset revocation

Governance may remove an asset from the Asset Registry through an **asset delisting proposal**, created the same way from the Asset Registry section. A delisting proposal requires only the motivation text — the listing application fields do not apply.

Revocation may be considered when:

* New risks emerge
* Project circumstances change
* The issuer acts maliciously
* Governance determines the asset no longer meets community standards

When an asset is revoked:

* The asset is removed from the registry
* Pools containing the asset become ineligible for emissions
* The change takes effect at the start of the next reward epoch

***

### Governance signaling

The introduction of the Asset Registry does not change how governance signaling works.

AQUA holders may continue voting for any market on Aquarius, including markets that contain non-approved assets.

Votes for non-approved assets remain:

* Visible
* Counted
* Tracked by the protocol

However, voting alone does not activate AQUA emissions.

A market only becomes emission-eligible when all assets in the market are approved in the Asset Registry.

This allows governance voting to continue serving as a discovery and signaling mechanism while maintaining eligibility safeguards.

***

### Pool Incentives

Pool Incentives allow projects and third parties to provide additional rewards to liquidity providers.

To align incentive programs with governance standards, Pool Incentives follow the same eligibility rules as AQUA emissions.

A pool may receive Pool Incentives only if all assets in the pool are approved in the Asset Registry.

This creates a consistent incentive framework across the protocol.

***

### External incentives and bribes

External incentives remain permissionless.

Projects may continue to:

* Offer bribes
* Commit future incentives
* Encourage governance participation

However, external incentives do not affect asset eligibility.

Receiving bribes or governance votes does not automatically grant access to AQUA emissions.

Eligibility is determined exclusively through Asset Registry approval.

***

### Registry transparency

Aquarius surfaces [Asset Registry](https://aqua.network/asset-registry/) information directly in the user interface.

Users can view:

* Asset approval status
* Governance voting activity
* Market eligibility status
* AQUA emission eligibility
* Pool Incentive eligibility

This provides a transparent view of how governance decisions affect incentives across the protocol.


# Swap

How to swap one Stellar asset for another in the Aquarius interface and adjust slippage tolerance.

<figure><img src="/files/eEB4lT01HKdXGAwW44aK" alt="The Aquarius swap form with sell and buy asset fields"><figcaption></figcaption></figure>

The [**Swap**](https://aqua.network/swap) section allows you to **exchange one asset for another** using **Aquarius AMMs** as the liquidity source. Only assets included in **Aquarius AMM pools** can be traded.

#### How to swap assets

1. **Select** the asset you want to trade from and the asset you want to receive. Popular assets are pinned on the top and less popular ones can be found using full text search.

<figure><img src="/files/BxeL2FuPtl28GlizbCzC" alt="The asset selector with pinned assets and a search field"><figcaption></figcaption></figure>

2. **Enter** the amount you wish to swap. You can edit either side — enter the Sell amount to see what you'll receive, or the Buy amount to see what it costs, calculated from the best route Aquarius finds. Review the estimate, then click **Swap Assets**.

<figure><img src="/files/w1VZxLa7xlsHTJr23Zz8" alt="The swap form showing an entered sell amount and estimated buy amount"><figcaption></figcaption></figure>

3. **Confirm** the swap details. Select **Confirm Swap** to submit. You can inspect the route's **liquidity pools** by selecting their icons.

<figure><img src="/files/qZ3gu1nvYqTfPXoukjZy" alt="The swap confirmation screen with amounts, route, and minimum received"><figcaption></figcaption></figure>

4. The swap is complete. To view transaction details, click **View on Explorer**.

<figure><img src="/files/PeqqtvKUeo7Pnuz1we3q" alt="A completed swap with a link to the transaction explorer"><figcaption></figcaption></figure>

#### Adjusting slippage tolerance

**Slippage** is the difference between the rate you're quoted and the rate you actually get — it appears when other trades move the pool between your quote and your transaction settling, and it's larger in low-liquidity markets. Your **slippage tolerance** caps how much of it you accept: if the final rate would be worse than your cap, the transaction fails instead of filling at a bad price.

<figure><img src="/files/hhbazX6UIKjx9QvIOphM" alt="The transaction settings button on the swap form"><figcaption></figcaption></figure>

1. Click the **transaction settings** button.
2. Select a **pre-set slippage percentage** (0.1%, 0.5%, or the default 1%) or enter a **custom value** of up to 10%. Tighter values suit stable pairs; wider values help swaps go through in volatile or thin markets, at the cost of a worse possible rate.
3. Once satisfied, click **Save**.

<figure><img src="/files/lFKKOGuQWFFAnKYo2VI4" alt="The slippage settings modal with preset and custom tolerance options"><figcaption></figcaption></figure>


# Pools

The Pools section of aqua.network — analytics, pool type badges, and the pool overview screen.

<figure><img src="/files/uvO4Cc3hAXOXbpuGRsWg" alt="The pools page with aggregate liquidity and trading volume charts"><figcaption></figcaption></figure>

The [**Pools**](https://aqua.network/pools/) section of [aqua.network](https://aqua.network) is the hub for **Aquarius AMMs**.

### Analytics

At the top, you’ll find graphs displaying the **trading volume** across all Aquarius AMMs and the **total liquidity** across all pools. You can adjust the timeframe to view historical trends.

### List of pools

This section allows you to browse all available pools on Aquarius. Use the **search bar** to filter for specific tokens or the **sorting options** in the column headers to organize pools by **TVL (Total Value Locked)**, **volume**, **daily rewards**, **Base (LP) APY**, or **Rewards APY**.

### Pool badges

Pools are labeled with badges to indicate their type:

* **Stable** – The exchange rate remains as close to **1:1** as possible. These pools are designed for stablecoins (**USDC, USDx, etc.**) or assets pegged 1:1 (**for example, ETH and yETH**).
* **Volatile** – Prices in these pools follow the **constant product formula**, adjusting based on supply and demand in swaps. They are designed for **volatile asset pairs**.
* **Concentrated** – Liquidity is provided within a **chosen price range**, earning fees only while the market trades inside it. See [Managing Concentrated Positions](/user-guides/pools/manage-concentrated-liquidity-pool-positions).
* **Reward Zone** – Pools in this category have received enough votes to qualify for **AQUA rewards** for liquidity providers.

<figure><img src="/files/sSUBwMNn28oPuBJwyzjF" alt="Stable, volatile, concentrated, and reward zone badges in the pool list"><figcaption></figcaption></figure>

### Pool overview

<figure><img src="/files/1evynZVjNBzulFW2VnJx" alt="A pool overview with reserves, total shares, liquidity, and volume"><figcaption></figcaption></figure>

Clicking on any market in the **Top Pools** section opens the **Pool Overview**. Here, you can:

* **Deposit & withdraw liquidity**
* **View pool details**, such as type and total shares
* **Check pool statistics**, including **TVL** and **volume**
* **See recent transactions and pool members**


# Creating a pool

How to create a volatile or stable swap pool in the Aquarius interface, including the 300,000 AQUA deployment cost.

Aquarius AMMs enable anyone to create liquidity pools using tokens available on the **Stellar blockchain** or **Soroban smart contract layer**. Three types of pools can be created: **Volatile**, **Stable Swap**, and **Concentrated**.

> **Pool Creation Cost:** 300,000 AQUA

### Pool types

Pool types, their price formulas, fee tiers, and trade-offs are covered in [How Aquarius AMMs Work](/amm-and-pools/what-are-aquarius-amms). In short: volatile pools suit regular asset pairs; stable pools suit assets pegged 1:1 (choosing stable for a volatile pair can lead to loss of deposited funds); concentrated pools are for active liquidity providers working within a chosen price range.

### How to create a pool

Follow these steps to create a liquidity pool on Aquarius:

1. **Select the pool type** (**Volatile**, **Stable Swap**, or **Concentrated**).

<figure><img src="/files/eaB6IzoCDkxqY58YcHeX" alt="Create pool select type"><figcaption></figcaption></figure>

2. **Choose the assets** to include in the pool:

* **Volatile pools**: Up to **two assets**
* **Stable Swap pools**: Up to **three assets** (**four in a future update**)
* **Concentrated pools**: Exactly **two assets**. You'll also set the initial price and range, and acknowledge the risks of concentrated positions — see [Managing Concentrated Positions](/user-guides/pools/manage-concentrated-liquidity-pool-positions).

<figure><img src="/files/NigVx61qNw7UGY7Wcxpn" alt="Create pool choose assets"><figcaption></figcaption></figure>

3. **Set the trading fee** users will pay when swapping assets.

<figure><img src="/files/eIZo9faikGykiMsKe7sQ" alt="Create pool set fee"><figcaption></figcaption></figure>

4. **Acknowledge the 300,000 AQUA fee** required to deploy the pool.
5. Click **Create Pool** to confirm the transaction and launch your pool.


# Deposit & withdraw liquidity

How to deposit liquidity into an Aquarius pool, withdraw it, and claim rewards from the My Liquidity view.

Aquarius allows you to **provide liquidity** to markets available in the **Pools** section. Follow the steps below to **deposit or withdraw liquidity**.

> **Note:** If you are the first to deposit into an **empty pool**, you must manually set the exchange rate between assets. It’s best to deposit a **small amount** initially and **cross-check market rates** from other sources before confirming the transaction.

#### How to deposit or withdraw liquidity

1. Go to [**aqua.network/pools**](https://aqua.network/pools/) and select the **market** where you want to provide liquidity.

<figure><img src="/files/AgyC2QtVlASZqC46LzEu" alt="The pool list with a market selected for liquidity provision"><figcaption></figcaption></figure>

2. Inside the **Pool Overview**, choose **Deposit** or **Withdraw**.

<figure><img src="/files/gQBbtaqsuduVZMsbf5Ux" alt="The Deposit and Withdraw actions on a pool overview"><figcaption></figcaption></figure>

**For Deposits**:

<figure><img src="/files/qSlR5RXiJHuUIY97JCmk" alt="The token amount fields and reward boost shown before a pool deposit"><figcaption></figcaption></figure>

* Enter the **asset amounts** to deposit.
* Review the amounts and select **Deposit**. The **Rewards Boost** value reflects the deposit amounts and your ICE balance; see [ICE boosts](/aqua-and-ice/ice-boosts-how-to-maximize-lp-rewards).

**For Withdrawals**:

<figure><img src="/files/jf8YOlCVZc7Azsbs5NQo" alt="The pool-share percentage selector for a liquidity withdrawal"><figcaption></figcaption></figure>

* Select the **percentage of pool shares** you want to remove.
* Once confirmed, click **Remove** to withdraw your share from the pool.

#### My liquidity

<figure><img src="/files/xxFDZP8kXKRw199yQR41" alt="The My Liquidity tab listing pool positions and claimable rewards"><figcaption></figcaption></figure>

You can view all your liquidity positions under the **“**[**My Liquidity**](https://aqua.network/pools/?tab=my\&filter=all)**”** tab in the **My Aquarius** section. Here, you can:

* View your Aquarius AMM liquidity positions.
* **Add or remove liquidity** from a position.
* **Claim rewards** for one pool or in bulk. A claim can include AQUA rewards, Pool Incentives, and AMM position fees.

Concentrated liquidity pools have a different deposit logic and have a [dedicated page](/user-guides/pools/manage-concentrated-liquidity-pool-positions).


# Managing concentrated positions

How to open, monitor, and rebalance concentrated liquidity positions in the Aquarius interface.

Concentrated liquidity pools can be found on the same page as volatile and stable. One can filter pools by type and view concentrated pools only:

<figure><img src="/files/uU87pUBKhHEz1kdpC1DN" alt="Concentrated pools list"><figcaption></figcaption></figure>

{% hint style="info" %}
Note: concentrated liquidity pools are still under audit — a [Halborn audit](/security/audits) has been ongoing since June 2026.
{% endhint %}

<figure><img src="/files/zcRy7aW3uLr8lyB6QnUW" alt="Concentrated pool page"><figcaption></figcaption></figure>

Concentrated liquidity pools are inspired by the Uniswap v3 primitive and share the general approach - the depositor determines the price range their liquidity should be used in swaps.

{% hint style="info" %}
Prefer managing positions with code? See the [Concentrated liquidity developer guide](/developers/concentrated-liquidity) for the smart contract interface and examples.
{% endhint %}

Before depositing one can view how previously deposited liquidity is distributed on the price range in relation to the market price always positioned in the center of the diagram:

<figure><img src="/files/fniibvUJ0DmPQ00uufQA" alt="Concentrated liquidity distribution"><figcaption></figcaption></figure>

Like on normal deposits a user would specify the amounts of tokens deposited - the amount will be automatically adjusted in relation to the current market price. Besides, a user needs to specify the price range their deposited liquidity will be used in. All the percentage values relate to the current market price:

* Tight. Works for stable and low-volatility pairs. Your liquidity will be concentrated from 0.3% below the current price to 0.3% above it.
* Medium. Works for moderately volatile pairs. Your liquidity will be concentrated from 20% below the current price to 20% above it.
* Wide. Works for volatile pairs. Your liquidity will be concentrated from 50% of the current price to double the current price.
* One-sided up. Works if you believe the price will go up. Your liquidity will be concentrated from the current price to 50% above it.
* One-sided down. Works if you believe the price will go down. Your liquidity will be concentrated from 50% below the current price to the current price.
* Full range. Works like a regular volatile pool. Your liquidity will be spread across the entire available price range.

<figure><img src="/files/gdtbJBqFB9tzDRfdN2f5" alt="Concentrated range presets"><figcaption></figcaption></figure>

Besides using a preset, user can also specify the price range manually in the inputs below. In this case the preset will be unselected.

<figure><img src="/files/Enzb7IUbbHpssOpxUQnw" alt="Concentrated manual range"><figcaption></figcaption></figure>

The values provided (with a preset or manually) will be reflected on the visual diagram below and determine the positions of the vertical controls (marked with an arrow). Actions available with the diagram:

* Move the controls dragging them left and right, see the value reflected in the fields above
* Use `<=` and `=>` arrows to horizontally navigate
* Use `-` and `+` buttons to zoom out and in
* Use "refresh" button to bring the diagram to its pre-edit state

<figure><img src="/files/DcBeRyt1Bz0zKwweVILI" alt="Concentrated range diagram controls"><figcaption></figcaption></figure>

When you're done with setting up the price range proceed further, review the deposit parameters, acknowledge the risk of interacting with an unaudited part of the protocol, and deposit.

<figure><img src="/files/jVf7nEnWkfJCglwyayNZ" alt="Concentrated deposit review"><figcaption></figcaption></figure>

Your concentrated liquidity positions will be reflected on the pool page. To continue earning rewards make sure that your positions are `IN RANGE`. In this case you can deposit more liquidity into the same position by clicking `ADD LIQUIDITY`.

<figure><img src="/files/FmHEmTm68YH2XNpq4WWE" alt="Concentrated positions pool page"><figcaption></figcaption></figure>

In case you have multiple positions they will be stacked together and the `Your liquidity` chart on the right will visually reflect them on the price range.

In case the position is `OUT OF RANGE` it doesn't earn any rewards and the most reasonable thing to do would be removing/withdrawing liquidity and depositing it with another price range.

As a rule of thumb, concentrated liquidity positions should be constantly monitored and managed to avoid capital inefficiency.

<figure><img src="/files/8rVeqq46Z7wme4Qojpda" alt="Concentrated position management"><figcaption></figcaption></figure>


# My Aquarius

The My Aquarius dashboard — balances, reward history, liquidity positions, ICE locks, and vote management in one place.

<figure><img src="/files/FgwSo62QBwgGmBGFE72T" alt="The My Aquarius navigation entry"><figcaption></figcaption></figure>

After signing in, use **My Aquarius** to view balances, rewards, votes, ICE locks, and liquidity positions for the connected wallet.

## Main overview

<figure><img src="/files/j2KGtr2Z5WhknBJCv5Fb" alt="The My Aquarius overview with AQUA, ICE, and XLM balances"><figcaption></figcaption></figure>

You can view your **AQUA, ICE, and XLM balances** in the **main overview** section.

* [**AQUA**](/aqua-and-ice/what-are-aqua-tokens) **Balance:** View AQUA allocated to liquidity, ICE locks, and votes.
* [**ICE**](/aqua-and-ice/ice-tokens-locking-aqua-and-getting-benefits) **Holdings:** View the available balance of each ICE type.
* [**XLM**](/ecosystem-overview/stellar-essentials) **Balance:** Check the XLM available for transaction fees and reserves.

## Balances

<figure><img src="/files/SyroBA1bAhUePqv3gg1p" alt="The wallet asset balances shown in My Aquarius"><figcaption></figcaption></figure>

The Balances section helps you track the assets in your Stellar wallet.

This has been introduced to help with [Aquarius AMMs](/amm-and-pools/what-are-aquarius-amms), allowing you to see what assets you have available to provide liquidity.

## SDEX rewards

<figure><img src="/files/QJ0qedjI5fSU3Gn4Rh0v" alt="The SDEX rewards section with reward amounts by market"><figcaption></figcaption></figure>

The SDEX Rewards section gives you an overview of the rewards you earn by participating in eligible Stellar DEX (SDEX) markets.

## Payments history

<figure><img src="/files/OU7IpKCx4TwQZonYIcaX" alt="The payments history with reward transactions and explorer links"><figcaption></figcaption></figure>

The Payments History section lets you explore your SDEX and AMM reward history and find the payments on stellar.expert.

The history includes recorded [SDEX reward](/voting-and-rewards/sdex-rewards) and [bribe](/for-projects/what-are-bribes) payments received by the wallet.

## My liquidity

<figure><img src="/files/FLZ3IqhD6BVUjxRbJs7X" alt="The My Liquidity section listing current pool positions"><figcaption></figcaption></figure>

The **My Liquidity** section allows you to track all liquidity you’ve provided to [**Aquarius AMMs**](/amm-and-pools/what-are-aquarius-amms), as well as any positions in the **legacy Stellar Classic AMM**.

> **Note:** SDEX market-making orders are not displayed here.

In this section, you can:

* View the **total value** of your liquidity.
* Browse through the **Aquarius AMMs** you’re currently participating in.
* **Add or remove liquidity** from pools.
* **Claim rewards** individually per pool or in bulk.

## ICE locks

<figure><img src="/files/ux53R7VhSUlsWtLbgclC" alt="The ICE Locks section with active and claimable AQUA locks"><figcaption></figcaption></figure>

The ICE Locks section shows which AQUA locks are available for you to claim back and allows you to claim them all in bulk.

## Liquidity votes

<figure><img src="/files/5NJ9dsTdGWCiQ9cIbyVG" alt="The Liquidity Votes section with upvote and reclaim actions"><figcaption></figcaption></figure>

The **Liquidity Votes** section lets you manage the tokens you’ve used to vote for markets you want to be incentivized with rewards.

You can:

* Use the **Manage Unlocked Votes** button to **bulk claim** votes across multiple markets.
* **Upvote or reclaim votes** for specific markets individually by clicking the respective buttons.

## Governance votes

<figure><img src="/files/rjK6vpNnFa3tYnuXEBiy" alt="The Governance Votes section with completed votes available to claim"><figcaption></figcaption></figure>

The Governance Votes section gives you an overview of proposals you have previously voted on where you haven't claimed back votes.

To claim back, select the tick box next to a vote and click "Claim Selected".


# Locking AQUA into ICE

How to lock AQUA into ICE with the Locker tool and verify the resulting balances.

The [Locker tool](https://aqua.network/locker) can be found in the top menu in the **AQUA\&ICE** section. It is used to lock ("freeze") AQUA tokens into ICE — the tokens that carry voting power in Aquarius. More about this logic can be found in the section dedicated to [ICE tokens](/aqua-and-ice/ice-tokens-locking-aqua-and-getting-benefits).

<figure><img src="/files/YaownuwP1FMsLeArmUbw" alt="The AQUA Locker form in the AQUA and ICE section"><figcaption></figcaption></figure>

Enter the amount of AQUA to lock and select an unlock date. A longer lock period produces a higher lock multiplier, up to the maximum for a lock of at least 3 years.

<figure><img src="/files/FTRG0PAJC7IbxloyBsHa" alt="The AQUA amount and unlock date fields with an estimated ICE balance"><figcaption></figcaption></figure>

Review the amount, unlock date, and estimated ICE before building the transaction.

<figure><img src="/files/2xFF0NGCMwTsxQKVAyXI" alt="The lock confirmation screen with AQUA amount, unlock date, and ICE estimate"><figcaption></figcaption></figure>

Confirm the transaction in the connected wallet. The wallet pays the Stellar network fee, and the resulting ICE balances reflect the confirmed lock.

The balances of AQUA and ICE tokens can be seen in the Dashboard:

<figure><img src="/files/xwGkYFnmDN2vxF9M48Nu" alt="AQUA and ICE balances in the My Aquarius dashboard after locking"><figcaption></figcaption></figure>


# Voting for markets

How to find markets, place ICE votes, earn bribes, and withdraw votes in the Aquarius voting interface.

This guide walks through voting for markets in the Aquarius interface. For how liquidity voting works under the hood — on-chain claimable balances, eligibility thresholds, and reward caps — see [Aquarius voting](/voting-and-rewards/aquarius-voting).

Voting requires [ICE](/aqua-and-ice/ice-tokens-locking-aqua-and-getting-benefits): lock AQUA into ICE first, then vote with upvoteICE. Votes can be withdrawn at any time. Delegated dICE votes the same way.

### Search for pairs

Once inside the [voting platform](https://aqua.network/vote), you can start exploring Stellar market pairs. You can use the tabs to view the most popular pairs, top voted pairs, or pairs with bribes. To add a pair to your vote, click **Add To Vote**.

<figure><img src="/files/tr9LnNRWHjn7ljx2fAH3" alt="The liquidity voting page with market search and sorting tabs"><figcaption></figcaption></figure>

You can also use the search bar to find specific pairs, including pairs no one has voted for yet. These will show a **Create Pair** tab next to them. Adding these to your vote will enable others to vote for this pair. Creating pairs requires up to 5 XLM, which gets used to make new voting wallets containing trustlines for the assets in question.

<figure><img src="/files/3x8xeno9Bn56hdG8SAOL" alt="A search result with the Create Pair action for a new voting market"><figcaption></figcaption></figure>

### Selected pairs

As you add and create pairs, they get placed in your voting basket. You can navigate to it anytime by clicking the **Chosen Pairs** bubble in the bottom right corner.

<figure><img src="/files/vVnPdiOWAX5kQRhJzP0R" alt="The Chosen Pairs voting basket with selected markets"><figcaption></figcaption></figure>

Inside the modal window, adjust the amount of ICE you dedicate to voting, then break down how much to allocate to each pair. When you are done, proceed to submitting the transaction.

<figure><img src="/files/GTvxehwIgVCKTcyw3FWK" alt="The voting modal with upvoteICE amounts allocated among selected markets"><figcaption></figcaption></figure>

### Earning bribes

Some markets carry [bribes](/for-projects/what-are-bribes) — rewards offered to voters of that market, paid out daily. Click a market's bribe bubble to see how long the bribe is active and which Stellar assets are offered as incentives.

To qualify for a bribe distribution, assign votes to the bribed market and add trustlines for its reward assets. Eligibility depends on the daily snapshot and the campaign's distribution conditions. Bribed markets are listed under the **With Bribes** tab; to fund a campaign, see [Creating bribes](/user-guides/how-to-create-bribes).

<figure><img src="/files/AORVrFNFaetyg8PxlSYZ" alt="A market&#x27;s bribe details with reward assets and campaign dates"><figcaption></figcaption></figure>

### Adding more votes

To add voting power to an existing allocation, select the market's upvote button. The market returns to the voting basket, where you can enter the additional amount.

<figure><img src="/files/SZPd570P1Q4MlwM5UbRa" alt="The upvote action for adding voting power to an existing market allocation"><figcaption></figcaption></figure>

### Withdrawing votes

To withdraw your votes, go to My Aquarius, open the **My Liquidity Votes** tab and click **Manage unlocked votes**.

<figure><img src="/files/QxOJ9hP7tNl4E7u6lUku" alt="The Manage Unlocked Votes action in My Aquarius"><figcaption></figcaption></figure>

In the modal, select the markets you want to withdraw votes from and click **Claim selected** to build the transaction.

<figure><img src="/files/T1D7y1W4Hq3M756feOZA" alt="The unlocked-vote modal with selected markets ready to claim"><figcaption></figcaption></figure>

### Voting boosts for paired markets

Markets paired with AQUA receive a 50% **voting boost**, and markets paired with USDC or XLM receive 30%. The boosts are cumulative and multiply the market's votes — raising its share of rewards — until the market reaches 10% of adjusted votes.

{% hint style="info" %}
Liquidity providers, not voters, benefit from the boost. See [Deposit & withdraw liquidity](/user-guides/pools/deposit-and-withdraw-liquidity) to provide liquidity.
{% endhint %}

<figure><img src="/files/kAFsvZF1PQMCGWRKeINU" alt="Market rows showing cumulative AQUA, USDC, and XLM voting boosts"><figcaption></figcaption></figure>


# Creating Pool Incentives

How to schedule a Pool Incentive — requirements, limits, and the creation flow at aqua.network/incentives.

This guide covers creating a [Pool Incentive](/for-projects/pool-incentives) — an additional reward stream for liquidity providers of a specific Aquarius pool, funded by you rather than by the protocol. Anyone can create one: projects bootstrapping liquidity for their asset, DAOs, or individual users.

### Before you start

Make sure your planned incentive meets the [requirements](/for-projects/pool-incentives#how-pool-incentives-work): worth at least **100,000 AQUA per day** at creation time (in any token traded on Aquarius), minimum duration of **1 day**, and every token in the target pool approved in the [Asset Registry](/governance/asset-registry). The full parameters and distribution mechanics are covered on the [Pool Incentives](/for-projects/pool-incentives) concept page.

### Creating an incentive

1. Go to [aqua.network/incentives](https://aqua.network/incentives/) and click the **Add Incentive** button at the top of the page.
2. **Select the pool** you'd like to incentivize. Only eligible pools (all tokens whitelisted in the Asset Registry) can be selected.
3. **Choose the reward token** the incentive will be paid in.
4. **Enter the daily reward amount and the duration** (in days). The interface checks that the daily amount meets the 100,000 AQUA-equivalent minimum.
5. Review the parameters and **confirm the transaction** in your connected wallet.

Once submitted, the incentive is scheduled on-chain and distribution starts automatically according to the chosen period.

### How liquidity providers receive it

Rewards accrue automatically to everyone providing liquidity in the incentivized pool, proportionally to their share. Providers claim them in [My Aquarius](/user-guides/dashboard) under **My Liquidity** — Pool Incentives, AQUA rewards, and position fees can be claimed together.

### For developers

Pool Incentives are implemented as a gauge mechanism on the Aquarius AMM smart contract. The relevant entry points on the [router](/developers/reference/router-and-pool-contracts) are `pool_gauge_schedule_reward` (create an incentive), `gauges_get_reward_info` (read accrued incentives), and `gauges_claim` (claim). The current on-chain minimums can be read with `pool_gauge_get_min_daily_amount` and `pool_gauge_get_min_duration`.


# Creating bribes

How to create a bribe for a market — minimum size, the weekly collection schedule, and the creation flow.

This guide walks through creating a bribe for a market in the Aquarius interface. For how bribes work under the hood — collection, validation, and distribution — see [Bribes](/for-projects/what-are-bribes). To learn about earning bribes as a voter, refer to the [voting guide](/user-guides/how-to-vote-for-markets-on-aquarius).

{% hint style="info" %}
Since June 2026 AQUA emission is limited to the assets whitelisted in the Asset Registry. While bribe creation remains open for any market, bribes on non-whitelisted markets won't lead to distributing any AQUA rewards.
{% endhint %}

### Create a bribe

You can access the platform by navigating to [**vote.aqua.network/bribes**](https://vote.aqua.network/bribes)**.** Click the **create bribe +** button.

<figure><img src="/files/uDIAXnm37AvZxH9GPOKF" alt="Bribe create button"><figcaption></figcaption></figure>

As the first step, you need to select a market you wish to create a bribe for:

<figure><img src="/files/JpadK6N0K8V0MNqhv2KP" alt="Bribe select market"><figcaption></figcaption></figure>

Next you'll have to select the asset the bribe is going to be paid in, and the amount. All bribes must be worth a minimum of **100,000 AQUA per week at the time of collection** — Aquarius validates this by [purchasing 100,000 AQUA from the bribe via a path payment](/for-projects/what-are-bribes#requirements-and-validity). Bribes that fall short of the threshold are rejected and refunded.

<figure><img src="/files/vrfcWI5vcyA34PZEFCJi" alt="Bribe select asset amount"><figcaption></figcaption></figure>

Finally, you'll have to set the period for the bribe:

<figure><img src="/files/U3NV60eVvtkSQOOOk3zb" alt="Bribe set period"><figcaption></figcaption></figure>

Bribes run in one-week cycles: every Sunday the Aquarius bribe fund wallet collects the bribes submitted for the upcoming week and distributes them linearly over the chosen week to those voting on your desired market.

Once you are happy everything is filled in correctly, click **create bribe**.

### Confirm transaction

Double check everything is correct before submitting the bribe to the network. Once submitted, you can't edit or cancel the bribe.

Click **add bribe** again to complete your bribe submission.

<figure><img src="/files/gpjYG3VL2tjLh8SypNnE" alt="Bribe confirm transaction"><figcaption></figcaption></figure>

Your bribe is now submitted and will be distributed to voters on the week you chose for the market you selected. If the validity check fails at collection, the bribe is returned to the sender.

{% hint style="info" %}
A market can be incentivized with multiple Stellar assets in the same week, from many different users — bribes from separate creators stack on the same market.
{% endhint %}


# Using Aquarius Governance

The proposal process from discussion to publication — phases, AQUA fees, creator rewards, and the ICE quorum.

Aquarius Governance enables AQUA holders to shape the protocol through proposals and voting. Anyone can submit a proposal for community voting, but keep in mind that proposals lacking clear benefits for the Aquarius or Stellar ecosystem are unlikely to gain support.

### Proposal process

Proposals go through two phases before voting:

**1. Discussion phase (7 days)** — a mandatory discussion period, primarily on the Aquarius Discord server. Proposal creators gather community feedback, refine their ideas, and decide whether to proceed, edit, or abandon the proposal.

**2. Publication phase (up to 30 days)** — after the discussion phase, creators can publish the proposal for voting immediately or continue refining it. Each edit resets both the 7-day discussion and the 30-day publication window. A proposal not published within 30 days of its creation or last edit is marked as finished.

### Fees and rewards

Creating, editing, or publishing a proposal costs AQUA:

* Creation: **100,000 AQUA**
* Editing: **100,000 AQUA** per edit
* Publication: **900,000 AQUA**

All fees are sent to the AQUA issuer wallet, effectively burning them. The fees exist to ensure proposals are well thought out before moving forward.

If a general governance proposal reaches quorum and finishes with more "For" than "Against" votes, the creator earns a **1.5 million AQUA** reward — covering the 1 million AQUA spent on creation and publication, plus a 500,000 AQUA bonus if no edits were made.

The creator reward applies to general proposals only. [Asset listing and delisting proposals](/governance/asset-registry) pay the same creation and publication fees, but an approved asset proposal earns no reward.

### Proposal queue and voting slots

Only one proposal can be in voting at a time — general governance and [asset listing proposals](/governance/asset-registry) share a single queue.

Voting happens in weekly **voting slots**: each slot runs from Monday 00:00:00 UTC to Sunday 23:59:59 UTC and holds exactly one proposal. When publishing, you choose a free slot from the next calendar week up to 6 weeks ahead; the current week cannot be booked. The proposal stays queued until its slot begins, then votes for that full week.

### Validity and quorum

To be valid, a proposal must reach a **20% quorum of the circulating ICE supply** — "For", "Against", and "Abstain" votes all count toward it. For example, with 50 billion ICE in existence, at least 10 billion ICE must participate in the vote.

A proposal with 100% "For" votes but less than 20% participation is automatically rejected. This ensures only widely supported proposals move forward.


# Making a governance vote

How to vote for, against, or abstain on an active governance proposal with governICE.

To access Aquarius Governance, visit [gov.aqua.network](https://gov.aqua.network).

Here you can browse all past and present proposals. To participate, sign in with any of the supported Stellar wallets — the LOBSTR wallet and extension, Freighter, Ledger, or WalletConnect-compatible wallets.

### How to vote

Once signed in, open an active proposal and select **For**, **Against**, or **Abstain**. All three choices count toward quorum. The proposal outcome compares **For** and **Against** votes; **Abstain** does not increase either total.

Voting uses governICE received by [locking AQUA into ICE](/aqua-and-ice/ice-tokens-locking-aqua-and-getting-benefits). The governICE is held in a claimable balance for the duration of the vote and can be reclaimed after the proposal concludes.

<figure><img src="/files/MXMBi2UpJMm8daaH6SyS" alt="An active governance proposal with For, Against, and Abstain voting choices"><figcaption><p>User screen when viewing &#x26; voting for a proposal</p></figcaption></figure>

<figure><img src="/files/ZlIgFfbAQp7LuzrdmgqP" alt="The governance vote confirmation screen showing the selected choice and governICE amount"><figcaption></figcaption></figure>


# Creating a proposal

How to create, discuss, and publish a governance proposal in the Aquarius interface.

Anyone can create a proposal. The creator of a general proposal receives **1,500,000 AQUA** only if the proposal reaches quorum and finishes with more **For** than **Against** votes. Asset Registry proposals do not receive this creator reward.

**1. Start the discussion**

* Click the **Create Discussion +** button on the home page.
* Choose a clear, descriptive title.
* Explain the requested change, its expected effect, and a feasible implementation plan.

**2. Engage with the community on Discord**

* The proposal process includes an option to add a Discord discussion.
* You can ask an Aquarius Discord admin to create a channel before submitting the proposal.
* Once set up, add the channel URL, the discussion channel name, and your Discord nickname to the proposal.

**3. Preview and submit**

* Click **Next** to preview your proposal.
* Once ready, submit it to the discussion phase (see [fees and rewards](/user-guides/how-to-use-aquarius-governance#fees-and-rewards)).

<figure><img src="/files/ZMdLpNVh2ZFFhPSbc7fm" alt="A governance proposal preview with title, content, and discussion details"><figcaption></figcaption></figure>

### Discussion and editing

Each proposal enters a mandatory 7-day discussion period for community feedback. Edits are possible during this phase; each edit creates a new version and resets the phase windows — the mechanics and per-edit fees are covered in the [governance overview](/user-guides/how-to-use-aquarius-governance).

### Publishing and voting

Publication requires the final fee (see [fees and rewards](/user-guides/how-to-use-aquarius-governance#fees-and-rewards)) and books a [voting slot](/user-guides/how-to-use-aquarius-governance#proposal-queue-and-voting-slots): choose a free week from the next calendar week up to 6 weeks ahead. The proposal votes for that full week, Monday 00:00:00 UTC to Sunday 23:59:59 UTC.

<figure><img src="/files/FldheUDB28g3QZAoDGyZ" alt="Proposal edit publish"><figcaption><p>Creators editing &#x26; publishing screen</p></figcaption></figure>

### Implementation

A passing general proposal records the governance decision but does not execute code automatically. Implementation can require separate development and treasury actions. Asset Registry proposals follow the listing or delisting process described in the [Asset Registry](/governance/asset-registry).


# Earning SDEX rewards

How to place order book offers that qualify for hourly SDEX rewards, shown step by step in the LOBSTR app.

The Stellar DEX (SDEX) is Stellar's protocol-level order book for trading classic Stellar assets.

SDEX rewards incentivize placing offers onto order books of markets chosen by AQUA holders through liquidity voting. Every hour market makers get rewarded AQUA tokens based on how much liquidity they have held on the order books, with filled offers gaining higher rewards than those remaining unfulfilled.

Offers are created on the Stellar ledger as follows.

## Creating offers for the Stellar ledger

Offer creation happens through Stellar user interfaces (Stellar UIs). Popular UIs include [LOBSTR wallet](https://lobstr.co/), as well as web clients: [StellarX](https://www.stellarx.com/markets), [StellarTerm](https://stellarterm.com/markets/), [Stellarport](https://stellarport.io/exchange) and [Lumenswap](https://obm.lumenswap.io/market).

The screenshots below use the LOBSTR mobile app.

### How to get to the trade section and select markets

1. Open LOBSTR and select the menu control in the top-left corner.
2. Select **Trade**.
3. Select a market.

<figure><img src="/files/PNaCa5HCysgMQ23Gxf0J" alt="The LOBSTR Trade section with a list of SDEX markets"><figcaption></figcaption></figure>

### How to view the current order book, decide your buy/sell price and submit an order

1. Open the **Orderbook** tab on the market page.
2. Review the current buy and sell offers.
3. Select **Buy** or **Sell** to open the order form.
4. Select **Limit Order**.
5. Enter a price and amount you are willing to trade. Offers closer to the spread generally receive a higher reward score but are also more likely to execute. Do not submit an offer unless you are prepared for it to be filled partially or in full.

<figure><img src="/files/dPksFSPQXgb9YyXqSxqo" alt="A LOBSTR order book with buy and sell offers and the limit order form"><figcaption></figcaption></figure>

### After order submission

1. Review the order parameters and submit the transaction.
2. Return to the **Orderbook** to confirm that the offer is open.
3. Monitor the offer for partial or full fills, and cancel it if you no longer want the remaining amount to trade.

<figure><img src="/files/wEzNZrCI5hbdPuXA0TGz" alt="LOBSTR confirming that a limit order was submitted to the SDEX"><figcaption></figcaption></figure>

An eligible offer contributes to the hourly reward calculation while it remains on the order book. The score depends on its time on the book, distance from the spread, and fulfillment. Placing an offer does not guarantee a reward, and execution can create market and inventory risk. See [SDEX rewards](/voting-and-rewards/sdex-rewards) for the calculation factors.


# Audits

Completed and ongoing audits, and notable bug bounty fixes

## Audits

### Aquarius AMM audit by CoinFabrik - March 2024

The Aquarius Foundation engaged CoinFabrik to conduct an audit of Aquarius smart contracts in January 2024, which was completed in April 2024. The Aquarius team reviewed and addressed all feedback provided during the audit process.

{% file src="/files/O3LXU55MeM4fCiqdctzB" %}

### Aquarius AMM audit by Certora - December 2024

The Aquarius Foundation engaged Certora to conduct a new audit of Aquarius smart contracts in June 2024, which was completed in December 2024. The Aquarius team reviewed and addressed all feedback provided during the audit process.

{% file src="/files/4H2fsEXFPFWV2pkXG4dz" %}

### Competitive Aquarius AMM audit by Cantina - May-June 2025

Cantina coordinated 33 independent researchers to [audit](https://cantina.xyz/portfolio/81e6d203-8a65-49a0-b581-a5fe2b5c0a13) Aquarius smart contracts. The competitive audit lasted for 2 months. All the identified high-risk issues, as well as the most of the medium-risk ones were fixed by Aquarius team.

{% file src="/files/9KXWbk1PERbjlQwFeKPJ" %}

### Concentrated liquidity audit by Halborn - ongoing

The Aquarius Foundation engaged Halborn in June 2026 to audit the Aquarius concentrated liquidity pool smart contracts. The audit is ongoing; the report will be published here once it is completed and all findings are addressed.

## Bug bounty fixes

### Potential duplication of LP rewards - September 2024

{% file src="/files/IlR20fpyFFMnXdPOtic0" %}


# Bug bounties

Receive AQUA for helping us squash bugs

Part of keeping Aquarius’ constant growth is ensuring the protocol is operational, reliable, and consistently performing to the highest standards. Now and then, a bug inside the code or loopholes can cause issues, creating vulnerabilities to the Aquarius protocol.

**Bug bounties reward those who find & raise vulnerabilities with the team, allowing fixes to be deployed and safeguarding Aquarius.**

**We have an allocated Bug Bounty fund tied to the** [**emergency fund**](https://stellar.expert/explorer/public/account/GB3BDPP5HOK5U7DGVKBPKKDZ6EWUIUEKFIZAE2ZMCCBJVG6TCBINIZFA), **which we** **use to reward those who find vulnerabilities.**

## What bugs can result in rewards?

Reward considerations apply to most bugs found that can negatively impact Aquarius. We pay bounties at our discretion, with reward values depending on the severity & complexity of the issue.

While we can consider a lot of different issues for a bounty, the following issues would not come under our scope:

* Bugs in any third party platform that interacts with Aquarius
* Vulnerabilities already reported and/or discovered by the team or advisors
* Any already-reported bugs by others in the community

Vulnerabilities that occur due to any of the following are also outside of the bug bounties scope:

* Front end bugs
* DDOS attacks
* Spamming
* Phishing
* Compromise or misuse of third-party systems or services.

## How should I report potential bugs?

Any vulnerability or bug discovered should be reported via private message to any of the admins of the Telegram, Discord, or Reddit channels or our bug reporting email address **<security@aqua.network>**.

**The vulnerability must not be disclosed publicly or to any other person, entity, or email address before Aquarius has been notified and a fix deployed.** The disclosure of a bug must be made preferably within 24 hours following its discovery. Once fixed, permission will be granted for public disclosure.

The more detailed a vulnerability report, the higher the likelihood of a reward and its value. Please provide as much information about the vulnerability as possible, including:

* What conditions cause the bug to occur
* The steps needed to reproduce the bug or, preferably, a proof of concept.
* The potential implications of the vulnerability being abused.

Anyone who reports a unique, previously unreported, and publicly undisclosed vulnerability that results in a deployed fix by our developers will be recognized publicly for their contribution if they so choose.

## Eligibility

To be eligible for a reward under this Program, you must:

* Discover a previously unreported, non-public vulnerability that would result in loss of user’s funds or abuse of the Aquarius protocol, which is within the scope of this Program.
* Be the first to disclose the unique vulnerability to the Aquarius team in compliance with the disclosure requirements above. If multiple users report similar vulnerabilities within 24 hours, rewards will be split at the discretion of Aquarius.
* Provide sufficient information to enable our developers to reproduce and fix the vulnerability.
* Not engage in any unlawful conduct when disclosing the bug to Aquarius, including through threats, demands, or any other coercive tactics.
* Not exploit the vulnerability in any way, including making it public or obtaining a profit (other than a reward under this Program).
* Make a good faith effort to avoid privacy violations, data destruction, interruption, or degradation of the Aquarius protocol.
* Submit only one vulnerability per submission unless you need to chain vulnerabilities to provide impact regarding any of the vulnerabilities.
* Not separately submit underlying vulnerabilities caused by a known issue already considered for a bug bounty.
* Be at least 18 years of age or, if younger, submit your vulnerability with the consent of your parent or guardian.
* Not be subject to US sanctions or reside in a US-embargoed country.
* Not be one of our current or former employees, vendors, contractors, or employees of any of those vendors or contractors.
* Comply with all the eligibility requirements of the Program.

Other Terms

By submitting your report, you grant Aquarius all rights, including intellectual property rights, needed to validate, mitigate, and disclose the vulnerability. All reward decisions, including eligibility, reward amounts, and how such rewards will be paid, are made at our discretion.

Aquarius may alter the terms and conditions of this Program at any time.


# Stellar essentials

The Stellar basics behind Aquarius — trustlines, network reserves, fees, lumens, and Soroban.

Aquarius is built on the Stellar network and its Soroban smart contract platform. This page collects the Stellar basics you'll run into while using Aquarius — trustlines, reserves, fees, and the native XLM token.

### What is Stellar?

[Stellar](https://www.stellar.org/) is a decentralized, public blockchain focused on currencies and payments. It is fast, cost-effective, and energy-efficient, and it enables the creation, transfer, and trading of digital representations of all types of assets — dollars, bitcoin, or virtually anything else. Assets issued by **anchors** (regulated entities that connect Stellar to traditional finance) can be redeemed for their real-world counterparts.

### What is Soroban?

[Soroban](https://developers.stellar.org/docs/smart-contracts) is the smart contracts platform on the Stellar network. Contracts are small programs written in Rust and compiled to WebAssembly for deployment. Aquarius uses Soroban to power its AMMs and other products in the ecosystem.

### Lumens (XLM) and network fees

The **lumen (XLM)** is Stellar's native token. Every account needs a small amount of XLM to exist on the network, create trustlines, and pay transaction fees. Fees start at 0.00001 XLM per operation; during rare surges of network activity, transactions offering higher maximum fees are processed first — the network only ever charges what is required.

### Trustlines

When you hold an issued asset on Stellar, you hold credit from its issuer — and your account must explicitly opt in to each asset by creating a **trustline**. Trustlines protect you from receiving random, unwanted assets.

You will meet trustlines constantly on Aquarius: an **AQUA trustline is required to receive any rewards**, bribes arrive only for assets you hold trustlines to, and ICE tokens are delivered via trustlines set up by the locker.

### Network reserves

Your wallet locks small amounts of XLM as reserves while you use Stellar and Aquarius features:

* An active account reserves 1 XLM
* Each trustline reserves 0.5 XLM
* Each order book offer reserves 0.5 XLM
* Each additional signer reserves 0.5 XLM
* Each claimable balance for Aquarius voting & governance reserves 1 XLM
* Each claimable balance created by the AQUA locker reserves 0.5 XLM

Reserved XLM returns to your balance when you close trustlines, claim back balances, fill or cancel orders, or remove signers. Keep some spare XLM in your wallet so reserves never block you from voting, locking, or placing orders.

### Claimable balances

Claimable balances are the Stellar feature Aquarius voting and locking are built on: a payment split into a "send" and a "claim" part, with time conditions attached. When you vote or lock AQUA, your tokens sit in a claimable balance — on-chain, reclaimable only by you once the lock period ends. Wallets display these as pending payments.

For the full mechanics, see the [Stellar documentation on claimable balances](https://developers.stellar.org/docs/learn/encyclopedia/transactions-specialized/claimable-balances).


# Archive

Historical materials — concluded airdrops, the Signers Guild announcement, and superseded design documents.

Historical materials kept for reference: concluded airdrops, the original Signers Guild announcement, and early protocol design documents that have since been superseded.


# The Initial Airdrop

The 2021 Initial Airdrop — eligibility, five-phase distribution, and claim rules. Concluded.

{% hint style="info" %}
This page is kept for historical reference. The Initial Airdrop took place in 2021–2022 and has fully concluded — unclaimed AQUA was returned to the Community DAO fund.
{% endhint %}

<figure><img src="/files/tFUWoRSjEO7cQDdBRYYO" alt="The five Initial Airdrop distribution dates and collection deadlines from August 2021 to January 2022"><figcaption></figcaption></figure>

### Who was eligible?

Every Stellar wallet that completed at least one trade (an offer that was filled partially or fully) on the SDEX before the snapshot date of January 1, 2021, 00:00:00 UTC, was eligible. Wallets that "only" used swap or path payment functionality before the snapshot were not eligible for this airdrop.

You could check your eligibility at [airdrop.aqua.network](https://airdrop.aqua.network/).

### How was it distributed?

AQUA tokens were delivered to eligible wallets in 5 phases using [claimable balances](/ecosystem-overview/stellar-essentials#claimable-balances) on the Stellar network.

Each of the 5 claimable payments had to be collected by the eligible wallet within a 30-day claim period. Payments left unclaimed after their 30-day period were collected back to the Community DAO Fund. The [airdrop wallet](https://stellar.expert/explorer/public/account/GDWDKSV247DCZKSLTR3CSC5V6MJT7GFD6TBAN46BKWDNKI7OFUTBM3NC) and its five phase wallets remain inspectable on-chain.

### Where can I find more information?

Aquarius Medium - [**Announcing the Initial Airdrop**](https://medium.com/aquarius-aqua/announcing-the-initial-aqua-airdrop-6bac308bcc54)


# Airdrop #2

Airdrop #2 — the January 2022 snapshot, eligibility, boost mechanics, and three-year distribution. Concluded.

{% hint style="info" %}
This page is kept for historical reference. The Airdrop #2 snapshot took place on January 15, 2022, and the 3-year distribution has concluded — unclaimed AQUA was returned to the Community DAO fund.
{% endhint %}

### Who was eligible?

Any wallet, old or new, was able to take part in Airdrop #2. To be eligible, a wallet needed to hold a balance of at least 500 XLM (or yXLM) & at least 1 AQUA at the time of the snapshot.

The reward calculation was based on the XLM, yXLM, and AQUA holdings in the wallet. You could check your eligibility at [aqua.network/airdrop2](https://aqua.network/airdrop2).

### When was the snapshot?

The snapshot occurred on January 15, 2022, at 00:00:00 UTC.

### Were there any CEXs taking part?

Yes, a few CEXs took part. You can find them all in the following Medium article: [**Airdrop #2 — Participating Exchanges**](https://medium.com/aquarius-aqua/airdrop-2-participating-exchanges-daec43175387)

### How big was Airdrop #2?

Airdrop #2 distributed 15 billion AQUA tokens. These tokens were shared amongst all eligible users depending on their XLM, yXLM, and AQUA holdings at the time of the snapshot.

Taking a simple example, a user holding 5,000 XLM received 5 times more AQUA than a user holding 1,000 XLM.

### How was it distributed?

Airdrop #2 was distributed using [claimable balances](/ecosystem-overview/stellar-essentials#claimable-balances) on the Stellar network. The distribution ran for three years (36 months) under the following rules:

* Payments for all airdrop rewards were locked in claimable balances before February 15, 2022.
* Each reward was split into 36 payments, unlocking monthly over 3 years.
* The first payment became available to claim in March 2022.
* Every wallet received a randomized day of the month to claim. This day was the same for each month.
* Each payment needed to be claimed within 3 months.
* All unclaimed funds were sent to the Aquarius DAO treasury wallet.

### Could I have increased my potential reward?

Yes, users who used the locking tool found at locker.aqua.network were able to increase their reward. Using this tool, users locked AQUA for up to 3 years against their wallet. The more AQUA, and the longer those AQUA were locked for, the more boost a user received on their airdrop reward. Users could gain a maximum of 4 times their base reward.

The simplest explanation of the locking effect: the more LockedValue in AQUA a wallet had vs the UnlockedValue of XLM, yXLM, and AQUA, the higher the boost. To achieve the maximum 300% boost, LockedValue vs UnlockedValue needed to be close to 50% locked & 50% unlocked.

### Where can I find more information?

Aquarius Medium - [**Announcing Airdrop 2**](https://medium.com/aquarius-aqua/announcing-aqua-airdrop-2-b338e21c2bf6)


# Signers Guild

The multisig team of community signers controlling the main AQUA treasury wallets.

{% hint style="info" %}
Historical announcement. The body below preserves its original present- and future-tense wording. It does not verify the current signer set, signing threshold, treasury balances, member requirements, or operational responsibilities.
{% endhint %}

The Aquarius Signers Guild is a team of signers responsible for the main AQUA wallets and their operations. The primary responsibility of the guild is to provide decentralized control over community funds and allocate AQUA in line with decisions taken by the whole Aquarius community through voting.

### What percentage of the AQUA supply is controlled by the Signers Guild?

The guild controls 75%+ of the uncirculated AQUA supply.

### Who is in the Signers Guild?

The guild is made up of people who understand Aquarius, DeFi, and Stellar — the most active, respected, and trusted Aquarius and Stellar community members. Inactive or misbehaving members can be removed and replaced as needed.

### How does the signing process work?

The whole signing process happens through native Stellar multisignature capabilities. Every member of the Signers Guild is a separate signer for the treasury. A Stellar account can have up to 20 signers, which is why the guild aims to have 20 members. To implement this idea as soon as possible, the guild started with a team of 7–10 signers, adding more members along the way. A majority of votes (signers) is needed to control any movement of funds.

### What is expected from a guild member?

Every qualified signer needs to make a refundable deposit to participate. Being part of the guild is a paid position, with payments distributed in AQUA and controlled by guild members. All communications happen in Discord, with every signer having a unique role flair in the Aquarius Discord chat. Using Discord also allows members to stay anonymous if they wish.

### How can I sign up for this position?

The original application round has closed. If you feel you might be suitable for this position, reach out to the Aquarius team in the [Aquarius Discord](https://discord.gg/sgzFscHp4C) — new members may be added as needed.

### What wallets do Guild members manage?

[**AQUA Liquidity Rewards**](https://stellar.expert/explorer/public/account/GBU44GPNHLW5GSGE4HDOEFLSSF5A6TF4BAPQ7ZWPB7YTZ2F4I4L22DOM)

[**AQUA Airdrop 2**](https://stellar.expert/explorer/public/account/GDFCYDQOVJ2OEWPLEGIRQVAM3VTOQ6JDNLJTDZP5S5OGTEHM5CIWMYBH)

[**AQUA Community DAO Fund**](https://stellar.expert/explorer/public/account/GDB3GJCDWLAMVV7ZL6Q7BH3PGVMFWHA5KSRIY2NML3RXX35ESL3MBOXU)

[**AQUA Voting & Staking Rewards**](https://stellar.expert/explorer/public/account/GDLPCKVNQZ3337RDURRV6MCKZATJGPLVCKZQXXGQW6JTITW6IEBZBTLT)

### How can I learn more about this?

Aquarius Medium - [**Aquarius Signers Guild**](https://medium.com/aquarius-aqua/aquarius-signers-guild-8f7f383d2fa9)


# The Aquarius voting mechanism

How governance votes can be used and verified by everyone

{% hint style="info" %}
Historical snapshot from 2021. The body below preserves its original present- and future-tense wording and does not describe the current Aquarius interface, voting assets, reward amounts, or incentive programs. See [Aquarius voting](/voting-and-rewards/aquarius-voting) and [ICE delegation](/aqua-and-ice/overview) for current mechanics.
{% endhint %}

<figure><img src="https://cdn-images-1.medium.com/max/1280/1*gQja-EyAyOiPvoEbZnxlrA.png" alt="An illustration of a hand recording a verified vote on a digital ballot"><figcaption></figcaption></figure>

Achieving governance votes that are reliable, verifiable, and trusted can be a significant obstacle for any project wishing to implement them. If done correctly, it provides a way of innovating a product and can lead to great tokenomics.

Aquarius is more than just a liquidity incentive for the SDEX. It’s a way to showcase new use cases while demonstrating the power of Stellar. Every aspect needs to be on-chain for governance votes to work with Aquarius. This way, everyone can see results as they happen, with everything being verifiable through Stellar explorers.

We’ve created a way for AQUA holders to keep ownership of their tokens when voting while locking them in place to earn staking rewards. So instead of being just another token, AQUA will earn rewards for being put to good use.

It is crucial that AQUA holders vote, as the protocol relies on these votes to know how the community wants to direct the project through governance voting and which market pairs to reward. The Aquarius protocol scans voting wallets to determine governance outcomes and which markets users would like to incentivize with AQUA rewards.

Instead of all SDEX market pairs receiving rewards, the voting mechanism enables the protocol to focus on ones the community believes are essential to the network.

## How voting was implemented

**The voting mechanism makes use of claimable balances on the Stellar network.**

We have used this function to send airdrops to eligible users, giving them specific time parameters to claim their rewards. Instead, this variance in method sends AQUA to voting wallets and locks them in place until after a set time.

Claimable balances send assets into a void while locked inside parameters. These tokens still exist but are unusable until the contract rules are complete. We can calculate the amount of AQUA locked in voting wallets to see how people want Aquarius to function. As a thank you, voters will be rewarded for locking their AQUA in voting wallets.

## Detailed design

Once broken down, this process can be easily implemented into different projects, allowing them to create governance voting systems. Here is how it works.

### Voting wallets

We will first need a few Stellar wallets to create a voting process. For Aquarius, these wallets represent market pairs users would like to receive AQUA rewards. Other projects would set each wallet up for options relevant to them, like political parties running for government.

For this example, let’s look at an Aquarius liquidity voting wallet.

<figure><img src="https://cdn-images-1.medium.com/max/1280/1*Ea8tl9zrbAWPmCqmXiulKw.png" alt="A locked Stellar voting wallet with AQUA claimable balances and asset trustlines"><figcaption></figcaption></figure>

This wallet will be where voters can send their AQUA if they would like to see the liquidity providers of the USDC/XLM trade pair get rewarded. This method of voting wallets also gets used when we figure out governance votes, with two wallets set up for either “for” or “against” votes on a proposal.

To track transactions relating to liquidity voting wallets, they all have the following signer attached to them.

**GA2UB7VXXXUSEAQUAXXXAQUARIUSVOTINGWALLETXXXPOWEREDBYAQUA (seen as GA2U…AQUA in Account Signers in the graphic)**

Inside voting wallets, there are either one or two trustlines alongside the XLM native asset, which is needed for all Stellar wallets to function. The one or two extra trustlines relate to the market pair this wallet represents.

**As this example is XLM/USDC, we have the XLM asset & one trustline, USDC (centre.io).**

If we want to represent yXLM/USDC, we would have XLM & two trustlines, yXLM (ultrastellar.com) & USDC (centre.io).

To ensure votes aren’t tampered with, voting wallets are locked permanently. No one can execute transactions from these wallets, and all data will be forever hardcoded into the blockchain.

### Claimable balances and voting

Now we have wallets representing each market pair for our use case; we need a way for users to provide their votes. For this method of voting to work, we use claimable balances.

Through the Aquarius voting platforms, [vote.aqua.network](https://vote.aqua.network) & [gov.aqua.network](https://gov.aqua.network), users can choose how much AQUA they would like to stake against proposed market pairs or governance proposals. If a market pair isn’t represented in the liquidity voting system yet, a user can create this new pair for everyone to vote on simply by selecting ***Create Pair***.

<figure><img src="https://cdn-images-1.medium.com/max/1280/1*9hVjs4Q1H7U-pVkrzIkt1g.png" alt="The original liquidity voting interface listing markets, voter counts, and AQUA votes"><figcaption></figcaption></figure>

Once users input their desired stake to each market pair, they can select a voting duration to lock their vote in for, from between 1 week & 6 months. Once decided, they can proceed to submit their votes to the network. The voting platform will create & submit claimable balances to the relevant voting wallets during this process.

We can see these votes inside of the voting wallets in the form of **pending claimable balances**. We can scan each of these wallets using blockchain explorer software to calculate how much AQUA is staked in the form of a claimable balance.

As voting wallets are locked, only the original user can reclaim the claimable balance after the selected voting period ends. Wallets being locked means voting wallets can’t claim the AQUA sent to them. Only the wallet that submits the claimable balance can retrieve the staked AQUA, securely staking them inside the Stellar network.

The simplified logic for a 7-day duration transaction would be:

X AND Y NOT claim IF Time < 7DAYS

Voting Wallet (X) & Sender of AQUA (Y) cannot claim the claimable balance before 7 days.

This formula locks the AQUA tokens into the network, with the tokens taken out of circulation for the entire duration chosen.

<figure><img src="https://cdn-images-1.medium.com/max/1280/1*9Rzuc5d1Wrxcr0mUG_bMpA.png" alt="Pending AQUA claimable balances in a locked Stellar voting wallet"><figcaption></figcaption></figure>

When transactions get done this way, it allows anyone to view how many votes have been designated to a voting wallet, thus creating verifiability on-chain. Once the duration has passed, users can reclaim their AQUA and redesignate them to maybe the same market pairs, new ones, or even use their AQUA for other purposes on the Stellar network.

### **Rewards**

So why would a user want to vote?

* They have an asset on the SDEX they genuinely believe in and would like to introduce another incentive to encourage liquidity.
* An asset developer may want to reward users for trading their tokens.
* They would like to vote “for” or “against” a governance proposal.

In these examples, it comes down to whether a user or developer believes enough in a project or a proposal to bother with voting.

A user's belief can work, but it’s not very enticing for the entire community, So to encourage all holders of AQUA to vote, we designated ***`5,000,000,000 AQUA`*****to the** [**AQUA Voting & Staking Rewards wallet**](https://stellar.expert/explorer/public/account/GDLPCKVNQZ3337RDURRV6MCKZATJGPLVCKZQXXGQW6JTITW6IEBZBTLT) when we launched the project.

When users participate in voting, they will receive an extra voting reward. We are finalizing how voting rewards will get distributed and how best to implement them. Once finalized, we will update the community and retroactively reward all previous participation in voting.

How a holder votes will depend entirely on their AQUA strategy. Some may know which markets and governance proposal they would like to vote for, while others might change tactics each month and prefer to review all angles before voting.

As market makers & liquidity providers of chosen market pairs also get rewarded in AQUA, holders may opt to vote for markets they often use themselves or proposals that benefit them the most. Taking part in voting compounds how many ways a user can earn AQUA and control how the protocol functions.

**For liquidity voting**

* A user can stake their AQUA to a designated pair
* Trade this pair, creating liquidity
* Receive rewards for both voting and trading on that pair

**For governance voting**

* A user can stake their AQUA against a proposal, either for or against
* Vote for proposals that benefit them most. Maybe they use AMMs a lot so vote to increase AMM rewards

If a trader uses many different pairs month to month, they may change their vote to correlate with the trading or liquidity they provide across the SDEX.

It’s important users understand this concept, as no one wants to lock their AQUA into low liquidity trading pairs. Although users would earn a staking reward for locking their AQUA into the voting protocol, the market they voted for may not get enough other votes, so there would be no benefit for their favorite market pair.

These types of decisions will not affect everyone. If a holder doesn’t partake in trading or providing liquidity on the SDEX, they will receive a staking reward regardless, which for some is enough.

These are fantastic tokenomics for Aquarius.

Not only does this provide a use case for AQUA tokens, but it also actively encourages users to engage with the protocol and provide liquidity on the SDEX.


# SDEX v2 proposal & algorithm

Changes to the SDEX reward algorithm

{% hint style="info" %}
This is a community-authored draft proposal (2022) that shaped the current SDEX rewards algorithm. It is preserved here in its original form for reference. For the implemented reward logic, see [SDEX Rewards](/voting-and-rewards/sdex-rewards) and the [ICE boost formula](/aqua-and-ice/ice-boosts-how-to-maximize-lp-rewards#how-the-boost-is-calculated).
{% endhint %}

Draft proposal for improving the AQUA SDEX rewards, to better deal with wash trading, and provide a fairer rewards distribution to traders that provide liquidity to the SDEX.

### **The purpose:**

*The purpose of the AQUA SDEX orderbook rewards program is to reward and incentivize traders to add liquidity to the orderbook, so that everyone who wants to trade (especially swap) on the SDEX, gets a fair price, and can trade higher volume without losing a lot of value to slippage. Instead of fully fulfilling this purpose, the current rewards program rewards* a massive share of rewards towards the Wash Traders (WTs) that generate the highest (fake) volume, and contribute almost no liquidity to the orderbook. This is a highly technical problem, requiring a highly technical solution, but I here suggest an SDEX rewards model that gives more AQUA rewards to the traders that contribute liquidity to the orderbook, instead of those providing fake volume. But this new model also needs to be resistant against other forms of orderbook manipulation, such as Spoofing (someone placing huge limit orders to manipulate the orderbook, with the intention to cancel the order before it gets fulfilled, so that they also provide very little real liquidity).

### **Recap of the current situation:**

AQUA is rewarded hourly for selected markets to individual wallets, according to their share of the total trading volume that has been placed on that market as limit orders, waiting for at least 12 blocks (ca 1 minute) before being fulfilled. Here, I’m assuming 200000 AQUA is paid out every day to traders in different markets (although this is not the case anymore).

TotalLimitVolume = total traded volume (of limit orders that has waited for at least 12 blocks) in the last hour.

TraderLimitVolume = a trader's total traded volume (of limit orders that has waited for at least 12 blocks) in the last hour.

TraderVolumeShare = TraderLimitVolume/TotalLimitVolume

TraderHourlyReward = (200000/24 AQUA) x TraderVolumeShare

So if a trader trades 10% of the total hourly volume, they receive 10% of the 8333 AQUA that gets distributed that hour, meaning 833 AQUA.

### **The problem:**

At the time of writing, this was being exploited by several wash-trading operations, most visibly on 1-to-1 pegged markets such as yXLM/XLM and yUSDC/USDC. These used two wallets that alternate between placing large limit orders at the edge of the spread (difference between lowest limit sell order and highest limit buy order in the orderbook), which the other wallet buys after 1 minute. Therefore, these WTs generate huge fake volumes, taking almost all the AQUA rewards. Meanwhile, they provide almost no liquidity to the market, since the orders are waiting for only the minimum required time, and are highly unlikely to be matched by anyone other than their own bots. Such WTs trade profitably with almost no risk, since even if someone else fulfils their orders, the liquidity caused by legit market-makers (MM) enables the WTs to buy back their order with a minuscule loss, compared to the outsized AQUA rewards they receive. WTs like these operate not only on 1-to-1 pegged markets, but also on other non-pegged markets, where they are less obvious.

Because the rewards program is focused entirely on trading volume, it is very hard for legit MMs (such as people running a Kelp bot

<https://www.stellar.org/developers-blog/kelp-gui-your-first-automated-trading-bot?locale=en>

) to out-compete these WTs. Real marker-makers have to take a real risk of loss by providing real liquidity, and since the real trading volume caused by regular traders is minuscule compared to these WTs, most of the incentive for MMs is gone.

So, to conclude, the current AQUA SDEX orderbook rewards program heavily incentivizes WTs providing fake volume and almost no liquidity, in favor of liquidity-providing by MM. Therefore, we should change the rewards program to more heavily incentivize providing actual liquidity, in addition to volume.

### **The proposed solution:**

(This solution is definitely still not yet complete and is very much subject to discussion and change, but hopefully the description is good enough to be understandable, and can serve as a guideline towards a better solution.) The goal of this proposed change in the rewards program, is to refocus the AQUA rewards towards the MMs that provide actual liquidity, rather than the WTs. This is done by replacing the previous TraderVolumeShare by a TraderLiquidityShare.

TraderHourlyReward = (200000/24 AQUA) x TraderLiquidityShare

A trader can achieve a high TraderLiquidityShare by **holding a high volume in limit orders in the orderbook very close to the current price, for as long as possible**. Compared to previously, orders that are not fulfilled before being cancelled, will still count towards TraderLiquidityShare, but fulfilled orders count more than orders that get cancelled. Volume placed in orders closer to the spread contributes more liquidity than orders placed far away from the spread. Therefore, orders placed at a competitive price, will contribute far more towards TraderLiquidityShare, than orders placed far away. These three requirements: 1) to hold an order for a long time, 2) placing orders close to the spread, and 3) fulfilled orders count more than cancelled order, are intended to encourage MMs to close the spread down towards 0%, while discouraging WTs and Spoofers. To calculate a trader’s TraderLiquidityShare according to these rules, I will here suggest a method, though this is very much up to discussion for the AQUA community, and there might realistically be technical limits to how the AQUA team can actually implement this method.

Definitions:

• TraderLiquidityShare = TraderWeightedLiquidity / TotalWeightedLiquidity = the share of liquidity provided by an individual trader compared to the total liquidity provided by all traders in a market.

• TraderWeightedLiquidity = the summed total of weighted liquidity provided by all limit orders of an individual trader in a market over a period of time.

• TotalWeightedLiquidity = the summed total of weighted liquidity provided by all limit orders in a market over a period of time.

• OrderPrice = the price of an order.

• OrderVolume = the volume of an order.

• LowestSellPrice = the price of the lowest order on the sell-side of the orderbook.

• HighestBuyPrice = the price of the highest order on the buy-side of the orderbook.

• TimeWeight = duration of a time interval that an order remains in the orderbook.

• SpreadWeight = the weight of an order depending on its OrderPrice relative to LowestSellPrice or HighestBuyPrice.

• FulfilledWeight = 2 if an order gets fulfilled, between 1 and 2 if the order is partially fulfilled, else FulfilledWeight = 1

• OrderWeightedLiquidity = OrderVolume x TimeWeight x SpreadWeight x FulfilledWeight = the weighted liquidity of an order during a time interval

### Method:

1. First, is one of the bigger challenges with this proposal, the AQUA rewards engine need to know the state of the orderbook over time, including keeping track of which wallets have placed which orders.
2. This is used to count for each wallet a contribution towards the wallet’s TraderLiquidityShare, for every order that a wallet places in the orderbook, for as long as the order remains in the orderbook.
3. For a time interval, e.g. 5 minutes, the prices of the foremost orders, LowestSellPrice and HighestBuyPrice, are determined (e.g. by weighted averaging the price of the foremost orders during the time interval), and each order is given a TimeWeight for how long it remains in the orderbook during the interval.

◦ Example: an order remains in the orderbook for the entire time interval, it therefore gets a TimeWeight = 5/5 = 1 for this interval. Another order from a WT remains in the orderbook for only one minute, and get a TimeWeight = 1/5 = 0.2

◦ Example: in a 5 min interval, for the first minute the LowestSellPrice is 0.03, and for the remaining four minutes the LowestSellPrice is 0.031. The LowestSellPrice for the entire interval is then (0.03 x 1 + 0.031 x 4) / 5 = 0.0308.

◦ It is possible the time interval should be adjusted according to the liquidity/volume/activity of a market. For example, a market with a high liquidity and frequently changing orderbook might need a short time interval of 1 minute to maintain up-to-date LowestSellPrice and HighestBuyPrice, while a low liquidity market with only a few trades per day might only need a time interval of 60 minutes.

1. During the same time interval in 3., for each order in the orderbook, its SpreadWeight is calculated from the OrderPrice relative to the LowestSellPrice or HighestBuyPrice, by the formula:

◦ SpreadWeight = (LowestSellPrice/OrderPrice)^6 (sell-side of the orderbook)

◦ SpreadWeight = (OrderPrice/HighestBuyPrice)^6 (buy-side of the orderbook)

◦ Example: LowestSellPrice is at a price of 0.03, another order sell order is placed above it at a price of 0.04, and another at 0.06. The lowest sell order at LowestSellPrice then has SpreadWeight = (0.03/0.03)^6 = 1, the second order has a SpreadWeight = (0.03/0.04)^6 = 0.1778, and for the third order SpreadWeight = (0.03/0.06)^6 = 0.015625.

◦ This means that as an order gets placed further away from the spread, it quickly loses its weighting for the rewards formula. Yet, these rewards become not only for trading bots, but it is still possible for casual traders to provide liquidity by placing orders a bit further from the spread, which might remain a long time in the orderbook, and get a small reward for it. Also, because LowestSellPrice and HighestBuyPrice are weighted averages of the time interval, it is possible for traders to briefly place orders in front of the HighestBuyPrice and LowestSellPrice, which would give a SpreadWeight higher than 1.

◦ I have here set an exponent for SpreadWeight of 6, which would quite aggressively encourage MM bots to compete to close the spread towards 0%. However, an even higher exponent would give a stronger incentive for MM bots, while making it harder for casual traders. And a lower exponent would make it easier for casual traders to get some rewards, but the spread and liquidity would likely get worse. It might be that the exponent should be adjusted according to volume/liquidity of markets, with a higher exponent for higher liquidity markets, and a lower exponent for lower liquidity markets.

1. FulfilledWeight of an order during a time interval is 1 + the amount of OrderVolume that gets fulfilled by another trader during the time interval. This is intended to further incentivise traders to place orders close to the spread, where they have a high chance of being fulfilled. ◦ Example: an order gets that completely fulfilled during the time interval has FulfilledWeight = 2, and an order that get 30% filled has FulfilledWeight = 1.3, an order that remains untouched or canceled during the time interval has FulfilledWeight = 1
2. Finally, the OrderWeightedLiquidity of an order during a time interval is calculated as OrderWeightedLiquidity = OrderVolume x TimeWeight x SpreadWeight x FulfilledWeight
3. TraderWeightedLiquidity for a trader over an hour can now be calculated by adding together all the OrderWeightedLiquidity of each of the trader’s orders during each of the time periods.
4. TraderLiquidityShare of each trader during a period of an hour can now be calculated, by TraderLiquidityShare = TraderWeightedLiquidity / TotalWeightedLiquidity.

Let’s now have a look at how this method compares to the current rewards program by looking at a scenario over 1 hour of a hypothetical AQUA/XLM market, with three traders who have placed limit orders in the orderbook. For the first half-hour (six 5-min intervals), the HighestBuyPrice on the buy-side of the orderbook remains at 0.03 XLM/AQUA, while in the second half-hour, the HighestBuyPrice stays at 0.029 XLM/AQUA.

1st: a Casual Trader has placed a buy order of 5000 XLM at a price of 0.025, which remains untouched for the entire hour. 2nd: a Wash Trader places buy orders of 200000 XLM, three times, that remains in the orderbook close to the edge of the for spread for 1 minute, before the Wash Trader’s other wallet sells into its own orders. The Wash Trader therefore generates a trading volume of 3 x 200000 = 600000 XLM. 3rd: an aggressive Market Maker maintains 20000 XLM buy orders close to the edge of the spread for the entire hour, which gets fulfilled 12 times, once per 5 min interval. The Market Maker generates a trading volume of 12 x 20000 = 240000 XLM.

According to the current rewards model, which only takes trading volume into account, the Casual Trader would get no rewards, since the order remains untouched. The Wash Trader generates a trading 600000/840000 = 71.4% of the trading volume, and thus receives 5952 AQUA. The Market Maker generates 240000/840000 = 28.6% of the trading volume, and receives 2381 AQUA.

According to the proposed rewards model, which mainly takes into account hold liquidity in the orderbook, the rewards get distributed very differently. 1st: The Casual Trader’s order generates a OrderWeightedLiquidity for every 5-min interval, even though the order never gets fulfilled. In the first 6 intervals the order has a SpreadWeight = (0.025/0.03)^6 = 0.334898, and in the last 6 intervals the order has a SpreadWeight = (0.025/0.029)^6 = 0.410442. For each of the 6 first intervals, the order generates OrderWeightedLiquidity = OrderVolume x TimeWeight x SpreadWeight x FulfilledWeight = 5000 x 1 x 0.334898 x 1 = 1674.49, and the 6 last intervals, OrderWeightedLiquidity = 5000 x 1 x 0.410442 x 1 = 2052.21. In total, the Casual Trader generates TraderWeightedLiquidity = 6 x 1674.49 + 6 x 2052.21 = 22360.2.

2nd: The Wash Trader only has orders in the orderbook for 1 minute before they get fulfilled at the same price at HighestBuyPrice, twice during the first half hour while HighestBuyPrice is 0.03, and once in the second half hour, when HighestBuyPrice is 0.029. Both of these first two orders then generate OrderWeightedLiquidity = 200000 x 0.2 x (0.03/0.03)^6 x 2 = 80000, and for the last order OrderWeightedLiquidity = 200000 x 0.2 x (0.029/0.029)^6 x 2 = 80000. In total TraderWeightedLiquidity = 3 x 80000 = 240000. 3rd: The Market Maker competitively keeps 20000 XLM orders at the front of the orderbook for the entire hour, and fulfils one order in each time interval. In each time interval the 20000 XLM order then generates OrderWeightedLiquidity = 20000 x 1 x (0.03/0.03)^6 x 2 = 40000. In total TraderWeightedLiquidity = 12 x 40000 = 480000.

Then finally, we can calulate for each trader TraderLiquidityShare = TraderWeightedLiquidity / TotalWeightedLiquidity, where TotalWeightedLiquidity = 240000 + 480000 + 22360.2 = 742360.2. 1. Casual Trader: TraderLiquidityShare = 22360.2/742360.2 = 3.012% or 251 AQUA 2. Wash Trader: TraderLiquidityShare = 240000/742360.2 = 32.33% or 2694 AQUA 3. Market Maker: TraderLiquidityShare = 480000/742360.2 = 64.66% or 5388 AQUA

To be discussed and determined: Does the new model also need a minimum time of 12 blocks (1 minute) for being counted towards TraderLiquidityShare? A minimum time limit can now possibly be counterproductive. On one hand it likely still reduces wash trading, but on the other hand, it is normal for professional high-performance MM bots to place orders for very brief periods of time. And anyways, a trader will get a much higher TraderLiquidityShare by keeping orders in the orderbook for longer than 1 minute. Realistically, there should be a minimum threshold for TraderLiquidityShare before a trader receives a payout, to avoid needing to reward a large number of tiny rewards, like 0.0000001 AQUA every hour to an account that has placed a buy order with 100 XLM at 0.0000001 XLM/AQUA, while the price is 0.03 XLM/AQUA. There are likely some edge cases that can be challenging to calculate exactly, such as orders that get moved around changing prices and volumes very rapidly (within seconds).

Who will benefit from this proposed change: Foremost, MMs benefit from this, who need to take a real risk of loss to provide liquidity. (Disclaimer: I am personally in this category, running a Kelp-like MM bot). A higher reward to MMs means they can afford to take a higher risk, by providing more liquidity to the SDEX. A high MM reward even enables MMs to compete aggressively, trading at a slight loss, as long as the rewards are higher than the loss. Additionally, also casual traders, who maybe look at the SDEX for a few minutes a day and places only a single limit order, can benefit from this proposed rewards system. Even though the current AQUA SDEX rewards program has already succeeded in increasing the liquidity on the SDEX, this proposed will likely further push orderbook spreads down closer to 0%, and improve the SDEX liquidity possibly approaching that of top centralized exchanges, like Binance. Therefore, this proposed change benefits all traders on the SDEX, and is likely to help the entire Stellar ecosystem, by providing a better trading experience and fairer price to both casual and advanced traders alike.

Who will lose from this proposed change: Essentially only wash traders lose from this proposal.


