# Home

## About

[Doppler](https://doppler.lol) is an onchain protocol for launching tokens through various [price discovery auctions](https://aada.ms/pdfs/pda.pdf). Applications integrate Doppler, configure their token launch parameters, and get to market faster than building in-house smart contracts. Teams including [Zora](https://zora.co), [Paragraph](https://paragraph.com), [Noice](https://noice.so), and [Bankr](https://bankr.bot/) use Doppler to create new tokens by configuring inputs such as supply curves, vesting + inflation schedules, governance structures, and ongoing economics, such as fees or treasury management.

{% hint style="success" %}
Doppler is now available on Solana devnet. View the [SDK examples](/reference/svm-sdk-examples) to get started.
{% endhint %}

## Start building on Base

```bash
npm install @whetstone-research/doppler-sdk viem
```

```javascript
import { DopplerSDK } from '@whetstone-research/doppler-sdk';
import {
  createPublicClient,
  createWalletClient,
  http,
  parseEther
} from 'viem';
import { base } from 'viem/chains'

// 1. Set up viem clients
const publicClient = createPublicClient({
  chain: base,
  transport: http(),
});

// 2. Set up your local wallet client
const walletClient = createWalletClient({
  chain: base,
  transport: http(),
  account: '0x...', // Your wallet address
});

// 3. Initialize the SDK
const sdk = new DopplerSDK({
  publicClient,
  walletClient,
  chainId: base.id,
});

// 4. Configure a multicurve auction using market cap ranges
const WETH = '0x4200000000000000000000000000000000000006';

const params = sdk
  .buildMulticurveAuction()
  .tokenConfig({
    name: 'TEST',
    symbol: 'TEST',
    tokenURI: 'https://example.com/metadata.json'
  })
  .saleConfig({
    initialSupply: parseEther('1000000000'),
    numTokensToSell: parseEther('900000000'),
    numeraire: WETH,
  })
  .withCurves({
    numerairePrice: 3000, // ETH = $3000 USD
    curves: [
      {
        marketCap: { start: 500_000, end: 1_500_000 },
        numPositions: 10,
        shares: parseEther('0.4')
      },
      {
        marketCap: { start: 1_000_000, end: 5_000_000 },
        numPositions: 10,
        shares: parseEther('0.5')
      },
      {
        marketCap: { start: 5_000_000, end: 'max' },
        numPositions: 1,
        shares: parseEther('0.1')
      },
    ],
  })
  .withGovernance({ type: 'noOp' })
  .withMigration({ type: 'noOp' })
  .withUserAddress('0x...')
  .build()

const result = await sdk.factory.createMulticurve(params)
console.log('Pool:', result.poolId)
console.log('Token address:', result.tokenAddress)
```

### Example configuration details

With this [Doppler Multicurve](https://doppler.lol/multicurve.pdf) Launch on Base, we configured:

* Token config - metadata such as the name, symbol, and other relevant data like an image.
* Sale config - allocations of the token and how much is available for sale.
* Curves - Defined in USD, the first curve ($500k) sets launch price.
* Governance & Migration - no onchain governance or protocol migration configured.

## Next steps

Continue [learning more about Doppler](/explainer) and follow along with other SDK [examples](/reference/examples).


# Explainer

[Doppler](https://doppler.lol) is an onchain protocol for launching tokens through various [price discovery auctions](https://aada.ms/pdfs/pda.pdf).

Teams use Doppler to create new tokens and determine their prices via onchain public markets. In addition to basic creation of the digital assets, Doppler can handle various downstream functionality, such as vesting, inflation, governance, multiparty trading fees, buybacks, and more.

Doppler's [smart contracts](https://github.com/whetstoneresearch/doppler) can be interacted with directly or through an open source [SDK](https://github.com/whetstoneresearch/doppler-sdk) which abstracts away all relevant smart contract complexity. Data including market cap, volume, holder counts, and more is available through a publicly hosted [indexing API](/reference/api-usage).

All of the smart contracts have been through [security review &/or audited](/reference/security-and-bug-bounties).

Doppler is available on a variety of Ethereum Virtual Machine (EVM) based networks, such as Base, Ethereum Mainnet, and Monad. Doppler is also now available on Solana Devnet, and soon, Mainnet.

## Key concepts

### Doppler Airlock

Doppler's [Airlock](https://github.com/whetstoneresearch/doppler/blob/main/src/Airlock.sol) smart contract provides a unified interface for interacting with the protocol. Users pass their desired parameters into the Airlock, which then manages all interactions with downstream contracts - including token factories, hook initializers, governance factories, and any other contracts configured for a given launch.

### Price discovery auctions

Doppler currently supports three different kinds of price discovery auctions, each designed to be utilized in different contexts, such as available network compatibility or desired market structure.

* Static auctions - allow specifying a single supply or price curve.
* Multicurve auctions - allow specifying multiple supply or price curves.
* Dynamic auctions - allow specifying ranges for dynamically adjusting supply or price curves.

### Liquidity migration

After a price discovery auction completes, the liquidity proceeds generated in the price discovery auction can be migrated into a different DeFi protocol or automated market maker, automtaing end to end liquidity pool creation. Doppler supports fee rehypothecation, enabling fees to be programmatically redirected, e.g., to grow liquidity, perform buybacks, or consolidate into one side of the market.

### Doppler Hooks

Use custom callback functions on initialization, each swap, or token graduation, aka liquidity migration at a specified price or proceed target. This can enable custom logic or downstream triggers within external contracts. See the [Doppler Hooks](/advanced-features/doppler-hooks) section for more information.

### Fees

Every token can be created with a unique set of beneficiary addresses that earn earn fees on every swap in pools created by Doppler, forever. Fees can also optionally be configured to decay from the time they're created in efforts to mitigate the inecntives of automated purchasing, for example, starting at 80% and decreasing linearly down to 1% over the first N seconds.

#### Protocol fee

The Doppler Protocol receives 5% of trading fees on EVM and 7.5% on Solana. For example, if a token is configured with a 1% trading fee, the protocol receives 0.05% of swap volume on EVM or 0.075% on Solana. A token configured with a 0% trading fee generates no protocol fees.

### Governance & Treasury

Using OpenZeppelin's [Governor](https://docs.openzeppelin.com/contracts/4.x/api/governance#governor), tokens can optionally be created with onchain token holder governance and treasury management. Community stakeholders can hold onto a meaningful percentage of token supply, and after vesting or unlocks, vote on what to do with it's treasury.

### Design principles

* **Capital efficiency** - Highly optimized auctions help tokens find their fair market price.
* **MEV protecting** - Configure custom markets and launches designed to mitigate sniping & MEV.
* **Composability -** Doppler is fully onchain and composable with other programs.
* **Optionality -** turn on and off different modules, eg. use governance, or opt out of it entirely.

### Custom modules

Smart contracts called "modules" can be added to Doppler and used alongside the Airlock and rest of the protocol. This provides necessary customization or differentiation while still utilzing the rest of Doppler's end to end battle tested infrastructure. For example, teams could implement a custom module to migrate liquidity to a non-Uniswap AMM, or into a different DeFi protocol entirely, or teams choose to implement custom modules for specific compliance considerations or requirements.

### Usage & Integrations

The [Doppler Protocol](https://github.com/whetstoneresearch/doppler)'s smart contracts can be interacted with directly. The [Doppler TypeScript SDK](https://github.com/whetstoneresearch/doppler-sdk) enables seamless application integrations, abstracting away underlying smart contract complexity. The [Doppler Application](https://app.doppler.lol) provides a self-service interface for teams that want off-the-shelf customization without writing code.


# Creation & initialization

Learn about creating tokens using the Doppler Protocol

## Creation & Initialization

Doppler lets you create new digital assets and initialize liquidity markets in a single, configurable flow.

Using the SDK or Doppler App, you can:

* Deploy a new token
* Configure its supply distribution
* Define price discovery mechanics
* Route fees and treasury allocations
* Initialize liquidity into an AMM or custom market

***

### What You Can Create

Doppler supports multiple asset configurations:

* **Standard ERC-20 tokens**
* Tokens with **custom supply curves**
* Tokens with **vesting or inflation schedules**
* Assets with **programmable fee routing**
* Governance-ready or treasury-aware tokens

All token economics are defined at creation.

***

### Price Discovery Options

Instead of seeding a traditional LP directly, Doppler supports configurable price discovery mechanisms:

* Static price curves
* Multicurve distributions
* Dynamic / time-based curves

This allows liquidity to form through transparent on-chain trading before migration to a live AMM.

***

### Initializing Liquidity

After price discovery completes, liquidity proceeds can be:

* Migrated into a Uniswap-style AMM
* Consolidated into one side of the market
* Used for automated buybacks
* Directed to treasury or custom fee destinations

Liquidity initialization is programmable and defined at creation.

***

### High-Level Flow

1. Configure token + economics
2. Deploy via SDK or App
3. Run price discovery auction
4. Finalize and migrate liquidity
5. Market becomes live on target AMM

***

### Why Use Doppler for Creation?

* No custom smart contract development required
* On-chain, transparent distribution
* Configurable fee and treasury mechanics
* Clean migration into standard AMMs

Doppler turns token creation and liquidity initialization into a single, programmable system.


# Price discovery auctions

Learn about Doppler's price discovery auctions

## Price Discovery Auctions

Doppler uses **on-chain price discovery auctions** to determine fair market prices for a new token before migrating liquidity into a live AMM. Auctions let real users compete for tokens based on supply and demand rather than fixed pricing, resulting in transparent and efficient price formation.

***

### What Is a Price Discovery Auction

A price discovery auction is a market mechanism where participants place bids to acquire tokens. The final price emerges from buyer demand, supply constraints, and bidding behavior - revealing a market-clearing price that reflects true value in an environment with asymmetric information.&#x20;

In Doppler, this process allows token issuers to launch without having to attempt and price their own assets, letting the market determine the value while building a cohesive and liquid market.

***

### Key Auction Types Supported

* **Static Auctions** — Simplest path: price increases on a simple supply curve.
* **Multicurve Auctions** — Multiple supply curves that shape price evolution.
* **Dynamic Auctions** — Price evolves based on configuration, market demand and time.

Each kind of price discovery auction is differentiated on how price update as tokens are sold, enabling flexible mechanisms tailored to a given project's goals.

***

### How It Works (High-Level)

1. **Configure Auction Parameters**
   * Total tokens for sale
   * Curve type and slope
   * Fee routing and treasury rules
2. **Start Auction**
   * Users bid/buy along the curve
   * On-chain events record demand and fill
3. **Determine Clearing Price**
   * The auction clears when supply is exhausted or duration ends
   * The resulting price reflects aggregated demand
4. **Finalize & Migrate**
   * Auction proceeds are used to seed liquidity
   * Liquidity is migrated into a target AMM

This yields a **market-established price** and a live trading pool without manual pricing.

***

### Why Auctions Matter

* 📈 **Fair pricing** — Market forces set the initial price.
* 🔄 **Transparent mechanics** — On-chain bidding reveals demand.
* 💧 **Efficient liquidity** — Auction proceeds form the foundation of the trading pool.
* 📊 **Demand-driven launch** — Prevents arbitrary initial pricing.

By embedding price discovery into token initialization, Doppler aligns incentives between issuers and early participants, resulting in the best possible outcomes.


# Supply curves

Learn more about Doppler's custom supply curves

## Supply Curves

In Doppler, **supply curves** define how token price changes relative to the amount sold during a price discovery auction. Instead of a single fixed price, a supply curve maps cumulative tokens sold to price, allowing controlled, potentially predictable price progression as demand unfolds.

***

### What a Supply Curve Does

* Determines **how price increases** as tokens are purchased
* Shapes **auction dynamics** (early cheap access vs. steep climbs)
* Signals **scarcity and demand** to participants
* Influences **liquidity migration outcomes**

A well-designed curve aligns economic incentives and supports efficient market formation.

***

### Curve Types Supported

Doppler supports multiple supply curve models to fit different launch goals:

#### Static Supply Curves

Flat or linear progression with a single supply curve.\
Price increments are uniform as tokens sell.

> Useful for simple, predictable price growth without complex structure.

***

#### Multiple Supply Curves

Multiple segments of the same market with distinct slopes.\
Each segment can have a different price rate.

> Lets projects encode phases (e.g., early discount → steeper later pricing).

***

#### Dynamic / Time-Based Supply Curves

Curve parameters evolve over time or based on demand signals.\
Price can accelerate or decelerate dynamically.

> Ideal for launches that need tempo-aware pricing behavior or maximum efficiency.

***

### Designing Good Curves

* Gentle slope → smoother, more gradual price movement
* Steep early slope → faster price growth, tighter early access

The choice of curve impacts:

* Token distribution speed
* Participant experience
* Initial liquidity depth

Supply curves are a core economic primitive in Doppler - they convert demand into predictable pricing, designed to be easily updatable and enabling iterations towards an ideal market structure.

***

### When to Choose Which Curve

* **Static** → Simple, predictable auction
* **Multicurve** → Phased launches with differentiated pricing. Well suited for low value assets.
  * Multicurve is considered a *strict* improvement over static due its designs ability to implement a superset of static's functionality and should be used on supported networks.
* **Dynamic** → Demand/time-sensitive markets. Well suited for high value assets.

Custom supply curves offer flexibility for projects to design markets tailored to their use cases, without custom contracts - all defined and adjustable in simple configurations at launch time.


# Liquidity migration

Learn more about how liquidity migration can be customized

## Overview

Doppler supports migrating liquidity from auction contracts to long-term AMM pools optimized for sustained trading. Migration triggers are configurable at creation, typically based on proceeds thresholds that indicate sufficient liquidity for the next growth phase. Currently supported migration targets include Uniswap v2, v3, and v4, with configurable fee structures. Support for additional AMMs and ecosystems is expected.

### Non-migration (r*ecommended*)

In a variety of use cases, it may often make sense to *not* migrate liquidity from the pool utilized to auction the token and bootstrap liquidity to another, longer term AMM pool. This can be done to maximize simplicity, for example, providing unified experiences on trading charts and terminals, or a variety of other reasons. To not migrate liquidity, use the `noOp` parameter in the migration config. Notably this is supported for launches using Doppler Multicurve or "Lockable" V3 static auctions.

## Migration Options Guide

The SDK encodes post‑auction liquidity migration via a discriminated union `MigrationConfig`:

```ts
export type MigrationConfig =
  | { type: 'noOp' }
  | { type: 'uniswapV2' }
  | { type: 'uniswapV3'; fee: number; tickSpacing: number }
  | {
      type: 'uniswapV4'
      fee: number
      tickSpacing: number
      streamableFees: {
        lockDuration: number // seconds
        beneficiaries: { beneficiary: Address; shares: bigint }[] // shares in WAD (1e18 = 100%)
      }
    }
```

Internally, the factory resolves the on‑chain migrator address for your chain and ABI‑encodes the specific data shape required by that migrator.

### Quick Decision Guide

* Want the simpliest solution and unified charts/UX?
* Want simplest AMM and immediate trading? Use V2
* Want a concentrated liquidity range in the resulting pool? Use V3
* Want programmable fee streaming to beneficiaries and are on a V4‑ready chain? Use V4

### No Migration

```ts
.withMigration({ type: 'noOp' })
```

* Supported on: UniswapV4MulticurveInitializer, LockableUniswapV3Initializer (Static auctions only)
* **Note:** `noOp` migration is NOT supported for Dynamic auctions - the SDK will throw an error if you try to use it

### V2 Migration

```ts
.withMigration({ type: 'uniswapV2' })
```

* Encoded data: empty (`0x`)
* Migrator address resolved per chain (see `src/addresses.ts`)

### V3 Migration

```ts
.withMigration({ type: 'uniswapV3', fee: 3000, tickSpacing: 60 })
```

* Encoded data: `(fee:uint24, tickSpacing:int24)`
* Ensure `tickSpacing` matches the selected `fee` tier on your chain

### V4 Migration (streamable fees)

```ts
.withMigration({
  type: 'uniswapV4',
  fee: 3000,
  tickSpacing: 60,
  streamableFees: {
    lockDuration: 365 * 24 * 60 * 60, // 1 year
    beneficiaries: [
      { beneficiary: '0x...', shares: parseEther('0.6') },  // 60%
      { beneficiary: '0x...', shares: parseEther('0.4') },  // 40%
    ],
  },
})
```

* Encoded data:
  * `(fee:uint24, tickSpacing:int24, lockDuration:uint32, beneficiaries: (address, shares[WAD])[])`
  * The SDK sorts beneficiaries by address (ascending) as required by the contract
* Validation:
  * At least one beneficiary
  * Shares must sum to exactly 1e18 (100%)
  * Contract enforces: airlock owner must receive at least 5% of streamed fees (add as a beneficiary if applicable)
* Chain support:
  * Ensure `streamableFeesLocker` and `v4Migrator` are deployed on your target chain (see `src/addresses.ts`)

### Governance Selection

* No‑op governance: Call `withGovernance({ type: 'noOp' })`. The SDK throws if `noOpGovernanceFactory` is not deployed on the chain.
* Standard governance: Call `withGovernance({ type: 'default' })` for standard defaults.
* Custom governance: Call `withGovernance({ type: 'custom', initialVotingDelay, initialVotingPeriod, initialProposalThreshold })`.

### Address Resolution

Migrator contracts are selected per chain via `getAddresses(chainId)` (see `src/addresses.ts`).

* `v2Migrator`, `v3Migrator`, `v4Migrator` must be present for the chosen type
* Some optional contracts (`noOpGovernanceFactory`, `streamableFeesLocker`) may be `0x0` on certain chains — avoid V4 migration with fee streaming where not supported. Using no‑op governance requires `noOpGovernanceFactory`.

### When to choose which

* No migration (noOp)
  * Simpliest possible solution & fastest time to market
  * Unified charts for pre and post liquidity bootstrapping
  * Reduced complexity, debugging, and testing requirements
  * Utilizing Multicurve or another supported initializer
* Uniswap V2
  * Basic constant‑product pool; broad ecosystem tooling
  * No price range configuration; least complexity
  * Good default if you do not require V3/V4‑specific features
* Uniswap V3
  * Concentrated liquidity with a fixed range
  * Requires `fee` tier and matching `tickSpacing`
  * Choose when you want to seed a V3 pool with explicit range after the sale
* Uniswap V4
  * Pools with hooks; supports fee streaming via `StreamableFeesLocker`
  * Requires `fee`, `tickSpacing`, and `streamableFees`
  * Choose when you want programmable fee distribution to beneficiaries, and V4 infra is available on your chain


# Fees & economics


# Doppler Hooks

### Overview

Doppler Hooks, aka dhooks, are a set of callback functions that can be called during the lifecycle of "locked" pools initialized by the `DopplerHookInitializer` contract.&#x20;

Three main events will trigger these hooks:

* `initialization`: when a new pool is created
* `swap`: when a swap occurs in the pool
* `graduation`: when the pool reaches a certain price maturity

Additionally, pools associated with a Doppler Hook can have their LP fee updated by the associated timelock governance contract or a delegated address.

#### **A couple of things to note:**

* A pool initialized without a Doppler Hook can opt-in to use one later via the `setHook` function
* A pool initialized with a Doppler Hook can opt-out of using it later by setting the hook address to `address(0)`
* A pool can change its associated Doppler Hook to a different one at any time via the `setHook` function
* Doppler Hooks are approved by the protocol multisig
* A pool without a Doppler Hook cannot be initialized with a dynamic LP fee

#### Implementation

Here are the different callback functions available for the Doppler Hooks.

Note that they can be implemented selectively based on the use case:

| Callback Function                                                                                                                                                                | Triggered By                                                                                                                                                                                           |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `onInitialization(address asset, PoolKey calldata key, bytes calldata data`                                                                                                      | <p>- <code>initialize()</code> if a <code>dopplerHook</code> address is set in the <code>InitData</code><br>- <code>setDopplerHook()</code> if a Doppler Hook is set after the pool initialization</p> |
| `onSwap(address sender, PoolKey calldata key, IPoolManager.SwapParams calldata params,BalanceDelta delta, bytes calldata data) returns (Currency feeCurrency, int128 hookDelta)` | `afterSwap` before each swap happening in the Uniswap V4 pool                                                                                                                                          |
| `onGraduation(address asset, PoolKey calldata key, bytes calldata data)`                                                                                                         | `graduate` if the graduation conditions are met (e.g. `farTick` reached)                                                                                                                               |


# Fee Rehypothecation

Create Multicurve pools with RehypeDopplerHook for advanced fee distribution and buyback mechanisms

Rehype Pools use the **RehypeDopplerHookInitializer** to distribute trading fees across beneficiaries, LPs, and buyback destinations.

## Fee distribution model

Fees are split into four categories (must sum to 100%):

| Category              | Description                       |
| --------------------- | --------------------------------- |
| **Asset Buyback**     | Buy back the token                |
| **Numeraire Buyback** | Buy back the quote token          |
| **Beneficiary**       | Stream to configured addresses    |
| **LP**                | Distribute to liquidity providers |

***

## Basic example

```typescript
import { DopplerSDK, getAddresses } from '@whetstone-research/doppler-sdk';
import { parseEther, createPublicClient, createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';

const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
const rpcUrl = process.env.RPC_URL ?? 'https://mainnet.base.org';

async function main() {
  const account = privateKeyToAccount(privateKey);
  const addresses = getAddresses(base.id);

  const publicClient = createPublicClient({
    chain: base,
    transport: http(rpcUrl),
  });

  const walletClient = createWalletClient({
    chain: base,
    transport: http(rpcUrl),
    account,
  });

  const sdk = new DopplerSDK({
    publicClient,
    walletClient,
    chainId: base.id,
  });

  // Get protocol owner for beneficiary requirement (min 5%)
  const protocolOwner = await sdk.getAirlockBeneficiary();

  // Beneficiaries receive the beneficiaryPercentWad portion of fees
  const beneficiaries = [
    protocolOwner, // 5% to protocol (minimum required)
    { beneficiary: account.address, shares: parseEther('0.95') }, // 95% to creator
  ];

  const BUYBACK_DESTINATION = account.address; // receives bought-back tokens

  const params = sdk
    .buildMulticurveAuction()
    .tokenConfig({
      name: 'Rehype Token',
      symbol: 'RHT',
      tokenURI: 'https://example.com/metadata.json',
    })
    .saleConfig({
      initialSupply: parseEther('1000000000'),
      numTokensToSell: parseEther('1000000000'),
      numeraire: addresses.weth,
    })
    .withCurves({
      numerairePrice: 3000, // ETH = $3000 USD
      curves: [
        { marketCap: { start: 500_000, end: 1_500_000 }, numPositions: 10, shares: parseEther('0.3') },
        { marketCap: { start: 1_000_000, end: 5_000_000 }, numPositions: 15, shares: parseEther('0.4') },
        { marketCap: { start: 4_000_000, end: 50_000_000 }, numPositions: 10, shares: parseEther('0.3') },
      ],
      beneficiaries,
    })
    .withRehypeDopplerHook({
      hookAddress: addresses.rehypeDopplerHookInitializer!,
      buybackDestination: BUYBACK_DESTINATION,
      customFee: 3000, // 0.3% swap fee
      // Distribution must sum to WAD (100%)
      assetBuybackPercentWad: parseEther('0.20'),      // 20%
      numeraireBuybackPercentWad: parseEther('0.20'), // 20%
      beneficiaryPercentWad: parseEther('0.30'),       // 30%
      lpPercentWad: parseEther('0.30'),                // 30%
    })
    .withGovernance({ type: 'noOp' })
    .withMigration({ type: 'noOp' })
    .withUserAddress(account.address)
    .withDopplerHookInitializer(addresses.dopplerHookInitializer!)
    .withNoOpMigrator(addresses.noOpMigrator!)
    .build();

  const result = await sdk.factory.createMulticurve(params);

  console.log('Pool:', result.poolId);
  console.log('Token:', result.tokenAddress);
}

main();
```

***

## Fee distribution strategies

### Heavy buyback (price support)

```typescript
.withRehypeDopplerHook({
  hookAddress: addresses.rehypeDopplerHookInitializer!,
  buybackDestination: burnAddress,
  customFee: 5000, // 0.5%
  assetBuybackPercentWad: parseEther('0.50'),    // 50% to buy back tokens
  numeraireBuybackPercentWad: parseEther('0.10'),
  beneficiaryPercentWad: parseEther('0.20'),
  lpPercentWad: parseEther('0.20'),
})
```

### LP-first (attract liquidity)

```typescript
.withRehypeDopplerHook({
  hookAddress: addresses.rehypeDopplerHookInitializer!,
  buybackDestination: treasury,
  customFee: 3000,
  assetBuybackPercentWad: parseEther('0.10'),
  numeraireBuybackPercentWad: parseEther('0.10'),
  beneficiaryPercentWad: parseEther('0.20'),
  lpPercentWad: parseEther('0.60'),              // 60% to LPs
})
```

### Treasury-building

```typescript
.withRehypeDopplerHook({
  hookAddress: addresses.rehypeDopplerHookInitializer!,
  buybackDestination: treasury,
  customFee: 3000,
  assetBuybackPercentWad: parseEther('0.10'),
  numeraireBuybackPercentWad: parseEther('0.40'), // 40% to treasury
  beneficiaryPercentWad: parseEther('0.40'),      // 40% to beneficiaries
  lpPercentWad: parseEther('0.10'),
})
```

***

## Collecting fees

Anyone can trigger fee collection; fees are distributed automatically to the initializer, but each address listed as a beneficiary must then claim their own fees from the initializer:

```typescript
const pool = await sdk.getMulticurvePool(assetAddress);

const { fees0, fees1, transactionHash } = await pool.collectFees();
console.log('Fees collected (token0):', fees0);
console.log('Fees collected (token1):', fees1);
```

***

## Configuration reference

### RehypeDopplerHookConfig

| Parameter                    | Type      | Description                                                 |
| ---------------------------- | --------- | ----------------------------------------------------------- |
| `hookAddress`                | `Address` | Deployed RehypeDopplerHookInitializer (must be whitelisted) |
| `buybackDestination`         | `Address` | Receives buyback tokens                                     |
| `customFee`                  | `number`  | Swap fee in bps (3000 = 0.3%)                               |
| `assetBuybackPercentWad`     | `bigint`  | % for asset buyback (in WAD)                                |
| `numeraireBuybackPercentWad` | `bigint`  | % for numeraire buyback (in WAD)                            |
| `beneficiaryPercentWad`      | `bigint`  | % for beneficiaries (in WAD)                                |
| `lpPercentWad`               | `bigint`  | % for LPs (in WAD)                                          |
| `graduationCalldata`         | `Hex`     | Optional calldata on graduation                             |

***

## Rules

* **Distribution sum**: All four percentages must equal `WAD` (1e18)
* **Beneficiary shares**: Must sum to `WAD`; protocol owner needs at least 5%
  * Note: each beneficiary must collect their own fees by calling collectFees
* **Hook whitelisting**: `hookAddress` must be enabled in `DopplerHookInitializer`
* **Migration**: Use `noOp` - rehype pools don't migrate liquidity
* **Pool status**: Enters "Locked" (status = 2)


# Doppler404

Create hybrid fungible and non-fungible tokens

### About

Doppler404 enables token creators to issue assets with unified liquid markets by bridging the gap between non-fungible tokens (ERC-721) and fungible tokens (ERC-20). This provides automated price discovery, sniping mitigation, a unified liquid market, and an improved trading UX over traditional NFTs - in addition to expanding the utility of fungible tokens for a variety of key use cases.

**How it works**

Creators and users can define a custom quantity of fungible tokens that result in NFT ownership as inputs into the Doppler protocol contracts. For example, holding 100 of a specific fungible token, eg. $ABC, can result in holding 1 NFT from an art collection called ABCart. These assets can be auctioned using either Doppler's static or dynamic auctions, and migrate the liquidity generated to any of the supported AMMs, with or without onchain governance, just like other Doppler token configurations.

#### DN404 & ERC-7631 modifications

Doppler404 builds on top of the foundation of “DN404”, aka [ERC-7631 for “Dual Nature Token Pairs"](https://ethereum-magicians.org/t/erc-7631-dual-nature-token-pair/18796). With a few modifications, Doppler extends the [DN404](https://github.com/Vectorized/dn404) token standard allowing users to freeze their token balances, ensuring users don't accidentally trade away NFTs that they wish to hold.

### Example usage

```typescript
const dynamicBuilder = new DynamicAuctionBuilder()
  .tokenConfig({
    type: 'doppler404' as const,
    name: data.tokenName,
    symbol: data.tokenSymbol,
    baseURI: data.baseURI || `https://metadata.example.com/${data.symbol}/`,
  })
  .saleConfig({ initialSupply, numTokensToSell })
  .poolConfig({ fee: 3000, tickSpacing: 8 })
  .auctionByTicks({
    startTick, endTick,
    minProceeds: parseEther("100"),
    maxProceeds: parseEther("600"),
    duration: 7 * 24 * 60 * 60,  // 7 days in seconds
    epochLength: 43200,  // 12 hours (default)
  })
  .withMigration({
    type: 'uniswapV4', fee: 3000, tickSpacing: 60,
    streamableFees: {
      lockDuration: 60 * 60 * 24 * 30,
      beneficiaries: []
    }
  })
  .withGovernance({ type: 'noOp' })
  .withUserAddress(account.address)
  .withIntegrator()
  .withTime({ blockTimestamp: Number(adjustedTimestamp) })
  .build();
```

### Demo application

The Doppler Demo App supports Doppler404 and is the easiest way to get started.

<figure><img src="/files/fT9kKAHjO3JmBXs4Oyu2" alt=""><figcaption></figcaption></figure>

View it on GitHub: <https://github.com/whetstoneresearch/doppler-demo-app>


# SDK API

***

## DopplerSDK (EVM)

The top-level entry point for all EVM SDK operations. Import from `@whetstone-research/doppler-sdk`.

```ts
import { DopplerSDK } from '@whetstone-research/doppler-sdk'

const sdk = new DopplerSDK({
  publicClient,   // viem PublicClient
  walletClient,   // viem WalletClient (optional for read-only)
  chainId,        // SupportedChainId
})
```

### Builder shortcuts

```ts
sdk.buildStaticAuction()       // → StaticAuctionBuilder<C>
sdk.buildDynamicAuction()      // → DynamicAuctionBuilder<C>
sdk.buildMulticurveAuction()   // → MulticurveBuilder<C>
sdk.buildOpeningAuction()      // → OpeningAuctionBuilder<C>
```

### Token / governance helpers

| Method                           | Returns                                                   |
| -------------------------------- | --------------------------------------------------------- |
| `getAirlockOwner()`              | `Promise<Address>`                                        |
| `getAirlockBeneficiary(shares?)` | `Promise<BeneficiaryData>` — defaults to 5% (0.05e18 WAD) |
| `getPoolInfo(poolAddress)`       | `Promise<PoolInfo>`                                       |
| `getHookInfo(hookAddress)`       | `Promise<HookInfo>`                                       |

### Entity getters

These return entity instances bound to a specific on-chain address.

| Method                                                                                                       | Returns                                                                                       |
| ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
| `getStaticAuction(poolAddress)`                                                                              | `Promise<StaticAuction>`                                                                      |
| `getDynamicAuction(hookAddress)`                                                                             | `Promise<DynamicAuction>`                                                                     |
| `getMulticurvePool(tokenAddress)`                                                                            | `Promise<MulticurvePool>`                                                                     |
| `getRehypeDopplerHook(hookAddress)`                                                                          | `Promise<RehypeDopplerHook>`                                                                  |
| `getOpeningAuction(hookAddress)`                                                                             | `Promise<OpeningAuction>`                                                                     |
| `getOpeningAuctionLifecycle(initializerAddress?)`                                                            | `Promise<OpeningAuctionLifecycle>` — falls back to chain config; throws if unconfigured       |
| `getOpeningAuctionPositionManager(positionManagerAddress?)`                                                  | `Promise<OpeningAuctionPositionManager>` — falls back to chain config; throws if unconfigured |
| `getOpeningAuctionBidManager({ openingAuctionHookAddress, openingAuctionPoolKey, positionManagerAddress? })` | `Promise<OpeningAuctionBidManager>`                                                           |
| `getDerc20(tokenAddress)`                                                                                    | `Derc20`                                                                                      |

***

## Builder API Reference

Builders assemble type‑safe parameter objects for `DopplerFactory.createStaticAuction`, `DopplerFactory.createDynamicAuction`, and `DopplerFactory.createMulticurve`.

* Static auctions: Uniswap V3 style, fixed price range liquidity bootstrapping
* Dynamic auctions: Uniswap V4 hook, dynamic Dutch auction with epoch steps
* Multicurve auctions: Uniswap V4 initializer with multiple curves

### Common Concepts

* Governance defaults to `noOp` on supported chains (all except Ink)
* Fee tiers and tick spacing: 100→1, 500→10, 3000→60, 10000→200

***

## StaticAuctionBuilder

Methods (chainable):

* `tokenConfig({ name, symbol, tokenURI, yearlyMintRate? })`
* `saleConfig({ initialSupply, numTokensToSell, numeraire })`
* `withMarketCapRange({ marketCap: { start, end }, numerairePrice, fee?, numPositions?, maxShareToBeSold? })`
  * `marketCap.start` and `marketCap.end` are fully diluted market caps in USD (or whatever unit your numeraire is priced in)
  * Requires `saleConfig()` first
* `poolByTicks({ startTick, endTick, fee?, numPositions?, maxShareToBeSold? })`
* `withVesting({ duration?, cliffDuration?, recipients?, amounts? })`
* `withGovernance({ type: 'default' | 'custom' | 'noOp' })`
* `withMigration(MigrationConfig)`
* `withUserAddress(address)`
* `build()` → `CreateStaticAuctionParams`

***

## DynamicAuctionBuilder

Methods (chainable):

* `tokenConfig({ name, symbol, tokenURI, yearlyMintRate? })`
* `saleConfig({ initialSupply, numTokensToSell, numeraire? })`
* `poolConfig({ fee, tickSpacing })`
* `withMarketCapRange({ marketCap: { start, min }, numerairePrice, minProceeds, maxProceeds, duration?, epochLength? })`
  * `marketCap.start` is the starting market cap (auction begins here), `marketCap.min` is the floor price the auction descends to
  * Both values are fully diluted market caps in USD (or whatever unit your numeraire is priced in)
  * Requires `saleConfig()` first (do NOT use `poolConfig()` with this method - they are mutually exclusive)
* `auctionByTicks({ startTick, endTick, minProceeds, maxProceeds, duration?, epochLength?, gamma? })`
* `withVesting({ duration?, cliffDuration?, recipients?, amounts? })`
* `withGovernance({ type: 'default' | 'custom' | 'noOp' })`
* `withMigration(MigrationConfig)`
* `withUserAddress(address)`
* `withTime({ startTimeOffset?, blockTimestamp? })`
* `build()` → `CreateDynamicAuctionParams`

***

## MulticurveBuilder

Methods (chainable):

* `tokenConfig({ name, symbol, tokenURI, yearlyMintRate? })`
* `saleConfig({ initialSupply, numTokensToSell, numeraire })`
* `withCurves({ numerairePrice, curves, fee?, tickSpacing?, beneficiaries? })`
  * Requires `saleConfig()` first
  * `curves`: Array of `{ marketCap: { start, end }, numPositions, shares }`
  * `marketCap.start` and `marketCap.end` are fully diluted market caps in USD (or whatever unit your numeraire is priced in)
  * Shares must sum to 1e18 (100%)
* `poolConfig({ fee, tickSpacing, curves, beneficiaries? })`
  * `curves`: Array of `{ tickLower, tickUpper, numPositions, shares }`
* `withRehypeDopplerHook({ hookAddress, buybackDestination, customFee, assetBuybackPercentWad, numeraireBuybackPercentWad, beneficiaryPercentWad, lpPercentWad, graduationCalldata? })`
  * `hookAddress` — Deployed RehypeDopplerHook (must be whitelisted)
  * `buybackDestination` — Receives bought-back tokens
  * `customFee` — Swap fee in bps (3000 = 0.3%)
  * `assetBuybackPercentWad` — % for asset buyback (WAD, 1e18 = 100%)
  * `numeraireBuybackPercentWad` — % for numeraire buyback (WAD)
  * `beneficiaryPercentWad` — % for beneficiaries (WAD)
  * `lpPercentWad` — % for LPs (WAD)
  * `graduationCalldata` — Optional calldata executed on graduation
* `withVesting({ duration?, cliffDuration?, recipients?, amounts? })`
* `withGovernance({ type: 'default' | 'custom' | 'noOp' })`
* `withMigration(MigrationConfig)`
* `withUserAddress(address)`
* `build()` → `CreateMulticurveParams`

### Multicurve rules

* First curve's `marketCap.start` = the launch price
* Curves must be contiguous or overlapping (no gaps)
* Shares must sum to exactly 1e18 (100%)

***

## OpeningAuctionBuilder

An auction used to place single sided LP positions in advance of a Doppler Dynamic auction. It can be used to mitigate sniping or more effectively set the clearing price prior to other price discovery auctions. Obtain via `sdk.buildOpeningAuction()`.

Methods (chainable):

* `tokenConfig(params)` — standard or doppler404 token
  * Standard: `{ name, symbol, tokenURI, yearlyMintRate? }`
  * Doppler404: `{ type: 'doppler404', name, symbol, baseURI, unit? }`
* `saleConfig({ initialSupply, numTokensToSell, numeraire })` — `numTokensToSell ≤ initialSupply`
* `openingAuctionConfig(params: OpeningAuctionConfig)`:
  * `auctionDuration` — opening auction duration in seconds (positive integer)
  * `tickSpacing` — opening auction pool tick spacing
  * `fee` — opening auction pool fee (0–`V4_MAX_FEE`)
  * `minAcceptableTickToken0` / `minAcceptableTickToken1` — int24 price floor/ceiling
  * `minLiquidity` — minimum liquidity the auction must attract (bigint, positive)
  * `shareToAuctionBps` — share of `numTokensToSell` allocated to the opening pool (1–10000)
  * `incentiveShareBps` — bidder incentive share in bps (0–10000)
* `dopplerConfig(params: OpeningAuctionDopplerConfig)`:
  * `minProceeds`, `maxProceeds` — soft and hard raise targets (bigint)
  * `startTick`, `endTick` — Doppler price range (direction depends on numeraire token ordering)
  * `duration?` — Doppler auction duration in seconds
  * `epochLength?` — Doppler epoch length in seconds; must divide `duration` evenly
  * `gamma?` — tick step per epoch; computed optimally when omitted
  * `numPdSlugs?` — number of price-discovery slugs
  * `fee?` — Doppler pool fee
  * `tickSpacing?` — Doppler pool tick spacing; must divide `openingAuction.tickSpacing` evenly
* `withVesting({ duration?, cliffDuration?, recipients?, amounts? })`
* `withGovernance(params: GovernanceOption<C>)` — `{ type: 'default' | 'noOp' | 'launchpad' | 'custom' }`
* `withMigration(migration: MigrationConfig)` — required; e.g. `{ type: 'uniswapV2' }`
* `withUserAddress(address)` — required
* `withIntegrator(address?)` — optional integrator fee recipient
* `withGasLimit(gas?)` — optional gas override (bigint)
* `withTime({ startTimeOffset?, startingTime?, blockTimestamp? })` — mutually exclusive: use `startTimeOffset` (seconds from now) or `startingTime` (absolute unix timestamp or Date)
* Module overrides (advanced): `withOpeningAuctionInitializer(address)`, `withOpeningAuctionPositionManager(address)`, `withAirlock(address)`, `withTokenFactory(address)`, `withDopplerDeployer(address)`, `withGovernanceFactory(address)`, `withV2Migrator(address)`, `withV4Migrator(address)`, `withNoOpMigrator(address)`
* `build()` → `CreateOpeningAuctionParams` — validates all constraints; throws descriptive errors on invalid config

Factory method:

```ts
const { hookAddress, tokenAddress, poolId } = await sdk.factory.createOpeningAuction(params)
```

***

## Factory methods

```ts
const { poolAddress, tokenAddress } = await sdk.factory.createStaticAuction(params)
const { hookAddress, tokenAddress, poolId } = await sdk.factory.createDynamicAuction(params)
const { poolId, tokenAddress } = await sdk.factory.createMulticurve(params)
const { hookAddress, tokenAddress, poolId } = await sdk.factory.createOpeningAuction(params)
```

***

## Solana SDK

Import Solana helpers from the package subpath:

```ts
import {
  createLaunch,
  cpmm,
  cpmmHook,
  cpmmMigrator,
  initializer,
  deriveSolanaCpmmDeployment,
  DOPPLER_SOLANA_DEVNET_PROGRAM_ADDRESSES,
} from '@whetstone-research/doppler-sdk/solana'
```

### Deployment helpers

Use `deriveSolanaCpmmDeployment` to resolve the program IDs and config accounts used by the Solana helpers:

```ts
const deployment = await deriveSolanaCpmmDeployment(
  DOPPLER_SOLANA_DEVNET_PROGRAM_ADDRESSES,
)
```

The SDK exposes the CPMM hook as `cpmmHook` and its deployment address as `cpmmHookProgram`. The deployment object also includes the core protocol program IDs and the derived CPMM and initializer config accounts. For custom deployments, provide `cpmmHookProgram` for new launches.

### Initializer

The `initializer` namespace handles Doppler launches on Solana.

**`createLaunch(input)`**

Builds a complete `initialize_launch` instruction for a new Doppler launch. It derives launch PDAs, builds CPMM migration payloads by default, installs the CPMM hook, resolves hook flags, encodes hook payloads, and commits the relevant remaining-account hashes. Hook behavior is selected through the optional feature inputs below; there is no separate hook selector.

Key hook inputs:

| Field                 | Type                             | Description                                                                                            |
| --------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `dynamicFee`          | `DynamicFeeScheduleArgs \| null` | Optional per-launch fee schedule stored in the hook payload.                                           |
| `cosigner`            | `AddressOrSigner`                | Optionally enables cosigner gating through the CPMM hook.                                              |
| `cosignGateExpiresAt` | `bigint \| number \| null`       | Optional Unix timestamp after which the cosigner signature is no longer required. Requires `cosigner`. |

Hook features compose independently:

| Features     | New-launch inputs                |
| ------------ | -------------------------------- |
| Neither      | Omit `dynamicFee` and `cosigner` |
| Cosigning    | Set `cosigner`                   |
| Dynamic fees | Set `dynamicFee`                 |
| Both         | Set `dynamicFee` and `cosigner`  |

Migration configuration is independent of hook features. CPMM migration can be enabled with any of the four feature combinations above.

Dynamic fees and cosigning can be combined in one hook:

```ts
const { instruction, addresses } = await createLaunch({
  deployment,
  launchAccounts: {
    baseMint,
    quoteMint,
    baseVault,
    quoteVault,
  },
  payer,
  authority: payer,
  supply: {
    baseDecimals: 6,
    baseTotalSupply: 1_000_000_000n * 10n ** 6n,
    baseForDistribution: 0n,
    baseForLiquidity: 0n,
  },
  curve: {
    curveVirtualBase: 1_000_000_000n * 10n ** 6n,
    curveVirtualQuote: 10n * 1_000_000_000n,
    swapFeeBps: 200,
  },
  dynamicFee: {
    startingTime: 0n,
    startFeeBps: 8_000,
    endFeeBps: 200,
    durationSeconds: 10n * 60n,
  },
  cosigner,
  cosignGateExpiresAt,
  migration: {
    minRaiseQuote: 50n * 1_000_000_000n,
  },
  metadata: null,
  feeBeneficiaries: [{ wallet: payer.address, shareBps: 10_000 }],
})
```

**`createInitializeLaunchInstruction(accounts, args)`**

Builds the on-chain instruction for a new Doppler launch.

Accounts (key fields):

| Account                                  | Description                                                                                         |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `config`                                 | Global initializer config PDA                                                                       |
| `launch`                                 | New launch PDA                                                                                      |
| `launchAuthority`                        | Launch authority PDA                                                                                |
| `baseMint`                               | New base token mint keypair (signer)                                                                |
| `quoteMint`                              | Numeraire mint (e.g. WSOL)                                                                          |
| `baseVault` / `quoteVault`               | Token vault keypairs (signers)                                                                      |
| `launchFeeState`                         | Launch fee state PDA                                                                                |
| `payer` / `authority`                    | Fee payer and launch authority (signers)                                                            |
| `hookProgram`                            | CPMM hook program ID for new launches                                                               |
| `migratorProgram`                        | Migrator program ID (e.g. `CPMM_MIGRATOR_PROGRAM_ID`)                                               |
| `cpmmConfig`                             | CPMM config address when using the CPMM migrator                                                    |
| `baseTokenProgram` / `quoteTokenProgram` | Token program IDs for each mint                                                                     |
| `metadataAccount`                        | Token metadata account, required when `metadataName` is non-empty                                   |
| `hookCreateRemainingAccounts`            | Readonly accounts forwarded to create hooks when `HF_BEFORE_CREATE` or `HF_AFTER_CREATE` is enabled |

Args (key fields):

| Arg                                               | Type                          | Description                                                                                |
| ------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------ |
| `namespace`                                       | `Address`                     | Namespace for PDA uniqueness (typically payer address)                                     |
| `launchId`                                        | `Uint8Array`                  | 32-byte launch ID, typically from `initializer.createLaunchId()`                           |
| `baseDecimals`                                    | `number`                      | Decimals of the base token                                                                 |
| `baseTotalSupply`                                 | `bigint`                      | Total base token supply (with decimals)                                                    |
| `baseForDistribution`                             | `bigint`                      | Tokens reserved for creator at graduation                                                  |
| `baseForLiquidity`                                | `bigint`                      | Tokens reserved for post-graduation liquidity                                              |
| `curveVirtualBase`                                | `bigint`                      | XYK virtual base reserves (from `marketCapToCurveParams`)                                  |
| `curveVirtualQuote`                               | `bigint`                      | XYK virtual quote reserves (from `marketCapToCurveParams`)                                 |
| `swapFeeBps`                                      | `number`                      | Swap fee during bonding curve phase (e.g. 100 = 1%)                                        |
| `curveKind`                                       | `number`                      | Curve type — use `CURVE_KIND_XYK`                                                          |
| `curveParams`                                     | `Uint8Array`                  | Curve encoding — use `new Uint8Array([CURVE_PARAMS_FORMAT_XYK_V0])`                        |
| `allowBuy` / `allowSell`                          | `boolean`                     | Enables curve buys and sells                                                               |
| `hookFlags`                                       | `number`                      | Hook flags, e.g. `HF_BEFORE_SWAP`                                                          |
| `hookPayload`                                     | `Uint8Array`                  | Hook payload forwarded to the hook program                                                 |
| `hookCreateRemainingAccountsLen`                  | `number`                      | Number of create-hook remaining accounts at the start of the routed remaining-account list |
| `hookCreateRemainingAccountsHash`                 | `Uint8Array`                  | Hash of create-hook remaining accounts                                                     |
| `hookRemainingAccountsHash`                       | `Uint8Array`                  | Hash of swap hook remaining accounts                                                       |
| `migratorInitPayload`                             | `Uint8Array`                  | Encoded graduation params (from `cpmmMigrator.encodeRegisterLaunchPayload`)                |
| `migratorMigratePayload`                          | `Uint8Array`                  | Encoded migration args (from `cpmmMigrator.encodeMigratePayload`)                          |
| `migratorInitRemainingAccountsHash`               | `Uint8Array`                  | Hash of migrator init remaining accounts                                                   |
| `migratorRemainingAccountsHash`                   | `Uint8Array`                  | Hash of migration remaining accounts                                                       |
| `feeBeneficiaries`                                | `Array<{ wallet, shareBps }>` | Curve fee beneficiaries                                                                    |
| `metadataName` / `metadataSymbol` / `metadataUri` | `string`                      | On-chain token metadata                                                                    |

#### Solana CPMM hook payloads

The CPMM hook is the supported hook for new launches. It can act as a pass-through hook, set a per-launch fee schedule, require a cosigner, or do both. When a schedule is present, launches should enable:

```ts
initializer.HF_BEFORE_CREATE | initializer.HF_BEFORE_SWAP
```

Add `initializer.HF_FORWARD_READONLY_SIGNERS` when the same CPMM hook launch also requires a cosigner. The CPMM hook stores the schedule in the launch hook payload; it does not require a schedule account.

The 32-byte schedule payload layout is:

| Bytes    | Value                                             |
| -------- | ------------------------------------------------- |
| `0..8`   | Magic bytes: `DFEEV1__`                           |
| `8`      | Version, currently `1`                            |
| `9..16`  | Reserved padding                                  |
| `16..24` | Little-endian `i64` Unix timestamp `startingTime` |
| `24..26` | Little-endian `u16` `startFeeBps`                 |
| `26..28` | Little-endian `u16` `endFeeBps`                   |
| `28..32` | Little-endian `u32` `durationSeconds`             |

`startingTime: 0` means "start when the launch is created." During `BEFORE_CREATE`, the hook normalizes `0` or a past timestamp to the current Solana clock timestamp and the initializer stores that normalized payload on the launch account. Future timestamps are preserved.

The hook returns the greater of the dynamic schedule fee and the launch's static `swapFeeBps`, so the schedule cannot reduce the fee below the launch's configured static fee.

The presence of the cosigner config account enables gating. The gate payload only determines whether and when that gate expires, so an indefinite gate does not need an expiry payload.

Payloads are composed as:

```
no schedule or cosigner:              []
dynamic fee only:                    [32-byte schedule]
dynamic fee + indefinite cosigner:   [32-byte schedule]
dynamic fee + expiring cosigner:     [32-byte schedule][42-byte expiry payload]
indefinite cosigner only in this hook: []
expiring cosigner only in this hook: [42-byte expiry payload]
```

For low-level builders, dynamic-fee-only launches commit swap hook remaining accounts as:

```
[namespace]
```

Dynamic fee launches that also require cosigning commit:

```
[namespace, cosigner_config, cosigner]
```

If `namespace` equals `cosigner_config`, include that address only once:

```
[cosigner_config, cosigner]
```

The create-hook remaining account list is empty:

```
[]
```

When using `createInitializeLaunchInstruction` directly with `HF_BEFORE_CREATE`, set `hookCreateRemainingAccountsHash` to `initializer.computeRemainingAccountsHash([])`. The all-zero hash means "no create hook commitment" and is rejected when create hooks are enabled.

The SDK exposes helpers for encoding and inspection:

```ts
const payload = cpmmHook.encodeCpmmHookPayload({
  schedule: {
    startingTime: 0n,
    startFeeBps: 8_000,
    endFeeBps: 200,
    durationSeconds: 10n * 60n,
  },
})

cpmmHook.isDynamicFeeSchedulePayload(payload)
```

When `cpmmConfig` is provided, the SDK appends the CPMM migrator init remaining accounts automatically. Use the migrator helper to build the migration account list and committed hash:

```ts
const deployment = await deriveSolanaCpmmDeployment(
  DOPPLER_SOLANA_DEVNET_PROGRAM_ADDRESSES,
)

const migrationAccounts =
  await cpmmMigrator.buildCpmmMigrationRemainingAccounts({
    launch,
    baseMint,
    quoteMint,
    launchAuthority,
    adminBaseAta,
    adminQuoteAta,
    recipientAtas: [],
    cpmmProgram: deployment.cpmmProgram,
    cpmmMigratorProgram: deployment.cpmmMigratorProgram,
  })

const migratorInitRemainingAccountsHash =
  initializer.computeRemainingAccountsHash([
    migrationAccounts.cpmmMigrationState,
    migrationAccounts.cpmmConfig,
  ])
```

***

### CPMM Migrator

The `cpmmMigrator` namespace encodes the calldata forwarded to the CPMM migrator at graduation.

**`buildCpmmMigrationRemainingAccounts(args)`**

Derives the CPMM pool graph, account metas, and `migratorRemainingAccountsHash` used by `initialize_launch` and `migrate_launch`.

```ts
const migrationAccounts =
  await cpmmMigrator.buildCpmmMigrationRemainingAccounts({
    launch,
    baseMint,
    quoteMint,
    launchAuthority,
    adminBaseAta,
    adminQuoteAta,
    recipientAtas,
    cpmmProgram: deployment.cpmmProgram,
    cpmmMigratorProgram: deployment.cpmmMigratorProgram,
  })
```

**`encodeRegisterLaunchPayload(args)`** → `Uint8Array` (`migratorInitPayload`)

Encodes the `migratorInitPayload` passed to `createInitializeLaunchInstruction`. Called once at launch creation to register graduation parameters.

```ts
const migratorInitPayload = cpmmMigrator.encodeRegisterLaunchPayload({
  cpmmConfig:              migrationAccounts.cpmmConfig,
  initialSwapFeeBps:       30,            // Swap fee on graduated CPMM pool (0.3%)
  initialFeeSplitBps:      5000,          // % of fees distributed to LPs (50%)
  recipients: [
    { wallet: creatorAddress, amount: BASE_FOR_DISTRIBUTION },
  ],
  minRaiseQuote:           50_000_000_000n, // Graduation threshold in lamports (50 SOL)
  minMigrationPriceQ64Opt: null,            // Optional minimum graduation price floor
  migratedPoolHookConfig:  null,
})
```

**`encodeMigratePayload(args)`** → `Uint8Array` (`migratorMigratePayload`)

Encodes the `migratorMigratePayload` passed to `createInitializeLaunchInstruction`. Forwarded at graduation time.

```ts
const migratorMigratePayload = cpmmMigrator.encodeMigratePayload({
  baseForDistribution: BASE_FOR_DISTRIBUTION,
  baseForLiquidity:    BASE_FOR_LIQUIDITY,
})
```

***

### CPMM

After graduation the bonding curve becomes a CPMM pool. These are the core helpers for swaps.

**`createSwapInstruction(accounts)` / `createSwapExactInInstruction(accounts, args)`**

`createSwapInstruction` is a convenience wrapper; `createSwapExactInInstruction` is the low-level form.

```ts
const ix = cpmm.createSwapInstruction({
  config,
  pool:        poolAddress,
  authority:   pool.authority,
  vault0:      pool.vault0,
  vault1:      pool.vault1,
  token0Mint:  pool.token0Mint,
  token1Mint:  pool.token1Mint,
  userToken0:  userAta0,
  userToken1:  userAta1,
  user:        payer,
  amountIn:    1_000_000n,
  minAmountOut,
  tradeDirection: 0,
  programId: deployment.cpmmProgram,
})
```

**Swap quote (off-chain, no RPC)**

```ts
const quote = cpmm.getSwapQuote(pool, amountIn, tradeDirection)
// tradeDirection: 0 = token0→token1, 1 = token1→token0
// quote: { amountOut, feeTotal, feeDist, feeComp, priceImpact, executionPrice }
```

**Q64.64 fixed-point helpers**

```ts
const q64 = cpmm.numberToQ64(1.5)
const n = cpmm.q64ToNumber(q64)
```

**Pool fetching**

```ts
const deployment = await deriveSolanaCpmmDeployment(
  DOPPLER_SOLANA_DEVNET_PROGRAM_ADDRESSES,
)
const [poolAddress] = await cpmm.getPoolAddress(
  token0Mint,
  token1Mint,
  deployment.cpmmProgram,
)

// By address
const pool = await cpmm.fetchPool(rpc, poolAddress, {
  programId: deployment.cpmmProgram,
})

// By token pair (order-independent)
const result = await cpmm.getPoolByMints(rpc, mint0, mint1, {
  programId: deployment.cpmmProgram,
})
```


# EVM SDK Examples


# Multicurve

Create coins with Doppler Multicurve for more granular supply curves

## Using market cap ranges

```typescript
import { DopplerSDK, getAddresses } from '@whetstone-research/doppler-sdk/evm';
import { parseEther, createPublicClient, createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';

const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
const rpcUrl = process.env.RPC_URL ?? base.rpcUrls.default.http[0];

async function main() {
  const account = privateKeyToAccount(privateKey);

  const publicClient = createPublicClient({
    chain: base,
    transport: http(rpcUrl),
  });

  const walletClient = createWalletClient({
    chain: base,
    transport: http(rpcUrl),
    account,
  });

  const addresses = getAddresses(base.id);

  const sdk = new DopplerSDK({
    publicClient,
    walletClient,
    chainId: base.id,
  });

  const params = sdk
    .buildMulticurveAuction()
    .tokenConfig({
      name: 'My Token',
      symbol: 'MTK',
      tokenURI: 'https://example.com/token-metadata.json',
    })
    .saleConfig({
      initialSupply: parseEther('1000000000'),
      numTokensToSell: parseEther('900000000'),
      numeraire: addresses.weth,
    })
    .withCurves({
      numerairePrice: 3000, // ETH = $3000 USD
      curves: [
        {
          marketCap: { start: 500_000, end: 1_500_000 },
          numPositions: 10,
          shares: parseEther('0.3'),  // 30%
        },
        {
          marketCap: { start: 1_000_000, end: 5_000_000 },
          numPositions: 15,
          shares: parseEther('0.4'),  // 40%
        },
        {
          marketCap: { start: 4_000_000, end: 6_000_000 },
          numPositions: 10,
          shares: parseEther('0.2'),  // 20%
        },
        // Tail curve: extends liquidity to infinity
        {
          marketCap: { start: 6_000_000, end: 'max' },
          numPositions: 1,
          shares: parseEther('0.1'),  // 10% (completes 100%)
        },
      ],
    })
    .withVesting({
      duration: BigInt(365 * 24 * 60 * 60),
      cliffDuration: 0,
    })
    .withGovernance({ type: 'noOp' })
    .withMigration({ type: 'uniswapV2' })
    .withUserAddress(account.address)
    .build();

  const result = await sdk.factory.createMulticurve(params);

  console.log('Pool ID:', result.poolId);
  console.log('Token:', result.tokenAddress);
}

main();
```

### Curve rules

* First curve's `marketCap.start` = the launch price
* Curves must be contiguous or overlapping (no gaps)
* Shares must sum to exactly 1e18 (100%)
* **Tail curve (recommended)**: A final curve with `end: 'max'` and `numPositions: 1` ensures price continuity beyond the bonding curve. The tail's `start` must match the previous curve's `end`.

***

## Using raw ticks

```typescript
import { DopplerSDK, WAD, getAddresses } from '@whetstone-research/doppler-sdk/evm';
import { createPublicClient, createWalletClient, http, parseEther } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';

const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
const rpcUrl = process.env.RPC_URL ?? base.rpcUrls.default.http[0];

async function main() {
  const account = privateKeyToAccount(privateKey);
  const addresses = getAddresses(base.id);
  const publicClient = createPublicClient({
    chain: base,
    transport: http(rpcUrl),
  });
  const walletClient = createWalletClient({
    chain: base,
    transport: http(rpcUrl),
    account,
  });
  const sdk = new DopplerSDK({
    publicClient,
    walletClient,
    chainId: base.id,
  });

  const params = sdk
    .buildMulticurveAuction()
    .tokenConfig({ name: 'My Token', symbol: 'MTK', tokenURI: 'https://example.com/token.json' })
    .saleConfig({
      initialSupply: 1_000_000_000n * WAD,
      numTokensToSell: 900_000_000n * WAD,
      numeraire: addresses.weth,
    })
    .poolConfig({
      fee: 3000,
      tickSpacing: 60,
      curves: [
        { tickLower: -120000, tickUpper: -90000, numPositions: 8, shares: parseEther('0.4') },
        { tickLower: -90000, tickUpper: -69960, numPositions: 8, shares: parseEther('0.6') },
      ],
    })
    .withGovernance({ type: 'noOp' })
    .withMigration({ type: 'uniswapV2' })
    .withUserAddress(account.address)
    .build();

  const result = await sdk.factory.createMulticurve(params);

  console.log('Pool ID:', result.poolId);
  console.log('Token:', result.tokenAddress);
}

main();
```


# Static auctions

Create coins with Doppler's static bonding curve, aka Doppler v3

## Using market cap targets

```typescript
import { DopplerSDK } from '@whetstone-research/doppler-sdk';
import { parseEther, createPublicClient, createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { baseSepolia } from 'viem/chains';

const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
const rpcUrl = process.env.RPC_URL ?? baseSepolia.rpcUrls.default.http[0];

async function main() {
  const account = privateKeyToAccount(privateKey);

  const publicClient = createPublicClient({
    chain: baseSepolia,
    transport: http(rpcUrl),
  });

  const walletClient = createWalletClient({
    chain: baseSepolia,
    transport: http(rpcUrl),
    account,
  });

  const sdk = new DopplerSDK({
    publicClient,
    walletClient,
    chainId: baseSepolia.id,
  });

  const params = sdk
    .buildStaticAuction()
    .tokenConfig({
      name: 'My Token',
      symbol: 'MTK',
      tokenURI: 'https://example.com/token-metadata.json',
    })
    .saleConfig({
      initialSupply: parseEther('1000000000'),
      numTokensToSell: parseEther('900000000'),
      numeraire: '0x4200000000000000000000000000000000000006', // WETH on Base
    })
    .withMarketCapRange({
      marketCap: { start: 100_000, end: 10_000_000 }, // $100k to $10M
      numerairePrice: 3000, // ETH = $3000 USD
    })
    .withVesting({
      duration: BigInt(365 * 24 * 60 * 60),
      cliffDuration: 0,
    })
    .withGovernance({ type: 'noOp' })
    .withMigration({ type: 'uniswapV2' })
    .withUserAddress(account.address)
    .build();

  const result = await sdk.factory.createStaticAuction(params);

  console.log('Pool:', result.poolId);
  console.log('Token:', result.tokenAddress);
}

main();
```

### With Uniswap V4 migration

```typescript
import { DopplerSDK, getAirlockOwner } from '@whetstone-research/doppler-sdk';
import { parseEther, createPublicClient, createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { baseSepolia } from 'viem/chains';

const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
const rpcUrl = process.env.RPC_URL ?? baseSepolia.rpcUrls.default.http[0];

async function main() {
  const account = privateKeyToAccount(privateKey);

  const publicClient = createPublicClient({
    chain: baseSepolia,
    transport: http(rpcUrl),
  });

  const walletClient = createWalletClient({
    chain: baseSepolia,
    transport: http(rpcUrl),
    account,
  });

  const sdk = new DopplerSDK({
    publicClient,
    walletClient,
    chainId: baseSepolia.id,
  });

  const airlockOwner = await getAirlockOwner(publicClient);

  const params = sdk
    .buildStaticAuction()
    .tokenConfig({
      name: 'My Token',
      symbol: 'MTK',
      tokenURI: 'https://example.com/token-metadata.json',
    })
    .saleConfig({
      initialSupply: parseEther('1000000000'),
      numTokensToSell: parseEther('900000000'),
      numeraire: '0x4200000000000000000000000000000000000006',
    })
    .withMarketCapRange({
      marketCap: { start: 100_000, end: 10_000_000 },
      numerairePrice: 3000,
    })
    .withVesting({
      duration: BigInt(365 * 24 * 60 * 60),
      cliffDuration: 0,
    })
    .withGovernance({ type: 'noOp' })
    .withMigration({
      type: 'uniswapV4',
      fee: 3000,
      tickSpacing: 60,
      streamableFees: {
        lockDuration: 365 * 24 * 60 * 60,
        beneficiaries: [
          { beneficiary: account.address, shares: parseEther('0.95') },
          { beneficiary: airlockOwner, shares: parseEther('0.05') },
        ],
      },
    })
    .withUserAddress(account.address)
    .build();

  const result = await sdk.factory.createStaticAuction(params);

  console.log('Pool:', result.poolId);
  console.log('Token:', result.tokenAddress);
}

main();
```

***

## Using raw ticks

```typescript
const params = sdk.buildStaticAuction()
  .tokenConfig({
    name: 'My Token',
    symbol: 'MTK',
    tokenURI: 'https://example.com/token-metadata.json',
  })
  .saleConfig({
    initialSupply: parseEther('1000000000'),
    numTokensToSell: parseEther('900000000'),
    numeraire: '0x4200000000000000000000000000000000000006',
  })
  .poolByTicks({ startTick: 175000, endTick: 225000, fee: 10000 })
  .withGovernance({ type: 'noOp' })
  .withMigration({ type: 'uniswapV2' })
  .withUserAddress(account.address)
  .build();
```


# Dynamic auctions

Create coins with Doppler's Dutch auction bonding curve, aka Doppler v4

Dynamic auctions are Dutch auctions where the price starts high and descends over time through epochs until buyers purchase or the auction ends.

## Using market cap targets

`saleConfig()` must be called before `withMarketCapRange()`. Note: Do NOT use `poolConfig()` with `withMarketCapRange()` - they are mutually exclusive. Use `poolConfig()` only with `auctionByTicks()` for manual tick configuration.

```typescript
import { DopplerSDK } from '@whetstone-research/doppler-sdk';
import { parseEther, createPublicClient, createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { baseSepolia } from 'viem/chains';

const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
const rpcUrl = process.env.RPC_URL ?? baseSepolia.rpcUrls.default.http[0];

async function main() {
  const account = privateKeyToAccount(privateKey);

  const publicClient = createPublicClient({
    chain: baseSepolia,
    transport: http(rpcUrl),
  });

  const walletClient = createWalletClient({
    chain: baseSepolia,
    transport: http(rpcUrl),
    account,
  });

  const sdk = new DopplerSDK({
    publicClient,
    walletClient,
    chainId: baseSepolia.id,
  });

  const params = sdk
    .buildDynamicAuction()
    .tokenConfig({
      name: 'My Token',
      symbol: 'MTK',
      tokenURI: 'https://example.com/token-metadata.json',
    })
    .saleConfig({
      initialSupply: parseEther('1000000000'),
      numTokensToSell: parseEther('500000000'),
      numeraire: '0x4200000000000000000000000000000000000006', // WETH on Base
    })
    .withMarketCapRange({
      marketCap: { start: 500_000, min: 50_000 }, // $500k start, $50k floor
      numerairePrice: 3000, // ETH = $3000 USD
      minProceeds: parseEther('100'),
      maxProceeds: parseEther('5000'),
    })
    .withMigration({
      type: 'uniswapV4',
      fee: 3000,
      tickSpacing: 60,
      streamableFees: {
        lockDuration: 365 * 24 * 60 * 60,
        beneficiaries: [
          { beneficiary: account.address, shares: parseEther('0.95') },
          await sdk.getAirlockBeneficiary(),
        ],
      },
    })
    .withGovernance({ type: 'noOp' })
    .withUserAddress(account.address)
    .build();

  const result = await sdk.factory.createDynamicAuction(params);

  console.log('Hook:', result.hookAddress);
  console.log('Token:', result.tokenAddress);
  console.log('Pool ID:', result.poolId);
}

main();
```

***

## Using raw ticks

```typescript
const params = sdk.buildDynamicAuction()
  .tokenConfig({
    name: 'My Token',
    symbol: 'MTK',
    tokenURI: 'https://example.com/token-metadata.json',
  })
  .saleConfig({
    initialSupply: parseEther('1000000000'),
    numTokensToSell: parseEther('500000000'),
    numeraire: '0x4200000000000000000000000000000000000006',
  })
  .poolConfig({ fee: 3000, tickSpacing: 60 })
  .auctionByTicks({
    startTick: -92103,
    endTick: -69080,
    minProceeds: parseEther('100'),
    maxProceeds: parseEther('5000'),
    duration: 7 * 24 * 60 * 60,
    epochLength: 3600,
  })
  .withMigration({
    type: 'uniswapV4',
    fee: 3000,
    tickSpacing: 60,
    streamableFees: {
      lockDuration: 365 * 24 * 60 * 60,
      beneficiaries: [
        { beneficiary: account.address, shares: parseEther('1') },
      ],
    },
  })
  .withGovernance({ type: 'noOp' })
  .withUserAddress(account.address)
  .build();
```


# Quoting, monitoring, and metrics

Examples for getting swap quotes, monitoring auction progress, and fetching token details

### Price quotes

```typescript
/**
 * Example: Price Quoter
 * 
 * This example demonstrates:
 * - Getting price quotes across Uniswap V2, V3, and V4
 * - Comparing quotes to find best prices
 * - Handling different swap types (exact input/output)
 */

// UNCOMMENT IF RUNNING LOCALLY
// import { DopplerSDK } from '@whetstone-research/doppler-sdk';
import { DopplerSDK } from '../src';

import { createPublicClient, http, parseEther, formatEther, type Address } from 'viem'
import { baseSepolia } from 'viem/chains'

const token = process.env.TOKEN as `0x${string}`;
const rpcUrl = process.env.RPC_URL || baseSepolia.rpcUrls.default.http[0] as string;

if (!token) throw new Error('TOKEN is not set');

// Example token addresses (replace with actual addresses)
const weth = '0x4200000000000000000000000000000000000006' as Address // WETH on Base
const usdc = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as Address // USDC on Base

async function main() {
  // Initialize SDK
  const publicClient = createPublicClient({
    chain: baseSepolia,
    transport: http(rpcUrl)
  })

  const sdk = new DopplerSDK({
    publicClient,
    chainId: baseSepolia.id
  })

  const quoter = sdk.quoter

  console.log('💱 Price Quoter Example')
  console.log('=====================')

  // Example 1: Quote exact input on V3
  console.log('\n📊 Example 1: Swap 1 ETH for USDC on V3')
  try {
    const v3Quote = await quoter.quoteExactInputV3({
      tokenIn: weth,
      tokenOut: usdc,
      amountIn: parseEther('1'),
      fee: 3000 // 0.3% fee tier
    })
    
    console.log('- Amount out:', formatEther(v3Quote.amountOut), 'USDC')
    console.log('- Price impact (ticks crossed):', v3Quote.initializedTicksCrossed)
    console.log('- Gas estimate:', v3Quote.gasEstimate.toString())
    console.log('- Final sqrtPriceX96:', v3Quote.sqrtPriceX96After.toString())
  } catch (error) {
    console.error('V3 quote failed:', error.message)
  }

  // Example 2: Quote exact output on V3
  console.log('\n📊 Example 2: Get exactly 2000 USDC, pay in ETH on V3')
  try {
    const v3QuoteOut = await quoter.quoteExactOutputV3({
      tokenIn: weth,
      tokenOut: usdc,
      amountOut: parseEther('2000'), // Want exactly 2000 USDC
      fee: 3000
    })
    
    console.log('- Amount in required:', formatEther(v3QuoteOut.amountIn), 'ETH')
    console.log('- Price impact (ticks crossed):', v3QuoteOut.initializedTicksCrossed)
    console.log('- Gas estimate:', v3QuoteOut.gasEstimate.toString())
  } catch (error) {
    console.error('V3 exact output quote failed:', error.message)
  }

  // Example 3: Quote on V2 (if available)
  console.log('\n📊 Example 3: Swap 1 ETH for USDC on V2')
  try {
    const v2Quote = await quoter.quoteExactInputV2({
      amountIn: parseEther('1'),
      path: [weth, usdc]
    })
    
    console.log('- Amount out:', formatEther(v2Quote[1]), 'USDC')
    console.log('- Simple constant product AMM pricing')
  } catch (error) {
    console.error('V2 quote failed:', error.message)
  }

  // Example 4: Multi-hop V2 quote
  console.log('\n📊 Example 4: Multi-hop swap ETH -> USDC -> TOKEN on V2')
  try {
    const multiHopQuote = await quoter.quoteExactInputV2({
      amountIn: parseEther('1'),
      path: [weth, usdc, token] // ETH -> USDC -> TOKEN
    })
    
    console.log('Hop results:')
    console.log('- Start:', formatEther(multiHopQuote[0]), 'ETH')
    console.log('- After hop 1:', formatEther(multiHopQuote[1]), 'USDC')
    console.log('- Final:', formatEther(multiHopQuote[2]), 'TOKEN')
  } catch (error) {
    console.error('Multi-hop quote failed:', error.message)
  }

  // Example 5: V4 quote (for graduated dynamic auctions)
  console.log('\n📊 Example 5: Swap on V4 pool')
  try {
    const v4PoolKey = {
      currency0: weth,
      currency1: token,
      fee: 3000,
      tickSpacing: 60,
      hooks: '0x0000000000000000000000000000000000000000' as Address // No hook for graduated pool
    }
    
    const v4Quote = await quoter.quoteExactInputV4({
      poolKey: v4PoolKey,
      zeroForOne: true, // Swapping currency0 (WETH) for currency1 (TOKEN)
      exactAmount: parseEther('1')
    })
    
    console.log('- Amount out:', formatEther(v4Quote.amountOut), 'TOKEN')
    console.log('- Gas estimate:', v4Quote.gasEstimate.toString())
  } catch (error) {
    console.error('V4 quote failed:', error.message)
  }

  // Example 6: Compare quotes across versions
  console.log('\n🔄 Comparing quotes for 1 ETH -> USDC:')
  const results: { version: string; amountOut: bigint; gas: bigint }[] = []
  
  // Try V2
  try {
    const v2 = await quoter.quoteExactInputV2({
      amountIn: parseEther('1'),
      path: [weth, usdc]
    })
    results.push({ 
      version: 'V2', 
      amountOut: v2[1], 
      gas: BigInt(100000) // Approximate
    })
  } catch {}
  
  // Try V3
  try {
    const v3 = await quoter.quoteExactInputV3({
      tokenIn: weth,
      tokenOut: usdc,
      amountIn: parseEther('1'),
      fee: 3000
    })
    results.push({ 
      version: 'V3', 
      amountOut: v3.amountOut, 
      gas: v3.gasEstimate 
    })
  } catch {}
  
  // Sort by best output
  results.sort((a, b) => Number(b.amountOut - a.amountOut))
  
  console.log('\nBest quotes (sorted by output):')
  results.forEach((result, i) => {
    console.log(`${i + 1}. ${result.version}: ${formatEther(result.amountOut)} USDC (gas: ${result.gas})`)
  })
  
  if (results.length > 0) {
    console.log(`\n✅ Best option: ${results[0].version} with ${formatEther(results[0].amountOut)} USDC`)
  }

  console.log('\n✨ Example completed!')
}

main()
```

### Auction monitoring

```typescript
/**
 * Example: Monitor Existing Auctions
 *
 * This example demonstrates:
 * - Monitoring static and dynamic auctions
 * - Checking graduation status
 * - Tracking key metrics and progress
 */

// UNCOMMENT IF RUNNING LOCALLY
// import { DopplerSDK } from '@whetstone-research/doppler-sdk';

import { DopplerSDK } from '../src';
import {
  http,
  formatEther,
  type Address,
  createPublicClient,
} from 'viem';
import { baseSepolia } from 'viem/chains';

// Example addresses - replace with your actual auction addresses
const staticPoolAddress = process.env.STATIC_POOL_ADDRESS as `0x${string}`;
const dynamicHookAddress = process.env.DYNAMIC_HOOK_ADDRESS as `0x${string}`;
const rpcUrl = process.env.RPC_URL || baseSepolia.rpcUrls.default.http[0] as string;

async function monitorStaticAuction(sdk: DopplerSDK, poolAddress: Address) {
  console.log('\n📊 Monitoring Static Auction...');
  console.log('Pool address:', poolAddress);

  try {
    const auction = await sdk.getStaticAuction(poolAddress);

    // Get pool information
    const poolInfo = await auction.getPoolInfo();
    console.log('\nPool Information:');
    console.log('- Token:', poolInfo.tokenAddress);
    console.log('- Numeraire:', poolInfo.numeraireAddress);
    console.log('- Fee tier:', poolInfo.fee / 10000, '%');
    console.log('- Liquidity:', formatEther(poolInfo.liquidity));
    console.log('- SqrtPriceX96:', poolInfo.sqrtPriceX96.toString());

    // Get current price
    const currentPrice = await auction.getCurrentPrice();
    console.log('\nCurrent tick price:', currentPrice.toString());

    // Check graduation status
    const hasGraduated = await auction.hasGraduated();
    console.log(
      '\nGraduation status:',
      hasGraduated ? '✅ Graduated' : '⏳ Active'
    );

    // Get token address for further info
    const tokenAddress = await auction.getTokenAddress();
    console.log('Token contract:', tokenAddress);
  } catch (error) {
    console.error('Error monitoring static auction:', error);
  }
}

async function monitorDynamicAuction(sdk: DopplerSDK, hookAddress: Address) {
  console.log('\n📊 Monitoring Dynamic Auction...');
  console.log('Hook address:', hookAddress);

  try {
    const auction = await sdk.getDynamicAuction(hookAddress);

    // Get comprehensive hook information
    const hookInfo = await auction.getHookInfo();
    console.log('\nHook Information:');
    console.log('- Token:', hookInfo.tokenAddress);
    console.log('- Numeraire:', hookInfo.numeraireAddress);
    console.log('- Pool ID:', hookInfo.poolId);
    console.log('- Current epoch:', hookInfo.currentEpoch);

    console.log('\nSale Progress:');
    console.log(
      '- Total proceeds:',
      formatEther(hookInfo.totalProceeds),
      'ETH'
    );
    console.log('- Tokens sold:', formatEther(hookInfo.totalTokensSold));
    console.log(
      '- Min proceeds:',
      formatEther(hookInfo.minimumProceeds),
      'ETH'
    );
    console.log(
      '- Max proceeds:',
      formatEther(hookInfo.maximumProceeds),
      'ETH'
    );

    console.log('\nAuction Status:');
    console.log('- Early exit:', hookInfo.earlyExit ? '✅ Yes' : '❌ No');
    console.log(
      '- Insufficient proceeds:',
      hookInfo.insufficientProceeds ? '⚠️ Yes' : '✅ No'
    );

    // Calculate time remaining
    const now = BigInt(Math.floor(Date.now() / 1000));
    if (now < hookInfo.endingTime) {
      const remaining = Number(hookInfo.endingTime - now);
      const hours = Math.floor(remaining / 3600);
      const minutes = Math.floor((remaining % 3600) / 60);
      console.log('- Time remaining:', `${hours}h ${minutes}m`);
    } else {
      console.log('- Time remaining: Auction ended');
    }

    // Get current price tick
    const currentTick = await auction.getCurrentPrice();
    console.log('\nCurrent tick:', currentTick.toString());

    // Check if ended early
    const hasEndedEarly = await auction.hasEndedEarly();
    if (hasEndedEarly) {
      console.log('\n🎯 Auction ended early due to reaching max proceeds');
    }

    // Check graduation
    const hasGraduated = await auction.hasGraduated();
    console.log(
      'Graduation status:',
      hasGraduated ? '✅ Graduated' : '⏳ Active'
    );
  } catch (error) {
    console.error('Error monitoring dynamic auction:', error);
  }
}

async function main() {
  // Initialize SDK in read-only mode
  const publicClient = createPublicClient({
    chain: baseSepolia,
    transport: http(rpcUrl),
  });

  const sdk = new DopplerSDK({
    publicClient,
    chainId: baseSepolia.id,
  });

  console.log('🔍 Doppler Auction Monitor');
  console.log('=======================');

  // Monitor static auction if address provided
  if (staticPoolAddress) {
    await monitorStaticAuction(sdk, staticPoolAddress);
  } else {
    console.log('\n⚠️  No static auction address provided');
  }

  // Monitor dynamic auction if address provided
  if (dynamicHookAddress) {
    await monitorDynamicAuction(sdk, dynamicHookAddress);
  } else {
    console.log('\n⚠️  No dynamic auction address provided');
  }

  console.log('\n✨ Monitoring complete!');
}

main()
```

### Token interactions

```typescript
/**
 * Example: Token Interaction
 *
 * This example demonstrates:
 * - Interacting with DERC20 tokens launched via Doppler
 * - Checking balances and vesting data
 * - Approving spending and releasing vested tokens
 */

// UNCOMMENT IF RUNNING LOCALLY
// import { Derc20, Eth } from '@whetstone-research/doppler-sdk';

import { Derc20, Eth } from '../src';
import {
  createPublicClient,
  createWalletClient,
  http,
  parseEther,
  formatEther,
} from 'viem';
import { baseSepolia } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';

// Configuration
const spender = process.env.SPENDER as `0x${string}`;
const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
const rpcUrl = process.env.RPC_URL || baseSepolia.rpcUrls.default.http[0] as string;
const tokenAddress = process.env.TOKEN_ADDRESS as `0x${string}`;

if (!privateKey) throw new Error('PRIVATE_KEY is not set');
if (!tokenAddress) throw new Error('TOKEN_ADDRESS is not set');
if (!spender) throw new Error('SPENDER is not set');

async function main() {
  // 1. Set up clients
  const account = privateKeyToAccount(privateKey);

  const publicClient = createPublicClient({
    chain: baseSepolia,
    transport: http(rpcUrl),
  });

  const walletClient = createWalletClient({
    chain: baseSepolia,
    transport: http(rpcUrl),
    account,
  });

  console.log('💰 Token Interaction Example');
  console.log('===========================');
  console.log('Account:', account.address);
  console.log('Token:', tokenAddress);

  // 2. Create token instance
  const token = new Derc20(publicClient, walletClient, tokenAddress);

  try {
    // 3. Get token information
    console.log('\n📋 Token Information:');
    const [name, symbol, decimals, totalSupply] = await Promise.all([
      token.getName(),
      token.getSymbol(),
      token.getDecimals(),
      token.getTotalSupply(),
    ]);

    console.log('- Name:', name);
    console.log('- Symbol:', symbol);
    console.log('- Decimals:', decimals);
    console.log('- Total Supply:', formatEther(totalSupply), symbol);

    // 4. Check balances
    console.log('\n💸 Balances:');
    const balance = await token.getBalanceOf(account.address);
    console.log('- Your balance:', formatEther(balance), symbol);

    // Also check ETH balance
    const eth = new Eth(publicClient);
    const ethBalance = await eth.getBalanceOf(account.address);
    console.log('- ETH balance:', formatEther(ethBalance), 'ETH');

    // 5. Check vesting information
    console.log('\n⏰ Vesting Information:');
    const [vestingDuration, vestingStart, vestedTotal] = await Promise.all([
      token.getVestingDuration(),
      token.getVestingStart(),
      token.getVestedTotalAmount(),
    ]);

    if (vestingDuration > 0n) {
      const vestingEndTime = vestingStart + vestingDuration;
      const now = BigInt(Math.floor(Date.now() / 1000));
      const isVestingActive = now < vestingEndTime;

      console.log(
        '- Vesting duration:',
        Number(vestingDuration) / 86400,
        'days'
      );
      console.log(
        '- Vesting start:',
        new Date(Number(vestingStart) * 1000).toLocaleString()
      );
      console.log('- Total vested amount:', formatEther(vestedTotal), symbol);
      console.log('- Vesting active:', isVestingActive);

      // Check user's vesting data
      const vestingData = await token.getVestingData(account.address);
      console.log('\n📊 Your Vesting Data:');
      console.log(
        '- Total vested:',
        formatEther(vestingData.totalAmount),
        symbol
      );
      console.log(
        '- Already released:',
        formatEther(vestingData.releasedAmount),
        symbol
      );

      // Calculate available to release
      const available = await token.getAvailableVestedAmount(account.address);
      console.log('- Available to release:', formatEther(available), symbol);

      // Release vested tokens if available
      if (available > 0n) {
        console.log('\n🎯 Releasing vested tokens...');
        try {
          const txHash = await token.release(available);
          console.log('✅ Tokens released! Transaction:', txHash);
        } catch (error) {
          console.error('❌ Failed to release tokens:', error);
        }
      }
    } else {
      console.log('- No vesting configured for this token');
    }

    // 6. Token approvals
    console.log('\n🔓 Token Approvals:');
    const currentAllowance = await token.getAllowance(account.address, spender);
    console.log('- Current allowance:', formatEther(currentAllowance), symbol);

    // Approve spending if needed
    const approvalAmount = parseEther('100');
    if (currentAllowance < approvalAmount) {
      console.log(
        `\n📝 Approving ${formatEther(approvalAmount)} ${symbol} for spender...`
      );
      try {
        const txHash = await token.approve(spender, approvalAmount);
        console.log('✅ Approval successful! Transaction:', txHash);
      } catch (error) {
        console.error('❌ Approval failed:', error);
      }
    }

    // 7. Additional token info
    console.log('\n🔍 Additional Information:');
    const [tokenURI, pool, isPoolUnlocked, yearlyMintRate] = await Promise.all([
      token.getTokenURI(),
      token.getPool(),
      token.getIsPoolUnlocked(),
      token.getYearlyMintRate(),
    ]);

    console.log('- Token URI:', tokenURI);
    console.log('- Pool address:', pool);
    console.log('- Pool unlocked:', isPoolUnlocked);
    console.log(
      '- Yearly mint rate:',
      formatEther(yearlyMintRate),
      symbol,
      'per year'
    );
  } catch (error) {
    console.error('\n❌ Error:', error);
    process.exit(1);
  }

  console.log('\n✨ Example completed!');
}

main();
```


# SVM SDK Examples

These examples show how to create and trade Doppler launches on Solana.

* [Launch](/reference/svm-sdk-examples/launch) - create a standard Solana launch.
* [Dynamic fee launch](/reference/svm-sdk-examples/dynamic-fee-launch) - create a launch with a decaying swap fee schedule, optionally combined with cosigner gating.
* [Swap](/reference/svm-sdk-examples/swap) - swap against a Solana CPMM pool.
* [Launch, monitor, and e2e](/reference/svm-sdk-examples/launch-monitor-and-e2e) - create, monitor, trade, and migrate a launch end to end.


# Launch

Easily launch new assets on Solana with Doppler

```typescript
import {
  createKeyPairSignerFromBytes,
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  generateKeyPairSigner,
  pipe,
  createTransactionMessage,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  appendTransactionMessageInstructions,
  signTransactionMessageWithSigners,
  sendAndConfirmTransactionFactory,
  getSignatureFromTransaction,
  type Address,
  type Instruction,
} from '@solana/kit';
import {
  TOKEN_PROGRAM_ADDRESS,
  findAssociatedTokenPda,
} from '@solana-program/token';
import { SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system';
import { SYSVAR_RENT_ADDRESS } from '@solana/sysvars';
import {
  cpmm,
  cpmmMigrator,
  initializer,
  deriveSolanaCpmmDeployment,
  DOPPLER_SOLANA_DEVNET_PROGRAM_ADDRESSES,
} from '@whetstone-research/doppler-sdk/solana';

const keypairJson = process.env.SOLANA_KEYPAIR;
const rpcUrl = process.env.SOLANA_RPC_URL ?? 'https://api.devnet.solana.com';
const wsUrl = process.env.SOLANA_WS_URL ?? 'wss://api.devnet.solana.com';

if (!keypairJson) {
  throw new Error('SOLANA_KEYPAIR must be set (JSON array of 64 bytes)');
}

const WSOL_MINT: Address =
  'So11111111111111111111111111111111111111112' as Address;

async function getSolPriceUsd(): Promise<number> {
  const response = await fetch(
    'https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd',
  );
  const data = await response.json();
  return data.solana.usd;
}

async function main() {
  const payer = await createKeyPairSignerFromBytes(
    new Uint8Array(JSON.parse(keypairJson as string)),
  );
  const rpc = createSolanaRpc(rpcUrl);
  const rpcSubscriptions = createSolanaRpcSubscriptions(wsUrl);
  const deployment = await deriveSolanaCpmmDeployment(
    DOPPLER_SOLANA_DEVNET_PROGRAM_ADDRESSES,
  );
  const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
    rpc,
    rpcSubscriptions,
  });

  async function send(instructions: Instruction[]) {
    const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
    const message = pipe(
      createTransactionMessage({ version: 0 }),
      (tx) => setTransactionMessageFeePayerSigner(payer, tx),
      (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
      (tx) => appendTransactionMessageInstructions(instructions, tx),
    );
    const signedTransaction = await signTransactionMessageWithSigners(message);
    await sendAndConfirmTransaction(
      signedTransaction as Parameters<typeof sendAndConfirmTransaction>[0],
      { commitment: 'confirmed' },
    );
    return getSignatureFromTransaction(signedTransaction);
  }

  const BASE_DECIMALS = 6;
  const BASE_TOTAL_SUPPLY = 1_000_000_000n * 10n ** BigInt(BASE_DECIMALS);
  const MIN_RAISE_QUOTE = 50n * 1_000_000_000n;
  const SWAP_FEE_BPS = 200;

  const { start } = cpmm.marketCapToCurveParams({
    startMarketCapUSD: 100_000,
    endMarketCapUSD: 10_000_000,
    baseTotalSupply: BASE_TOTAL_SUPPLY,
    baseForCurve: BASE_TOTAL_SUPPLY,
    baseDecimals: BASE_DECIMALS,
    quoteDecimals: 9,
    numerairePriceUSD: await getSolPriceUsd(),
  });

  const baseMint = await generateKeyPairSigner();
  const baseVault = await generateKeyPairSigner();
  const quoteVault = await generateKeyPairSigner();

  const namespace = payer.address;
  const launchId = initializer.launchIdFromU64(BigInt(Date.now()));
  const [launch] = await initializer.getLaunchAddress(
    namespace,
    launchId,
    deployment.initializerProgram,
  );
  const [launchAuthority] = await initializer.getLaunchAuthorityAddress(
    launch,
    deployment.initializerProgram,
  );
  const [launchFeeState] = await initializer.getLaunchFeeStateAddress(
    launch,
    deployment.initializerProgram,
  );
  const [adminBaseAta] = await findAssociatedTokenPda({
    owner: payer.address,
    mint: baseMint.address,
    tokenProgram: TOKEN_PROGRAM_ADDRESS,
  });
  const [adminQuoteAta] = await findAssociatedTokenPda({
    owner: payer.address,
    mint: WSOL_MINT,
    tokenProgram: TOKEN_PROGRAM_ADDRESS,
  });

  const migrationAccounts =
    await cpmmMigrator.buildCpmmMigrationRemainingAccounts({
      launch,
      baseMint: baseMint.address,
      quoteMint: WSOL_MINT,
      launchAuthority,
      adminBaseAta,
      adminQuoteAta,
      recipientAtas: [],
      cpmmProgram: deployment.cpmmProgram,
      cpmmMigratorProgram: deployment.cpmmMigratorProgram,
    });

  const migratorInitPayload = cpmmMigrator.encodeRegisterLaunchPayload({
    cpmmConfig: migrationAccounts.cpmmConfig,
    initialSwapFeeBps: SWAP_FEE_BPS,
    initialFeeSplitBps: 10_000,
    recipients: [],
    minRaiseQuote: MIN_RAISE_QUOTE,
    minMigrationPriceQ64Opt: null,
    migratedPoolHookConfig: null,
  });
  const migratorMigratePayload = cpmmMigrator.encodeMigratePayload({
    baseForDistribution: 0n,
    baseForLiquidity: 0n,
  });

  const initializeLaunchIx =
    await initializer.createInitializeLaunchInstruction(
      {
        config: deployment.initializerConfig,
        launch,
        launchAuthority,
        baseMint,
        quoteMint: WSOL_MINT,
        baseVault,
        quoteVault,
        launchFeeState,
        payer,
        authority: payer,
        hookProgram: deployment.cpmmHookProgram,
        migratorProgram: deployment.cpmmMigratorProgram,
        cpmmConfig: migrationAccounts.cpmmConfig,
        baseTokenProgram: TOKEN_PROGRAM_ADDRESS,
        quoteTokenProgram: TOKEN_PROGRAM_ADDRESS,
        systemProgram: SYSTEM_PROGRAM_ADDRESS,
        rent: SYSVAR_RENT_ADDRESS,
      },
      {
        namespace,
        launchId,
        baseDecimals: BASE_DECIMALS,
        baseTotalSupply: BASE_TOTAL_SUPPLY,
        baseForDistribution: 0n,
        baseForLiquidity: 0n,
        curveVirtualBase: start.curveVirtualBase,
        curveVirtualQuote: start.curveVirtualQuote,
        swapFeeBps: SWAP_FEE_BPS,
        curveKind: initializer.CURVE_KIND_XYK,
        curveParams: new Uint8Array([initializer.CURVE_PARAMS_FORMAT_XYK_V0]),
        allowBuy: true,
        allowSell: true,
        hookFlags: initializer.HF_BEFORE_SWAP,
        hookPayload: new Uint8Array(),
        hookCreateRemainingAccountsLen: 0,
        hookCreateRemainingAccountsHash: new Uint8Array(32),
        migratorInitPayload,
        migratorMigratePayload,
        hookRemainingAccountsHash:
          initializer.computeRemainingAccountsHash([namespace]),
        migratorInitRemainingAccountsHash:
          initializer.computeRemainingAccountsHash([
            migrationAccounts.cpmmMigrationState,
            migrationAccounts.cpmmConfig,
          ]),
        migratorRemainingAccountsHash: migrationAccounts.hash,
        feeBeneficiaries: [{ wallet: payer.address, shareBps: 10_000 }],
        metadataName: '',
        metadataSymbol: '',
        metadataUri: '',
      },
      deployment.initializerProgram,
    );

  const signature = await send([initializeLaunchIx]);
  const launchAccount = await initializer.fetchLaunch(rpc, launch, {
    programId: deployment.initializerProgram,
  });

  console.log('Launch created:', launch);
  console.log('Base mint:', baseMint.address);
  console.log('Transaction:', signature);
  console.log(
    'Phase:',
    launchAccount && initializer.phaseLabel(launchAccount.phase),
  );
}

main();
```


# Dynamic fee launch

Create a Solana launch with a dynamic fee schedule

Pass `dynamicFee` to `createLaunch` to configure a fee schedule on the Doppler CPMM hook. The hook normalizes the schedule during the `BEFORE_CREATE` callback and stores the normalized schedule in the launch hook payload.

This snippet assumes `payer` and `rpc` are already initialized with `@solana/kit`.

```typescript
import {
  generateKeyPairSigner,
  type Address,
} from '@solana/kit';
import {
  createLaunch,
  cpmmHook,
  initializer,
  deriveSolanaCpmmDeployment,
  DOPPLER_SOLANA_DEVNET_PROGRAM_ADDRESSES,
} from '@whetstone-research/doppler-sdk/solana';

const WSOL_MINT =
  'So11111111111111111111111111111111111111112' as Address;

const deployment = await deriveSolanaCpmmDeployment(
  DOPPLER_SOLANA_DEVNET_PROGRAM_ADDRESSES,
);

const baseMint = await generateKeyPairSigner();
const baseVault = await generateKeyPairSigner();
const quoteVault = await generateKeyPairSigner();
const namespace = payer.address;
const launchId = initializer.createLaunchId();

const addresses = await initializer.deriveCreateLaunchAddresses({
  deployment,
  namespace,
  launchId,
  baseMint,
});

const { instruction } = await createLaunch({
  deployment,
  namespace,
  launchId,
  addresses,
  launchAccounts: {
    baseMint,
    quoteMint: WSOL_MINT,
    baseVault,
    quoteVault,
  },
  payer,
  authority: payer,
  supply: {
    baseDecimals: 6,
    baseTotalSupply: 1_000_000_000n * 10n ** 6n,
    baseForDistribution: 0n,
    baseForLiquidity: 0n,
  },
  curve: {
    curveVirtualBase: 1_000_000_000n * 10n ** 6n,
    curveVirtualQuote: 10n * 1_000_000_000n,
    swapFeeBps: 200,
  },
  dynamicFee: {
    startingTime: 0n,
    startFeeBps: 8_000,
    endFeeBps: 200,
    durationSeconds: 10n * 60n,
  },
  migration: {
    minRaiseQuote: 50n * 1_000_000_000n,
  },
  metadata: null,
  feeBeneficiaries: [{ wallet: payer.address, shareBps: 10_000 }],
});
```

Submit the returned `instruction` with the generated mint and vault signers. The SDK sets the CPMM hook program, `HF_BEFORE_CREATE | HF_BEFORE_SWAP`, the 32-byte schedule payload, the create-hook account commitment, and the swap hook remaining-account commitment.

`startingTime: 0n` means the hook should start the schedule at launch creation. During `initialize_launch`, the hook replaces it with the current Solana clock timestamp before the launch account is stored.

The effective swap fee is the greater of the current schedule fee and the launch's static `swapFeeBps`, so the schedule cannot reduce the fee below the static launch fee.

After sending the transaction, you can verify that the launch is using the CPMM hook:

```typescript
const launch = await initializer.fetchLaunch(rpc, addresses.launch, {
  programId: deployment.initializerProgram,
});

if (!launch) {
  throw new Error('Launch account not found');
}

const hookPayload = new Uint8Array(
  launch.hookPayload.bytes.slice(0, launch.hookPayload.len),
);

if (launch.hookProgram !== deployment.cpmmHookProgram) {
  throw new Error('Launch is not using the CPMM hook');
}

if (!cpmmHook.isDynamicFeeSchedulePayload(hookPayload)) {
  throw new Error('Launch hook payload does not contain a dynamic fee schedule');
}
```

To combine dynamic fees with cosigner gating, pass the same dynamic fee schedule plus `cosigner` and, optionally, `cosignGateExpiresAt`:

```typescript
const { instruction } = await createLaunch({
  // ...same launch inputs as above
  dynamicFee: {
    startingTime: 0n,
    startFeeBps: 8_000,
    endFeeBps: 200,
    durationSeconds: 10n * 60n,
  },
  cosigner,
  cosignGateExpiresAt,
});
```

With both features enabled, swaps require the configured cosigner until expiry, and the dynamic fee schedule continues to apply throughout the initializer trading phase. Omit `cosignGateExpiresAt` for an indefinite gate; in that case the schedule remains the entire hook payload and the presence of the cosigner config account enables gating.


# Swap

Easily swap Doppler created assets on Solana

```typescript
import {
  address,
  createKeyPairSignerFromBytes,
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  pipe,
  createTransactionMessage,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  appendTransactionMessageInstructions,
  signTransactionMessageWithSigners,
  sendAndConfirmTransactionFactory,
  getSignatureFromTransaction,
  type Address,
} from '@solana/kit';
import {
  TOKEN_PROGRAM_ADDRESS,
  findAssociatedTokenPda,
  getCreateAssociatedTokenIdempotentInstruction,
} from '@solana-program/token';
import {
  cpmm,
  deriveSolanaCpmmDeployment,
  DOPPLER_SOLANA_DEVNET_PROGRAM_ADDRESSES,
} from '@whetstone-research/doppler-sdk/solana';

const keypairJson = process.env.SOLANA_KEYPAIR;
const rpcUrl = process.env.SOLANA_RPC_URL ?? 'https://api.devnet.solana.com';
const wsUrl = process.env.SOLANA_WS_URL ?? 'wss://api.devnet.solana.com';

if (!keypairJson) {
  throw new Error('SOLANA_KEYPAIR must be set (JSON array of 64 bytes)');
}
if (!process.env.MINT_0 || !process.env.MINT_1) {
  throw new Error('MINT_0 and MINT_1 must be set');
}

const MINT_0: Address = address(process.env.MINT_0);
const MINT_1: Address = address(process.env.MINT_1);

async function main() {
  const payer = await createKeyPairSignerFromBytes(
    new Uint8Array(JSON.parse(keypairJson as string)),
  );
  const rpc = createSolanaRpc(rpcUrl);
  const rpcSubscriptions = createSolanaRpcSubscriptions(wsUrl);
  const deployment = await deriveSolanaCpmmDeployment(
    DOPPLER_SOLANA_DEVNET_PROGRAM_ADDRESSES,
  );

  const poolResult = await cpmm.getPoolByMints(rpc, MINT_0, MINT_1, {
    programId: deployment.cpmmProgram,
  });
  if (!poolResult) {
    throw new Error(`No pool found for ${MINT_0} / ${MINT_1}`);
  }

  const { address: poolAddress, account: pool } = poolResult;
  const tradeDirection = (process.env.SWAP_DIRECTION === '1' ? 1 : 0) as
    | 0
    | 1;
  const amountIn = BigInt(process.env.AMOUNT_IN ?? '1000000');
  const quote = cpmm.getSwapQuote(pool, amountIn, tradeDirection);
  const minAmountOut = (quote.amountOut * 9950n) / 10000n;

  const [userToken0] = await findAssociatedTokenPda({
    owner: payer.address,
    mint: pool.token0Mint,
    tokenProgram: TOKEN_PROGRAM_ADDRESS,
  });
  const [userToken1] = await findAssociatedTokenPda({
    owner: payer.address,
    mint: pool.token1Mint,
    tokenProgram: TOKEN_PROGRAM_ADDRESS,
  });

  const createUserToken0Ix = getCreateAssociatedTokenIdempotentInstruction({
    payer,
    ata: userToken0,
    owner: payer.address,
    mint: pool.token0Mint,
  });
  const createUserToken1Ix = getCreateAssociatedTokenIdempotentInstruction({
    payer,
    ata: userToken1,
    owner: payer.address,
    mint: pool.token1Mint,
  });
  const swapIx = cpmm.createSwapInstruction({
    config: deployment.cpmmConfig,
    pool: poolAddress,
    authority: pool.authority,
    vault0: pool.vault0,
    vault1: pool.vault1,
    token0Mint: pool.token0Mint,
    token1Mint: pool.token1Mint,
    userToken0,
    userToken1,
    user: payer,
    amountIn,
    minAmountOut,
    tradeDirection,
    programId: deployment.cpmmProgram,
  });

  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
  const transactionMessage = pipe(
    createTransactionMessage({ version: 0 }),
    (tx) => setTransactionMessageFeePayerSigner(payer, tx),
    (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
    (tx) =>
      appendTransactionMessageInstructions(
        [createUserToken0Ix, createUserToken1Ix, swapIx],
        tx,
      ),
  );

  const signedTransaction =
    await signTransactionMessageWithSigners(transactionMessage);
  const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
    rpc,
    rpcSubscriptions,
  });

  await sendAndConfirmTransaction(
    signedTransaction as Parameters<typeof sendAndConfirmTransaction>[0],
    { commitment: 'confirmed' },
  );

  console.log('Swap confirmed:', getSignatureFromTransaction(signedTransaction));
}

main();
```


# Launch, monitor, and e2e

Create a Solana launch, buy from the curve, and migrate to CPMM

```typescript
import {
  AccountRole,
  createKeyPairSignerFromBytes,
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  generateKeyPairSigner,
  getBase64EncodedWireTransaction,
  pipe,
  createTransactionMessage,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  appendTransactionMessageInstructions,
  signTransactionMessageWithSigners,
  sendAndConfirmTransactionFactory,
  getSignatureFromTransaction,
  type Address,
  type Instruction,
} from '@solana/kit';
import {
  TOKEN_PROGRAM_ADDRESS,
  findAssociatedTokenPda,
  getCreateAssociatedTokenIdempotentInstruction,
} from '@solana-program/token';
import { SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system';
import { SYSVAR_RENT_ADDRESS } from '@solana/sysvars';
import {
  cpmm,
  cpmmMigrator,
  initializer,
  deriveSolanaCpmmDeployment,
  DOPPLER_SOLANA_DEVNET_PROGRAM_ADDRESSES,
} from '@whetstone-research/doppler-sdk/solana';

const keypairJson = process.env.SOLANA_KEYPAIR;
const rpcUrl = process.env.SOLANA_RPC_URL ?? 'https://api.devnet.solana.com';
const wsUrl = process.env.SOLANA_WS_URL ?? 'wss://api.devnet.solana.com';

if (!keypairJson) {
  throw new Error('SOLANA_KEYPAIR must be set (JSON array of 64 bytes)');
}

const USDC_MINT: Address =
  '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU' as Address;

async function main() {
  const payer = await createKeyPairSignerFromBytes(
    new Uint8Array(JSON.parse(keypairJson as string)),
  );
  const rpc = createSolanaRpc(rpcUrl);
  const rpcSubscriptions = createSolanaRpcSubscriptions(wsUrl);
  const deployment = await deriveSolanaCpmmDeployment(
    DOPPLER_SOLANA_DEVNET_PROGRAM_ADDRESSES,
  );
  const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({
    rpc,
    rpcSubscriptions,
  });

  async function send(instructions: Instruction[]) {
    const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
    const message = pipe(
      createTransactionMessage({ version: 0 }),
      (tx) => setTransactionMessageFeePayerSigner(payer, tx),
      (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
      (tx) => appendTransactionMessageInstructions(instructions, tx),
    );
    const signedTransaction = await signTransactionMessageWithSigners(message);
    await sendAndConfirmTransaction(
      signedTransaction as Parameters<typeof sendAndConfirmTransaction>[0],
      { commitment: 'confirmed' },
    );
    return getSignatureFromTransaction(signedTransaction);
  }

  const BASE_DECIMALS = 6;
  const BASE_TOTAL_SUPPLY = 1_000_000_000n * 10n ** BigInt(BASE_DECIMALS);
  const MIN_RAISE_QUOTE = 100_000n;
  const BUY_AMOUNT_IN = 200_000n;
  const SWAP_FEE_BPS = 200;

  const { start } = cpmm.marketCapToCurveParams({
    startMarketCapUSD: 100_000,
    endMarketCapUSD: 10_000_000,
    baseTotalSupply: BASE_TOTAL_SUPPLY,
    baseForCurve: BASE_TOTAL_SUPPLY,
    baseDecimals: BASE_DECIMALS,
    quoteDecimals: 6,
    numerairePriceUSD: 1,
  });

  const baseMint = await generateKeyPairSigner();
  const baseVault = await generateKeyPairSigner();
  const quoteVault = await generateKeyPairSigner();

  const namespace = payer.address;
  const withDynamicHookAccounts = (instruction: Instruction): Instruction => ({
    ...instruction,
    accounts: [
      ...(instruction.accounts ?? []),
      { address: namespace, role: AccountRole.READONLY },
    ],
  });
  const launchId = initializer.launchIdFromU64(BigInt(Date.now()));
  const [launch] = await initializer.getLaunchAddress(
    namespace,
    launchId,
    deployment.initializerProgram,
  );
  const [launchAuthority] = await initializer.getLaunchAuthorityAddress(
    launch,
    deployment.initializerProgram,
  );
  const [launchFeeState] = await initializer.getLaunchFeeStateAddress(
    launch,
    deployment.initializerProgram,
  );
  const [payerBaseAta] = await findAssociatedTokenPda({
    owner: payer.address,
    mint: baseMint.address,
    tokenProgram: TOKEN_PROGRAM_ADDRESS,
  });
  const [payerQuoteAta] = await findAssociatedTokenPda({
    owner: payer.address,
    mint: USDC_MINT,
    tokenProgram: TOKEN_PROGRAM_ADDRESS,
  });

  const migrationAccounts =
    await cpmmMigrator.buildCpmmMigrationRemainingAccounts({
      launch,
      baseMint: baseMint.address,
      quoteMint: USDC_MINT,
      launchAuthority,
      adminBaseAta: payerBaseAta,
      adminQuoteAta: payerQuoteAta,
      recipientAtas: [],
      cpmmProgram: deployment.cpmmProgram,
      cpmmMigratorProgram: deployment.cpmmMigratorProgram,
    });

  const migratorInitPayload = cpmmMigrator.encodeRegisterLaunchPayload({
    cpmmConfig: migrationAccounts.cpmmConfig,
    initialSwapFeeBps: SWAP_FEE_BPS,
    initialFeeSplitBps: 10_000,
    recipients: [],
    minRaiseQuote: MIN_RAISE_QUOTE,
    minMigrationPriceQ64Opt: null,
    migratedPoolHookConfig: null,
  });
  const migratorMigratePayload = cpmmMigrator.encodeMigratePayload({
    baseForDistribution: 0n,
    baseForLiquidity: 0n,
  });

  const initializeLaunchIx =
    await initializer.createInitializeLaunchInstruction(
      {
        config: deployment.initializerConfig,
        launch,
        launchAuthority,
        baseMint,
        quoteMint: USDC_MINT,
        baseVault,
        quoteVault,
        launchFeeState,
        payer,
        authority: payer,
        hookProgram: deployment.cpmmHookProgram,
        migratorProgram: deployment.cpmmMigratorProgram,
        cpmmConfig: migrationAccounts.cpmmConfig,
        baseTokenProgram: TOKEN_PROGRAM_ADDRESS,
        quoteTokenProgram: TOKEN_PROGRAM_ADDRESS,
        systemProgram: SYSTEM_PROGRAM_ADDRESS,
        rent: SYSVAR_RENT_ADDRESS,
      },
      {
        namespace,
        launchId,
        baseDecimals: BASE_DECIMALS,
        baseTotalSupply: BASE_TOTAL_SUPPLY,
        baseForDistribution: 0n,
        baseForLiquidity: 0n,
        curveVirtualBase: start.curveVirtualBase,
        curveVirtualQuote: start.curveVirtualQuote,
        swapFeeBps: SWAP_FEE_BPS,
        curveKind: initializer.CURVE_KIND_XYK,
        curveParams: new Uint8Array([initializer.CURVE_PARAMS_FORMAT_XYK_V0]),
        allowBuy: true,
        allowSell: true,
        hookFlags: initializer.HF_BEFORE_SWAP,
        hookPayload: new Uint8Array(),
        hookCreateRemainingAccountsLen: 0,
        hookCreateRemainingAccountsHash: new Uint8Array(32),
        migratorInitPayload,
        migratorMigratePayload,
        hookRemainingAccountsHash:
          initializer.computeRemainingAccountsHash([namespace]),
        migratorInitRemainingAccountsHash:
          initializer.computeRemainingAccountsHash([
            migrationAccounts.cpmmMigrationState,
            migrationAccounts.cpmmConfig,
          ]),
        migratorRemainingAccountsHash: migrationAccounts.hash,
        feeBeneficiaries: [{ wallet: payer.address, shareBps: 10_000 }],
        metadataName: '',
        metadataSymbol: '',
        metadataUri: '',
      },
      deployment.initializerProgram,
    );

  console.log('Launch transaction:', await send([initializeLaunchIx]));
  console.log('Launch:', launch);
  console.log('Base mint:', baseMint.address);

  const previewIx = withDynamicHookAccounts(
    initializer.createPreviewSwapExactInInstruction(
      {
        launch,
        launchFeeState,
        baseVault: baseVault.address,
        quoteVault: quoteVault.address,
        hookProgram: deployment.cpmmHookProgram,
      },
      {
        amountIn: BUY_AMOUNT_IN,
        tradeDirection: initializer.TRADE_DIRECTION_BUY,
      },
      deployment.initializerProgram,
    ),
  );
  const { value: previewBlockhash } = await rpc.getLatestBlockhash().send();
  const previewMessage = pipe(
    createTransactionMessage({ version: 0 }),
    (tx) => setTransactionMessageFeePayerSigner(payer, tx),
    (tx) => setTransactionMessageLifetimeUsingBlockhash(previewBlockhash, tx),
    (tx) => appendTransactionMessageInstructions([previewIx], tx),
  );
  const signedPreview = await signTransactionMessageWithSigners(previewMessage);
  const { value: simulateResult } = await rpc
    .simulateTransaction(getBase64EncodedWireTransaction(signedPreview), {
      encoding: 'base64',
      replaceRecentBlockhash: true,
    })
    .send();
  const returnData = simulateResult.returnData?.data;
  if (returnData) {
    const bytes = Uint8Array.from(atob(returnData[0]), (c) => c.charCodeAt(0));
    const preview = initializer.decodePreviewSwapExactInResult(bytes);
    console.log('Preview amount out:', preview.amountOut.toString());
  }

  const createBaseAtaIx = getCreateAssociatedTokenIdempotentInstruction({
    payer,
    ata: payerBaseAta,
    owner: payer.address,
    mint: baseMint.address,
  });
  const createQuoteAtaIx = getCreateAssociatedTokenIdempotentInstruction({
    payer,
    ata: payerQuoteAta,
    owner: payer.address,
    mint: USDC_MINT,
  });
  const buyIx = withDynamicHookAccounts(
    initializer.createCurveSwapExactInInstruction(
      {
        config: deployment.initializerConfig,
        launch,
        launchAuthority,
        baseVault: baseVault.address,
        quoteVault: quoteVault.address,
        launchFeeState,
        userBaseAccount: payerBaseAta,
        userQuoteAccount: payerQuoteAta,
        baseMint: baseMint.address,
        quoteMint: USDC_MINT,
        user: payer,
        hookProgram: deployment.cpmmHookProgram,
        baseTokenProgram: TOKEN_PROGRAM_ADDRESS,
        quoteTokenProgram: TOKEN_PROGRAM_ADDRESS,
      },
      {
        amountIn: BUY_AMOUNT_IN,
        minAmountOut: 1n,
        tradeDirection: initializer.TRADE_DIRECTION_BUY,
      },
      deployment.initializerProgram,
    ),
  );
  console.log(
    'Buy transaction:',
    await send([createBaseAtaIx, createQuoteAtaIx, buyIx]),
  );

  const migrateLaunchIxBase = initializer.createMigrateLaunchInstruction(
    {
      config: deployment.initializerConfig,
      launch,
      launchAuthority,
      baseMint: baseMint.address,
      quoteMint: USDC_MINT,
      baseVault: baseVault.address,
      quoteVault: quoteVault.address,
      launchFeeState,
      migratorProgram: deployment.cpmmMigratorProgram,
      payer,
      baseTokenProgram: TOKEN_PROGRAM_ADDRESS,
      quoteTokenProgram: TOKEN_PROGRAM_ADDRESS,
      systemProgram: SYSTEM_PROGRAM_ADDRESS,
      rent: SYSVAR_RENT_ADDRESS,
    },
    deployment.initializerProgram,
  );
  const migrateLaunchIx = {
    ...migrateLaunchIxBase,
    accounts: [
      ...(migrateLaunchIxBase.accounts ?? []),
      ...migrationAccounts.metas,
    ],
  };

  console.log('Migration transaction:', await send([migrateLaunchIx]));
}

main();
```


# Data Indexing

## What is the Doppler Indexer?

The Doppler Indexer is a multi-chain blockchain indexing service built for tracking the state of Doppler protocol contracts and programs, and the tokens created by them. It tracks Uniswap V2, V3, and V4 protocol deployments across multiple EVM-compatible chains, plus Solana initializer and CPMM markets where Solana indexing is enabled.

The primary goal of this indexer is to provide a reliable, fast, and queryable data source for Doppler-related analytics, front-end applications, and market analysis tools. It aggregates data on pools, tokens, swaps, liquidity, and user activity, normalizing it into a consistent schema.

## What is Ponder?

This project is built using [Ponder](https://ponder.sh/), a powerful open-source framework for building backend applications on top of blockchain data. Ponder simplifies the process of indexing event data from EVM chains by:

* Providing a declarative way to define which contracts and events to watch.
* Handling real-time indexing, re-orgs, and RPC interactions.
* Generating a type-safe GraphQL API based on a defined schema.
* Using a standard PostgreSQL database for data storage.

Ponder provides the core indexing engine, while this repository contains the necessary business logic for processing and enriching Doppler protocol data.

## Core Concepts

### Supported Chains

The indexer is configured to support the following networks:

| Name              | Chain ID        | Purpose                                        |
| ----------------- | --------------- | ---------------------------------------------- |
| **Mainnet**       | `1`             | ETH Price Oracle                               |
| **Base**          | `8453`          | V2, V3, V4 Protocol Indexing                   |
| **Unichain**      | `130`           | V2, V3, V4 Protocol Indexing                   |
| **Ink**           | `57073`         | V2, V3, V4 Protocol Indexing                   |
| **Base Sepolia**  | `84532`         | V2, V3, V4 Protocol Indexing (Testnet)         |
| **Solana Devnet** | `solana:devnet` | Initializer and CPMM market indexing (Testnet) |

The configuration is modular, making it straightforward to add or remove chains. See the [Development Guide](broken://pages/VZBnc78XqauN19pCmwWr) for more details.

### Supported Protocols & Events

The indexer listens to a variety of events across different protocol versions to build a complete picture of market activity.

| Contract               | Event              | Protocol | Description                                                                               |
| ---------------------- | ------------------ | -------- | ----------------------------------------------------------------------------------------- |
| `Airlock`              | `Migrate`          | V2       | A V3 pool migrating its liquidity to a new V2 pool.                                       |
| `UniswapV2Pair`        | `Swap`             | V2       | A trade on a Uniswap V2-style pair.                                                       |
| `UniswapV3Initializer` | `Create`           | V3       | Creation of a new Doppler V3 pool.                                                        |
| `UniswapV3Pool`        | `Mint`, `Burn`     | V3       | Adding or removing concentrated liquidity.                                                |
| `UniswapV3Pool`        | `Swap`             | V3       | A trade on a Doppler V3 pool.                                                             |
| `UniswapV4Initializer` | `Create`           | V4       | Creation of a new Doppler V4 pool.                                                        |
| `UniswapV4Pool`        | `Swap`             | V4       | A trade on a Doppler V4 pool with hooks.                                                  |
| `DERC20`, `V4DERC20`   | `Transfer`         | V3/V4    | DERC20 token transfers, used to track user balances.                                      |
| Solana Initializer     | Launch account     | SVM      | Doppler launch state, including hook program, flags, payload, and remaining-account hash. |
| Solana CPMM            | Pool/swap accounts | SVM      | Post-migration CPMM pool state, swaps, and market activity.                               |
| (Block Handlers)       | `block`            | -        | Periodic tasks for oracle updates and data refresh.                                       |

## Database Schema Overview

The database schema is defined in `ponder.schema.ts`. It is designed to store normalized data from all supported protocols.

### Core Entities

* `user`: Represents a user's wallet address.
* `token`: Detailed information about an ERC20 token, including metadata, total supply, and market data.
* `asset`: Represents a DERC20 token in the context of its specific Doppler pool, linking it to governance, migrators, etc.
* `pool`: The central entity for a liquidity pool, storing aggregated data like price, liquidity, volume, and references to its tokens. This table holds data for V3 and V4 pools.
* `v2Pool`: A specific table for V2 pools that are created via migration, linked back to a `pool` entity.
* `swap`: A record of an individual trade, including amounts, user, price, and USD value.
* `position`: For V3 concentrated liquidity, tracking a user's liquidity position within specific tick ranges.

### Time-Series Data

* `eth_price`: Stores the price of ETH in USD, fetched periodically from a Chainlink oracle.
* `hour_bucket_usd`: Aggregated OHLC (Open, High, Low, Close) price data in USD for each pool, bucketed by the hour.
* `daily_volume`: Tracks rolling 24-hour trading volume for each pool.

## Quirks & Limitations

### Quote Currency

**The indexer is fundamentally designed to work with pools quoted in ETH or WETH.**

* All USD price calculations (`priceUsd`, `liquidityUsd`, `volumeUsd`) are derived by converting the quote currency (ETH) value to USD using the Chainlink ETH/USD oracle price.
* Pools that use a different numeraire (e.g., USDC) are not fully supported for USD-denominated analytics. While their data will be indexed, USD values may be incorrect or zero.
* The system identifies the quote token as ETH by checking if its address is the `zeroAddress` or the configured WETH address for that chain.

### Token Metadata Fetching

The indexer attempts to fetch and store metadata (name, symbol, image) for each token from its `tokenURI`.

* **Supported Formats**:
  1. **IPFS**: URIs starting with `ipfs://` are resolved using a Pinata gateway. The indexer looks for an `image` or `image_hash` field in the returned JSON.
  2. **HTTP(S)**: Direct HTTP/S links are fetched.
* **Retry Mechanism for Failed Fetches**:
  * If fetching metadata fails, the token is added to a `pending_token_images` table in the database.
  * A periodic block handler (`PendingTokenImagesBase:block`) runs every 50 blocks.
  * This handler retries fetching the images for pending tokens.
  * Each token has a 5-minute cooldown between retries and a maximum of 10 retry attempts before being abandoned.
  * This ensures that transient network issues or delayed metadata uploads don't prevent images from being indexed eventually.


# Indexer API

The indexer exposes the indexed data through a GraphQL API and a RESTful search endpoint.

{% hint style="success" %}
[Whetstone Research](https://whetstone.cc) hosts a free endpoint that supports Base Sepolia for development.

<https://test.indexer.doppler.lol/>

Production endpoints are available upon request.

Indexing APIs that support Solana are a work in progress.
{% endhint %}

## GraphQL API

The primary way to query data is through the GraphQL endpoint, available at `/graphql`. It is strongly typed and supports complex queries, filtering, and pagination.

### Example Queries

1. **Fetch top 5 pools on Base Sepolia by USD liquidity:**

   ```graphql
   query TopPoolsByLiquidity {
     pools(
       where: { chainId: 84532 }
       orderBy: "dollarLiquidity"
       orderDirection: "desc"
       limit: 5
     ) {
       items {
         address
         dollarLiquidity
         volumeUsd
         baseToken {
           symbol
         }
         quoteToken {
           symbol
         }
       }
     }
   }
   ```
2. **Get recent swaps for a specific pool:**

   ```graphql
   query RecentSwaps {
     swaps(
       where: { pool: "0x..." }
       orderBy: "timestamp"
       orderDirection: "desc"
       limit: 10
     ) {
       items {
         txHash
         timestamp
         type
         amountIn
         amountOut
         swapValueUsd
       }
     }
   }
   ```
3. **Fetch detailed information for a single token:**

   ```graphql
   query TokenDetails {
     token(address: "0x...", chainId: 84532) {
       address
       name
       symbol
       decimals
       image
       volumeUsd
       holderCount
       pool {
         address
         price
       }
     }
   }
   ```

## REST API

A simple REST endpoint is available for searching tokens.

### Search Endpoint

* **URL**: `/search/:query`
* **Method**: `GET`
* **Description**: Searches for tokens by name, symbol, or address.
* **URL Parameters**:
  * `query`: The search term (e.g., "MyToken", "MTK", or "0x...").
* **Query Parameters**:
  * `chain_ids`: A comma-separated list of chain IDs to filter the search (e.g., `?chain_ids=84532,130`).

**Example Usage:**

```bash
# Search for "doppler" token on Base Sepolia (chain ID 84532)
curl "http://localhost:42069/search/doppler?chain_ids=84532"

# Search by contract address on Base Sepolia and Ink
curl "http://localhost:42069/search/0x123...abc?chain_ids=84532,57073"
```

## Direct SQL Access (Development)

For development and debugging, you can connect directly to the PostgreSQL database. Use `pnpm db shell` to get an interactive psql session. You can also use any standard SQL client with the connection string from your `.env.local` file.


# Quotes & swaps

Use the Doppler SDK to discover a pool and quote a swap. Then pass the resulting pool, direction, and amounts to Uniswap's [Universal Router SDK](https://github.com/Uniswap/sdks/tree/main/sdks/universal-router-sdk) to construct and execute the transaction.

This guide focuses on the information Doppler provides for:

* **Dynamic auctions**
* **Multicurve**
* **Multicurve Rehype**

All three are Uniswap V4 pools. They use the same quoting API but expose their initialized `PoolKey` differently.

## Install

```bash
pnpm add @whetstone-research/doppler-sdk viem
```

## Set up the SDK

Only a public client is required to discover pools and request quotes:

```ts
import {
  DopplerSDK,
  getAddresses,
} from '@whetstone-research/doppler-sdk/evm'
import { createPublicClient, http } from 'viem'
import { base } from 'viem/chains'

const publicClient = createPublicClient({
  chain: base,
  transport: http(rpcUrl),
})

const sdk = new DopplerSDK({
  publicClient,
  chainId: base.id,
})

const addresses = getAddresses(base.id)
```

Use `addresses.universalRouter` when executing the swap. It is the Doppler-compatible Universal Router for the selected chain.

Use Universal Router version:

* `2.1.1` on Robinhood (chain ID `4663`)
* `2.0` on every other supported network

{% hint style="warning" %}
Do not substitute a same-chain Universal Router from another address registry. Each Universal Router deployment is connected to a specific V4 `PoolManager`. Another router on the same chain may use a different `PoolManager`, where the Doppler pool is not initialized.
{% endhint %}

## Get the V4 `PoolKey`

The `PoolKey` contains the exact currencies, fee configuration, tick spacing, and hook address of an initialized V4 pool. Use the key returned from onchain state without reconstructing or modifying it.

### Multicurve and Multicurve Rehype

Look up either pool type by its Doppler asset address:

```ts
const pool = await sdk.getMulticurvePool(assetAddress)
const poolState = await pool.getState()

const poolKey = poolState.poolKey
```

For a buy, the input is normally the pool's numeraire:

```ts
const currencyInAddress = poolState.numeraire
```

For a sell, the input is the Doppler asset:

```ts
const currencyInAddress = poolState.asset
```

Use `poolState.poolKey` unchanged for both Multicurve and Multicurve Rehype pools.

### Dynamic auctions

A Dynamic auction stores its initialized `PoolKey` on its hook:

```ts
import {
  dopplerHookAbi,
  normalizePoolKey,
} from '@whetstone-research/doppler-sdk/evm'

const storedPoolKey = await publicClient.readContract({
  address: hookAddress,
  abi: dopplerHookAbi,
  functionName: 'poolKey',
})

const poolKey = normalizePoolKey(storedPoolKey)
```

The returned key already contains the Dynamic auction's fee flag, tick spacing, currencies, and hook address.

## Determine the swap direction

Uniswap V4 uses `zeroForOne` to identify the input side:

* `true`: swap `currency0` for `currency1`
* `false`: swap `currency1` for `currency0`

Derive it from the requested input currency:

```ts
function sameAddress(left: string, right: string) {
  return left.toLowerCase() === right.toLowerCase()
}

const inputIsCurrency0 = sameAddress(
  currencyInAddress,
  poolKey.currency0,
)
const inputIsCurrency1 = sameAddress(
  currencyInAddress,
  poolKey.currency1,
)

if (!inputIsCurrency0 && !inputIsCurrency1) {
  throw new Error('Input currency is not part of this PoolKey')
}

const zeroForOne = inputIsCurrency0
const currencyOutAddress = zeroForOne
  ? poolKey.currency1
  : poolKey.currency0
```

V4 represents native currency as the zero address. Wrapped native currency uses its ERC-20 address.

## Quote an exact-input swap

Use base units for `amountIn`:

```ts
const quote = await sdk.quoter.quoteExactInputV4({
  poolKey,
  zeroForOne,
  exactAmount: amountIn,
  hookData: '0x',
})

console.log('Expected output:', quote.amountOut)
console.log('Quoter gas estimate:', quote.gasEstimate)
```

For example, use `parseUnits('1', decimals)` for an ERC-20 or `parseEther('0.01')` for an 18-decimal native or wrapped-native input.

Select the Universal Router SDK encoding version for the current chain:

```ts
const universalRouterVersion =
  publicClient.chain.id === 4_663 ? '2.1.1' : '2.0'
```

The values required for a single-pool exact-input V4 swap are now:

```ts
const swapQuote = {
  poolKey,
  zeroForOne,
  currencyInAddress,
  currencyOutAddress,
  amountIn,
  amountOut: quote.amountOut,
  hookData: '0x',
  universalRouter: addresses.universalRouter,
  universalRouterVersion,
}
```

Apply your application's slippage policy to the quote when constructing the router transaction.

## Quote an exact-output swap

To calculate the input required for an exact output:

```ts
const exactOutputQuote = await sdk.quoter.quoteExactOutputV4({
  poolKey,
  zeroForOne,
  exactAmount: amountOut,
  hookData: '0x',
})

console.log('Required input:', exactOutputQuote.amountIn)
console.log('Quoter gas estimate:', exactOutputQuote.gasEstimate)
```

Apply your application's slippage policy to `exactOutputQuote.amountIn` when determining the maximum input.

## Execute with Uniswap Universal Router

Use the values above with Uniswap's official [Universal Router SDK](https://github.com/Uniswap/sdks/tree/main/sdks/universal-router-sdk). Its documentation covers transaction construction, currency objects, Permit2 or token approvals, slippage protection, native value, simulation, and submission.

When building the transaction:

* Use `addresses.universalRouter`.
* Select version `2.1.1` on Robinhood (chain ID `4663`).
* Select version `2.0` on every other supported network.
* Pass the exact `poolKey` returned by the Doppler SDK.
* Pass the derived `zeroForOne` value.
* Use empty hook data (`0x`) for these swaps.
* Simulate the final transaction before submitting it.

## V2 and V3 quotes

V2 and V3 are relevant for migrated pools and older Doppler deployments. New Dynamic, Multicurve, and Multicurve Rehype integrations should use the V4 path above.

### V3 exact-input quote

```ts
const quote = await sdk.quoter.quoteExactInputV3({
  tokenIn: tokenInAddress,
  tokenOut: tokenOutAddress,
  amountIn,
  fee: 3_000,
  sqrtPriceLimitX96: 0n,
})

console.log('Expected output:', quote.amountOut)
```

Use the initialized V3 pool's actual fee tier.

### V2 exact-input quote

```ts
const amounts = await sdk.quoter.quoteExactInputV2({
  amountIn,
  path: [tokenInAddress, tokenOutAddress],
})

const amountOut = amounts.at(-1)
if (amountOut === undefined) {
  throw new Error('V2 quoter returned an empty path')
}
```

Use the resulting route and quote with the corresponding V2 or V3 operation in the Universal Router SDK.

## Quote troubleshooting

**The pool cannot be quoted.** Read the `PoolKey` from `getState()` or the Dynamic hook and use it unchanged. Do not guess the fee, tick spacing, currency order, or hook address.

**The quoter returns `NotEnoughLiquidity`.** Verify `currencyInAddress`, `zeroForOne`, amount units, and current pool state. The requested amount may also be outside the pool's active liquidity.

**The quote succeeds but transaction construction or execution fails.** Verify that the Universal Router integration uses `getAddresses(chainId).universalRouter`. Use version `2.1.1` on Robinhood and version `2.0` on every other supported network. Then refer to the Universal Router SDK documentation for approvals, encoding, and execution.


# Contract addresses

Here are the networks that Doppler is officially deployed to:

* Mainnets: Ethereum Mainnet, Monad Mainnet, Robinhood Mainnet, Base
* Testnets: Base Sepolia

{% hint style="danger" %}
If there are contracts not reflected here but claiming to be instances of Doppler, they are not considered canonical. Use with caution. :rotating\_light:
{% endhint %}

## Mainnet Deployments

### Ethereum Mainnet (1)

| Contract                     | Address                                                                                  | Transaction                                                                                                 | Commit                                                                   |
| ---------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Airlock                      | [0xde35...9dfa](https://etherscan.io/address/0xde3599a2ec440b296373a983c85c365da55d9dfa) | [0x7542...eb79](https://etherscan.io/tx/0x7542807009d53ce3fb34710c6025c690617183a27247fddca95ff3fdaf6ceb79) | [afc1226](https://github.com/whetstoneresearch/doppler/commit/afc1226)   |
| DopplerCreateXDeployer       | [0x1030...9b83](https://etherscan.io/address/0x103004e50bed65dfba30dd9c264b6bdf5e529b83) | [0xb22d...148d](https://etherscan.io/tx/0xb22dbf1620d402f161fab9e8baa44caea7cfd4c66b01db104622e9999f29148d) | [9b60ad7d](https://github.com/whetstoneresearch/doppler/commit/9b60ad7d) |
| DopplerDeployer              | [0xb354...e421](https://etherscan.io/address/0xb35469ee64a87afd19b31615094fe3962d73e421) | [0x0234...cd37](https://etherscan.io/tx/0x0234f396ddf05e6385ed323f9b0c7ca850af8d9111f99adc5d94b52bdfcecd37) | [5aa31e1](https://github.com/whetstoneresearch/doppler/commit/5aa31e1)   |
| DopplerERC20V1               | [0xdb7b...be87](https://etherscan.io/address/0xdb7b520bb5c3a2c5d4871198081911359f93be87) | [0x9739...9ea3](https://etherscan.io/tx/0x973953be41cb739225752d5360e45e722c1db956c677cf83d92d75bd633a9ea3) | [fce45321](https://github.com/whetstoneresearch/doppler/commit/fce45321) |
| DopplerERC20V1Factory        | [0x89c2...5292](https://etherscan.io/address/0x89c261c05b5f9b6bcba07c199b8dee7cfad45292) | [0x9739...9ea3](https://etherscan.io/tx/0x973953be41cb739225752d5360e45e722c1db956c677cf83d92d75bd633a9ea3) | [fce45321](https://github.com/whetstoneresearch/doppler/commit/fce45321) |
| DopplerHookInitializer       | [0xbdf9...6544](https://etherscan.io/address/0xbdf938149ac6a781f94faa0ed45e6a0e984c6544) | [0x1bdc...d761](https://etherscan.io/tx/0x1bdcb1656ed4f1db504f35bd0c3cd56cfa2c3cf4f8705edd7a9b72b6b7fdd761) | [0154a5f](https://github.com/whetstoneresearch/doppler/commit/0154a5f)   |
| DopplerHookMigrator          | [0x1e40...60c4](https://etherscan.io/address/0x1e40b0875dda35f41e15cfb475403859b8c860c4) | [0x37a7...4fa7](https://etherscan.io/tx/0x37a718f132ed55a9726f5af7a048a89db0f64f124021dd241f6b4bf522e64fa7) | [5fe4eb1](https://github.com/whetstoneresearch/doppler/commit/5fe4eb1)   |
| GovernanceFactory            | [0x9f30...8f61](https://etherscan.io/address/0x9f309d79bee3e8b2f56facf74b7195df176c8f61) | [0x78e8...9329](https://etherscan.io/tx/0x78e8ab7e241f65ceb91277e1882787a68abf5b5759419e735b9c1f2974d19329) | [f136b8e](https://github.com/whetstoneresearch/doppler/commit/f136b8e)   |
| LaunchpadGovernanceFactory   | [0x2a8d...8175](https://etherscan.io/address/0x2a8da67f277442cff7dc2143e52fdd0cbf5a8175) | [0x36ea...c758](https://etherscan.io/tx/0x36ea6ede06c9278d94fd6568532e1c727f74cbc450568e57e2a805cc157ac758) | [63dc78fe](https://github.com/whetstoneresearch/doppler/commit/63dc78fe) |
| LockableUniswapV3Initializer | [0xa2e0...56d7](https://etherscan.io/address/0xa2e0435225d52a8b950122752978007d758056d7) | [0x474b...2235](https://etherscan.io/tx/0x474bf89ff79a2a3d93051c80c3245499450a84f9c5e47ec7bdf1059907b62235) | [63dc78fe](https://github.com/whetstoneresearch/doppler/commit/63dc78fe) |
| NoOpGovernanceFactory        | [0xddae...b638](https://etherscan.io/address/0xddae8b3ed08184682f7bc32b74d943ceefeab638) | [0xf243...b557](https://etherscan.io/tx/0xf2433fc6c439311f49e728aca9406c0f94eb10a16a3547012905971b2bb5b557) | [074a3b8](https://github.com/whetstoneresearch/doppler/commit/074a3b8)   |
| NoOpMigrator                 | [0x233a...a5b5](https://etherscan.io/address/0x233a71a7bb928b1357a1ebf454298320989ca5b5) | [0x2425...7652](https://etherscan.io/tx/0x2425c8b9f7c4fe9cea80e2cc09d424c5b3c92f6af9407bfb1d3f872923c47652) | [bc477e4](https://github.com/whetstoneresearch/doppler/commit/bc477e4)   |
| Quoter                       | [0xc9be...0567](https://etherscan.io/address/0xc9be59b344ec66547a4c697aa374e959d59b0567) | [0xd309...c7ab](https://etherscan.io/tx/0xd309d4718e9717bcf2dae1933a0c2440695663f49860177dc6fbdbac2c57c7ab) | [6a0ff821](https://github.com/whetstoneresearch/doppler/commit/6a0ff821) |
| RehypeDopplerHookInitializer | [0xbd54...45c5](https://etherscan.io/address/0xbd54a9e1d2249185a27af097abaa930631ec45c5) | [0xdc2b...e23f](https://etherscan.io/tx/0xdc2b244ec21ba7fd444af276097e3ce05d9f88235e1a52972390b9df87efe23f) | [6a0ff821](https://github.com/whetstoneresearch/doppler/commit/6a0ff821) |
| RehypeDopplerHookMigrator    | [0x6607...c84b](https://etherscan.io/address/0x660740d7d6fb2c8998fa3fff459cceb9ac12c84b) | [0xd309...c7ab](https://etherscan.io/tx/0xd309d4718e9717bcf2dae1933a0c2440695663f49860177dc6fbdbac2c57c7ab) | [6a0ff821](https://github.com/whetstoneresearch/doppler/commit/6a0ff821) |
| StreamableFeesLockerV2       | [0xce32...3d47](https://etherscan.io/address/0xce3212e6536f33cd6fbfee265224131353ca3d47) | [0xc072...6d5e](https://etherscan.io/tx/0xc072af8a79aac8be66466753a84067cd69bf82482d14291e1d318f76ac4e6d5e) | [4ef84c4](https://github.com/whetstoneresearch/doppler/commit/4ef84c4)   |
| TimelockFactory              | [0x9fd4...6ea0](https://etherscan.io/address/0x9fd40af2baafaf03513f091444e821c0d5b06ea0) | [0x78e8...9329](https://etherscan.io/tx/0x78e8ab7e241f65ceb91277e1882787a68abf5b5759419e735b9c1f2974d19329) | [f136b8e](https://github.com/whetstoneresearch/doppler/commit/f136b8e)   |
| TopUpDistributor             | [0x4353...d814](https://etherscan.io/address/0x435312320c0330b1999746753551cdfbd83ad814) | [0x8025...6b21](https://etherscan.io/tx/0x8025107f901ac3fff7bb741e5a3fc03b9dea4ba5722bb0b9bce203edcc426b21) | [a4390e7](https://github.com/whetstoneresearch/doppler/commit/a4390e7)   |
| UniswapV2Locker              | [0x0673...e495](https://etherscan.io/address/0x06731e30df4b0209ed19c39ba8ed253e17b0e495) | [0xdf49...7d39](https://etherscan.io/tx/0xdf49d87fab5b221c45a60fbe890a00360a5500a861942952fb36c30d5b117d39) | [63dc78fe](https://github.com/whetstoneresearch/doppler/commit/63dc78fe) |
| UniswapV2MigratorSplit       | [0xd7ab...12d3](https://etherscan.io/address/0xd7aba5f1d80a330a6fe9e96f7ba122710e0912d3) | [0xdf49...7d39](https://etherscan.io/tx/0xdf49d87fab5b221c45a60fbe890a00360a5500a861942952fb36c30d5b117d39) | [63dc78fe](https://github.com/whetstoneresearch/doppler/commit/63dc78fe) |
| UniswapV4Initializer         | [0x53b4...e8ad](https://etherscan.io/address/0x53b4c21a6cb61d64f636abbfa6e8e90e6558e8ad) | [0x9fbc...7a38](https://etherscan.io/tx/0x9fbc00d98616b60f482beb8c94aa13cd08077fd8fb56099a15dd91f807b97a38) | [5aa31e1](https://github.com/whetstoneresearch/doppler/commit/5aa31e1)   |

### Monad Mainnet (143)

| Contract                     | Address                                                                                   | Transaction                                                                                                  | Commit                                                                   |
| ---------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| Airlock                      | [0x660e...8d12](https://monadscan.com/address/0x660eaaedebc968f8f3694354fa8ec0b4c5ba8d12) | [0x758e...c1fd](https://monadscan.com/tx/0x758eec05f2ffe6ebcc1a8d20c0daa3cb30a0c3e14aa615a68f9bbbce43f3c1fd) | [cc6efe5](https://github.com/whetstoneresearch/doppler/commit/cc6efe5)   |
| Bundler                      | [0xc99b...a684](https://monadscan.com/address/0xc99b485499f78995c6f1640dbb1413c57f8ba684) | [0xba28...de98](https://monadscan.com/tx/0xba28e2ceeb5658c5a8df8ae5ebc2cd0d20d333d6cb052f1d6208fea7ed70de98) | [cc6efe5](https://github.com/whetstoneresearch/doppler/commit/cc6efe5)   |
| DopplerCreateXDeployer       | [0x1030...9b83](https://monadscan.com/address/0x103004e50bed65dfba30dd9c264b6bdf5e529b83) | [0xfe87...111a](https://monadscan.com/tx/0xfe8752cdd0c659940fb1e59c1a50399fdc7a1476d303ce0db938fbfecd21111a) | [9b60ad7d](https://github.com/whetstoneresearch/doppler/commit/9b60ad7d) |
| DopplerDeployer              | [0xb354...e421](https://monadscan.com/address/0xb35469ee64a87afd19b31615094fe3962d73e421) | [0xdb03...dcfb](https://monadscan.com/tx/0xdb03841932dcfadf966752ec380cd5313146275281d8c42e306c8f68fa58dcfb) | [d223428](https://github.com/whetstoneresearch/doppler/commit/d223428)   |
| DopplerERC20V1               | [0xdb7b...be87](https://monadscan.com/address/0xdb7b520bb5c3a2c5d4871198081911359f93be87) | [0xf79a...4a30](https://monadscan.com/tx/0xf79a36ac5ff846649b9b7b7c647a99e0d1a5296fc43e016ce7d9527a36b04a30) | [fce45321](https://github.com/whetstoneresearch/doppler/commit/fce45321) |
| DopplerERC20V1Factory        | [0x89c2...5292](https://monadscan.com/address/0x89c261c05b5f9b6bcba07c199b8dee7cfad45292) | [0xf79a...4a30](https://monadscan.com/tx/0xf79a36ac5ff846649b9b7b7c647a99e0d1a5296fc43e016ce7d9527a36b04a30) | [fce45321](https://github.com/whetstoneresearch/doppler/commit/fce45321) |
| DopplerHookInitializer       | [0x56ea...a544](https://monadscan.com/address/0x56ea13da5f39863d3b3d54826187306af7ada544) | [0x0aaa...214f](https://monadscan.com/tx/0x0aaab65903a463331520579c6ea1c1f06df0486bcd29220cd05964165966214f) | [6b9fb9ea](https://github.com/whetstoneresearch/doppler/commit/6b9fb9ea) |
| DopplerHookMigrator          | [0x1e40...60c4](https://monadscan.com/address/0x1e40b0875dda35f41e15cfb475403859b8c860c4) | [0xe43d...01bf](https://monadscan.com/tx/0xe43ddc777ed5ae6d9967441fd0f68c6ad0661dae4788ea2859857f6a825601bf) | [5fe4eb1](https://github.com/whetstoneresearch/doppler/commit/5fe4eb1)   |
| GovernanceFactory            | [0xfaaf...6f45](https://monadscan.com/address/0xfaafde6a5b658684cc5eb0c5c2c755b00a246f45) | [0x7a40...3bc1](https://monadscan.com/tx/0x7a40bce8a81472208242ac04f4e3ff50c9bd005b036cd014038bad8db4653bc1) | [cc6efe5](https://github.com/whetstoneresearch/doppler/commit/cc6efe5)   |
| LaunchpadGovernanceFactory   | [0x5fbe...b76d](https://monadscan.com/address/0x5fbe931dc4b923a7abe4c47ad68d5bf9eda5b76d) | [0x72d9...a79b](https://monadscan.com/tx/0x72d936f51838751594f9eef0337872c16f42fc4b92bda38fe974a5082392a79b) | [cc6efe5](https://github.com/whetstoneresearch/doppler/commit/cc6efe5)   |
| LockableUniswapV3Initializer | [0x8b4c...c2a0](https://monadscan.com/address/0x8b4c7db9121fc885689c0a50d5a1429f15aec2a0) | [0x86b4...6f41](https://monadscan.com/tx/0x86b46a7780fbf246e65fc8174483a3c5abc5be8f1d5d5d40d5212a5dd5c76f41) | [cc6efe5](https://github.com/whetstoneresearch/doppler/commit/cc6efe5)   |
| NoOpGovernanceFactory        | [0xb4de...14d9](https://monadscan.com/address/0xb4dee32eb70a5e55f3d2d861f49fb3d79f7a14d9) | [0x5b1c...f180](https://monadscan.com/tx/0x5b1cb51919dca25735d638c5521853efb85f88917f80ea6133f18c1b6cdef180) | [cc6efe5](https://github.com/whetstoneresearch/doppler/commit/cc6efe5)   |
| NoOpMigrator                 | [0x5f3b...e731](https://monadscan.com/address/0x5f3ba43d44375286296cb85f1ea2ebfa25dde731) | [0x3abd...ce34](https://monadscan.com/tx/0x3abd3022d8aa480228a31690013ba67a15ffdc4d393964a0c65c90737facce34) | [cc6efe5](https://github.com/whetstoneresearch/doppler/commit/cc6efe5)   |
| Quoter                       | [0xc9be...0567](https://monadscan.com/address/0xc9be59b344ec66547a4c697aa374e959d59b0567) | [0x688c...bc1e](https://monadscan.com/tx/0x688cba7f4df84eaedcf4ee3e0fe31741baefb6dd4197b37b35241a678696bc1e) | [6a0ff821](https://github.com/whetstoneresearch/doppler/commit/6a0ff821) |
| RehypeDopplerHookInitializer | [0xbd54...45c5](https://monadscan.com/address/0xbd54a9e1d2249185a27af097abaa930631ec45c5) | [0x7426...f6f4](https://monadscan.com/tx/0x7426b9e0920e2b4398b3858e512a1e1f3161afe012d737a8661aad386fbbf6f4) | [6a0ff821](https://github.com/whetstoneresearch/doppler/commit/6a0ff821) |
| RehypeDopplerHookMigrator    | [0x6607...c84b](https://monadscan.com/address/0x660740d7d6fb2c8998fa3fff459cceb9ac12c84b) | [0x688c...bc1e](https://monadscan.com/tx/0x688cba7f4df84eaedcf4ee3e0fe31741baefb6dd4197b37b35241a678696bc1e) | [6a0ff821](https://github.com/whetstoneresearch/doppler/commit/6a0ff821) |
| StreamableFeesLockerV2       | [0xce32...3d47](https://monadscan.com/address/0xce3212e6536f33cd6fbfee265224131353ca3d47) | [0x64c8...ba72](https://monadscan.com/tx/0x64c8ed9531b6c37a977c1fc5a7546e81e3f9241c16c10b69a4fff2de7a6cba72) | [4ef84c4](https://github.com/whetstoneresearch/doppler/commit/4ef84c4)   |
| TimelockFactory              | [0x4461...59b2](https://monadscan.com/address/0x44610355465c1f5914c8b8ea0e7c887cc67459b2) | [0x7a40...3bc1](https://monadscan.com/tx/0x7a40bce8a81472208242ac04f4e3ff50c9bd005b036cd014038bad8db4653bc1) | [cc6efe5](https://github.com/whetstoneresearch/doppler/commit/cc6efe5)   |
| TopUpDistributor             | [0x4353...d814](https://monadscan.com/address/0x435312320c0330b1999746753551cdfbd83ad814) | [0xda50...9dd6](https://monadscan.com/tx/0xda50497dfa0fe8c07a1c47172ab50e18bd1f62f2b20e234e124a76a906c09dd6) | [a4390e7](https://github.com/whetstoneresearch/doppler/commit/a4390e7)   |
| UniswapV2Locker              | [0x0673...e495](https://monadscan.com/address/0x06731e30df4b0209ed19c39ba8ed253e17b0e495) | [0x8c40...de93](https://monadscan.com/tx/0x8c4049fc6120e7bb15dab0aa63d48f1efa03d9ca8fd0779b19fc05799d26de93) | [63dc78fe](https://github.com/whetstoneresearch/doppler/commit/63dc78fe) |
| UniswapV2MigratorSplit       | [0xd7ab...12d3](https://monadscan.com/address/0xd7aba5f1d80a330a6fe9e96f7ba122710e0912d3) | [0x8c40...de93](https://monadscan.com/tx/0x8c4049fc6120e7bb15dab0aa63d48f1efa03d9ca8fd0779b19fc05799d26de93) | [63dc78fe](https://github.com/whetstoneresearch/doppler/commit/63dc78fe) |
| UniswapV4Initializer         | [0x53b4...e8ad](https://monadscan.com/address/0x53b4c21a6cb61d64f636abbfa6e8e90e6558e8ad) | [0x57f2...7399](https://monadscan.com/tx/0x57f219b998a0a1f73ae2a3f7bca002f3e5a4f4b8db8de8f55976a96ff8447399) | [d223428](https://github.com/whetstoneresearch/doppler/commit/d223428)   |

### Robinhood Mainnet (4663)

| Contract                     | Address                                                                                                   | Transaction                                                                                                                  | Commit                                                                   |
| ---------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Airlock                      | [0xeb7c...0862](https://robinhoodchain.blockscout.com/address/0xeb7c034704ef8dcd2d32324c1545f62fb4ad0862) | [0x8ffd...291a](https://robinhoodchain.blockscout.com/tx/0x8ffd957b1985578fd9bfb8ce651bf546cb262f3b157f02f93b26c585046b291a) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| Bundler                      | [0xede0...c5a1](https://robinhoodchain.blockscout.com/address/0xede0b5fae363232c396724fa962250fa197cc5a1) | [0x51c3...31ce](https://robinhoodchain.blockscout.com/tx/0x51c3f10edf95e85f94008bb5b28204126858366732ea55045de4cecaec3031ce) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| DN404Factory                 | [0x37a9...bc9d](https://robinhoodchain.blockscout.com/address/0x37a9fa204a4d3a429fded7e3469ab076c854bc9d) | [0x33f9...da99](https://robinhoodchain.blockscout.com/tx/0x33f9d082518878262898a9c2e11d146516fddd7bc1ae3674b623f5854c09da99) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| DopplerCreateXDeployer       | [0x1030...9b83](https://robinhoodchain.blockscout.com/address/0x103004e50bed65dfba30dd9c264b6bdf5e529b83) | [0x2499...1a6a](https://robinhoodchain.blockscout.com/tx/0x24996e26139cad95831f3112ed727ed9fd2f5b9b8b057916df166ba291cf1a6a) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| DopplerDeployer              | [0x4389...573f](https://robinhoodchain.blockscout.com/address/0x4389ad34938b14f25cff7ed983c53f5a42a2573f) | [0xa87d...34bf](https://robinhoodchain.blockscout.com/tx/0xa87d3dfffd1c96d9c6a94b20ff2a758c7e3182cd78dd3ca0c0b91c19279834bf) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| DopplerERC20V1               | [0x3be8...c599](https://robinhoodchain.blockscout.com/address/0x3be8b97fd0e713b5abe0649fa830223b6b4bc599) | [0xb53e...f7c9](https://robinhoodchain.blockscout.com/tx/0xb53eb8261ef8e76f3bb89c08dd5ca742408ec9911ed5cb0a32ac3a92dc59f7c9) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| DopplerERC20V1Factory        | [0x1b37...b69a](https://robinhoodchain.blockscout.com/address/0x1b37d3a72082029c44b35b604ea473617580b69a) | [0xb53e...f7c9](https://robinhoodchain.blockscout.com/tx/0xb53eb8261ef8e76f3bb89c08dd5ca742408ec9911ed5cb0a32ac3a92dc59f7c9) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| DopplerHookInitializer       | [0x4e34...a544](https://robinhoodchain.blockscout.com/address/0x4e3468951d49f2eea976ed0d6e75ffcb44a9a544) | [0xd32e...00b1](https://robinhoodchain.blockscout.com/tx/0xd32e8ebbb51adc6b05d0608e3eaf289d8d648f4df42175990bbba2fcfbe700b1) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| DopplerHookMigrator          | [0x7bf3...e0c4](https://robinhoodchain.blockscout.com/address/0x7bf319d8e969f7596b1bc171da9ce322f67ae0c4) | [0x7e76...bbac](https://robinhoodchain.blockscout.com/tx/0x7e76a2e3198ecc5aa710e634ff9fcffc443cb1a5ad084e29268341a9c836bbac) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| DopplerLensQuoter            | [0xf4c2...5c04](https://robinhoodchain.blockscout.com/address/0xf4c22465532f64777ffcd7770831aeca38f35c04) | [0xc846...7819](https://robinhoodchain.blockscout.com/tx/0xc846bbac30cb2f7a7e39667f48311b6faa97c3cda5fcf1bfcc1eb2fe5dc37819) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| GovernanceFactory            | [0xdeb0...b7ef](https://robinhoodchain.blockscout.com/address/0xdeb0447dae3eb177c4dba8bbccca25c8f273b7ef) | [0x56eb...de8d](https://robinhoodchain.blockscout.com/tx/0x56ebbebad4f242cae43515c2008f37b53720b143dd260df408b4d475ba5cde8d) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| LaunchpadGovernanceFactory   | [0xdb03...37cf](https://robinhoodchain.blockscout.com/address/0xdb036746d65dd52126b1915f1adf555e6c5237cf) | [0xb76e...4d4d](https://robinhoodchain.blockscout.com/tx/0xb76ed227716e9dfbb00cf273739418a86356d15609746a78293da488ebe94d4d) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| LockableUniswapV3Initializer | [0xde88...29a3](https://robinhoodchain.blockscout.com/address/0xde8886a0019ea060b8378ee37b8a23b8117f29a3) | [0x6506...34ea](https://robinhoodchain.blockscout.com/tx/0x6506158f635a07a87b010d5326f0b87a4e920448d8ae1d57f3bee8e12c0934ea) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| NoOpGovernanceFactory        | [0x85f3...1ad7](https://robinhoodchain.blockscout.com/address/0x85f37f74ef2478a770318bc810177a9835911ad7) | [0x1a14...0b05](https://robinhoodchain.blockscout.com/tx/0x1a1494e76f22da363bcfedb3c2d2027c6e13e341c256666667e1ec62b9050b05) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| NoOpMigrator                 | [0xba2f...5a0e](https://robinhoodchain.blockscout.com/address/0xba2f330edb16cd8056f5988d8ce19bbc63475a0e) | [0x3ba7...ebd0](https://robinhoodchain.blockscout.com/tx/0x3ba7683099adcaa4853867a49214d5e6e5d88850b9f267dda4d17a2117d9ebd0) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| Quoter                       | [0xc9be...0567](https://robinhoodchain.blockscout.com/address/0xc9be59b344ec66547a4c697aa374e959d59b0567) | [0x17f0...ef08](https://robinhoodchain.blockscout.com/tx/0x17f08a11b651c80e55bd33e493b70d1f7f18bf4f38d64563838be3cfc460ef08) | [6a0ff821](https://github.com/whetstoneresearch/doppler/commit/6a0ff821) |
| RehypeDopplerHookInitializer | [0xbd54...45c5](https://robinhoodchain.blockscout.com/address/0xbd54a9e1d2249185a27af097abaa930631ec45c5) | [0x9cad...edf6](https://robinhoodchain.blockscout.com/tx/0x9cadfc4fb730ec482843f4331fd8b780912b32bf5086439bb1f03318a9bfedf6) | [6a0ff821](https://github.com/whetstoneresearch/doppler/commit/6a0ff821) |
| RehypeDopplerHookMigrator    | [0x6607...c84b](https://robinhoodchain.blockscout.com/address/0x660740d7d6fb2c8998fa3fff459cceb9ac12c84b) | [0x17f0...ef08](https://robinhoodchain.blockscout.com/tx/0x17f08a11b651c80e55bd33e493b70d1f7f18bf4f38d64563838be3cfc460ef08) | [6a0ff821](https://github.com/whetstoneresearch/doppler/commit/6a0ff821) |
| StreamableFeesLockerV2       | [0x7b61...63b8](https://robinhoodchain.blockscout.com/address/0x7b6147ac3f615bdb764e7ebd5f517dac1ad163b8) | [0x8ff7...01c5](https://robinhoodchain.blockscout.com/tx/0x8ff7d1d8b10b4c5f9531976d7b9a7c49e84790e41f197362be47708772c201c5) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| SwapRestrictorDopplerHook    | [0xc16c...aba2](https://robinhoodchain.blockscout.com/address/0xc16c826f75338a5ea626f94f8992191b4ce5aba2) | [0x0116...09f6](https://robinhoodchain.blockscout.com/tx/0x011656381ef7dc14e6afce43d301332374c3d2bd0b23a4d458048f8ea6bf09f6) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| TimelockFactory              | [0x6076...c578](https://robinhoodchain.blockscout.com/address/0x6076fddfcac0dd980e0350dff5239fec3f86c578) | [0x56eb...de8d](https://robinhoodchain.blockscout.com/tx/0x56ebbebad4f242cae43515c2008f37b53720b143dd260df408b4d475ba5cde8d) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| TopUpDistributor             | [0x46ad...ef06](https://robinhoodchain.blockscout.com/address/0x46adee7595d48b1ec53090e9bc78e1e69fa0ef06) | [0xb652...5b4f](https://robinhoodchain.blockscout.com/tx/0xb65238e8206ff35da3bb8c90dc95deecd46912806b4d351b68339ab632f65b4f) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| UniswapV2Locker              | [0x63f6...42a5](https://robinhoodchain.blockscout.com/address/0x63f6efe03f25a8c6650b38d05c5a454051d642a5) | [0x6bb5...f428](https://robinhoodchain.blockscout.com/tx/0x6bb59b4e221005e7121eee0e79fddd0967c230d03d0e108c7dcbd8b9a2b6f428) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| UniswapV2MigratorSplit       | [0xb050...a333](https://robinhoodchain.blockscout.com/address/0xb05046cea797c993fb5b583098b1c4682e9da333) | [0x6bb5...f428](https://robinhoodchain.blockscout.com/tx/0x6bb59b4e221005e7121eee0e79fddd0967c230d03d0e108c7dcbd8b9a2b6f428) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |
| UniswapV4Initializer         | [0x6cce...57ea](https://robinhoodchain.blockscout.com/address/0x6cce158b6d1747617fc218592b4d60b239b957ea) | [0x2ad4...b0b6](https://robinhoodchain.blockscout.com/tx/0x2ad4f96e1b3110da078167813b4f3f47db3c0b7719747df5674bdc468189b0b6) | [bda077cf](https://github.com/whetstoneresearch/doppler/commit/bda077cf) |

### Base (8453)

| Contract                     | Address                                                                                  | Transaction                                                                                                 | Commit                                                                   |
| ---------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Airlock                      | [0x660e...8D12](https://basescan.org/address/0x660eAaEdEBc968f8f3694354FA8EC0b4c5Ba8D12) | [0x029b...5d24](https://basescan.org/tx/0x029b03e1549bf6a8e115b9d961c62a75ba4869a912c0f13bdaa2da7d1f0a5d24) | [9b23399](https://github.com/whetstoneresearch/doppler/commit/9b23399)   |
| Bundler                      | [0x1361...677a](https://basescan.org/address/0x136191B46478cAB023cbC01a36160C4Aad81677a) | [0xac9d...bd21](https://basescan.org/tx/0xac9d54a5beabc34315e5c0969d6e13809460b9e1fcaaef1946c6f4d0ce6abd21) | [9b23399](https://github.com/whetstoneresearch/doppler/commit/9b23399)   |
| DN404Factory                 | [0x37a9...bc9d](https://basescan.org/address/0x37a9fa204a4d3a429fded7e3469ab076c854bc9d) | [0x6810...2298](https://basescan.org/tx/0x68103197c1244927826f46839f5bba9d96721ab647e1da2575fbc627f52a2298) | [d0be38dd](https://github.com/whetstoneresearch/doppler/commit/d0be38dd) |
| DopplerCreateXDeployer       | [0x1030...9b83](https://basescan.org/address/0x103004e50bed65dfba30dd9c264b6bdf5e529b83) | [0x8fd8...cc73](https://basescan.org/tx/0x8fd8df0a77483f11239a223abb8c202cc1c2e849323bf448213cae8bcfcfcc73) | [9b60ad7d](https://github.com/whetstoneresearch/doppler/commit/9b60ad7d) |
| DopplerDeployer              | [0xb354...e421](https://basescan.org/address/0xb35469ee64a87afd19b31615094fe3962d73e421) | [0x0965...949a](https://basescan.org/tx/0x096551fca6b6f418132fb5ae240a82d58f86ab5f6958a00614058a8a1d48949a) | [d223428](https://github.com/whetstoneresearch/doppler/commit/d223428)   |
| DopplerERC20V1               | [0xdb7b...be87](https://basescan.org/address/0xdb7b520bb5c3a2c5d4871198081911359f93be87) | [0x946a...a605](https://basescan.org/tx/0x946a639a88335b575180cc50384e7c5fb476d47ccbf5892527a4db67f223a605) | [fce45321](https://github.com/whetstoneresearch/doppler/commit/fce45321) |
| DopplerERC20V1Factory        | [0x89c2...5292](https://basescan.org/address/0x89c261c05b5f9b6bcba07c199b8dee7cfad45292) | [0x946a...a605](https://basescan.org/tx/0x946a639a88335b575180cc50384e7c5fb476d47ccbf5892527a4db67f223a605) | [fce45321](https://github.com/whetstoneresearch/doppler/commit/fce45321) |
| DopplerHookInitializer       | [0xbdf9...6544](https://basescan.org/address/0xbdf938149ac6a781f94faa0ed45e6a0e984c6544) | [0xc5d2...cb77](https://basescan.org/tx/0xc5d2fff42e46e9006e0e4d2411a48964c698e95d9f936df30fa4c910f694cb77) | [0154a5f](https://github.com/whetstoneresearch/doppler/commit/0154a5f)   |
| DopplerHookMigrator          | [0x1e40...60c4](https://basescan.org/address/0x1e40b0875dda35f41e15cfb475403859b8c860c4) | [0x8922...f781](https://basescan.org/tx/0x89227eaee3f00b545c38de6c43479b310472f4accd9a985fa6c0368821ebf781) | [5fe4eb1](https://github.com/whetstoneresearch/doppler/commit/5fe4eb1)   |
| DopplerLensQuoter            | [0x43d0...f2b3](https://basescan.org/address/0x43d0d97ec9241a8f05a264f94b82a1d2e600f2b3) | [0x4901...3297](https://basescan.org/tx/0x49017fe92ee9c62f3b812c949a812dcd3f44fc26fd75a5d955beae7c9baa3297) | [6e368f4](https://github.com/whetstoneresearch/doppler/commit/6e368f4)   |
| GovernanceFactory            | [0xa82c...2d4b](https://basescan.org/address/0xa82c66b6ddeb92089015c3565e05b5c9750b2d4b) | [0x2ccf...4aa7](https://basescan.org/tx/0x2ccf65a48cf57faac39f96950367775d3e36f110b21f4421e6b5667333094aa7) | [c7388da](https://github.com/whetstoneresearch/doppler/commit/c7388da)   |
| LaunchpadGovernanceFactory   | [0x40bc...ddca](https://basescan.org/address/0x40bcb4dda3bcf7dba30c5d10c31ee2791ed9ddca) | [0xe202...16a3](https://basescan.org/tx/0xe202eb08fbd6b9b8364c9286706ba7b01e91fc395a96932ba02a6608b22016a3) | [7c4e720](https://github.com/whetstoneresearch/doppler/commit/7c4e720)   |
| LockableUniswapV3Initializer | [0xe0dc...58d0](https://basescan.org/address/0xe0dc4012ac9c868f09c6e4b20d66ed46d6f258d0) | [0xb7e3...3cee](https://basescan.org/tx/0xb7e38496562c905cf6937a2678ac435f29d4078c53210a2cc8c633c2dbbf3cee) | [3d77e8f](https://github.com/whetstoneresearch/doppler/commit/3d77e8f)   |
| NoOpGovernanceFactory        | [0xe7df...090e](https://basescan.org/address/0xe7dfbd5b0a2c3b4464653a9becdc489229ef090e) | [0x9cb7...a8f5](https://basescan.org/tx/0x9cb74f0eecbe92fb1f45aa1fcba39d95f3e9416dfcb778da10b4238cb88ea8f5) | [509b88a](https://github.com/whetstoneresearch/doppler/commit/509b88a)   |
| NoOpMigrator                 | [0x6ddf...5c33](https://basescan.org/address/0x6ddfed58d238ca3195e49d8ac3d4cea6386e5c33) | [0x2e33...14c8](https://basescan.org/tx/0x2e33041fcf31503872054e93a9ed97265758916de747d561267c4f58fb9814c8) | [6a2dbfd](https://github.com/whetstoneresearch/doppler/commit/6a2dbfd)   |
| Quoter                       | [0xc9be...0567](https://basescan.org/address/0xc9be59b344ec66547a4c697aa374e959d59b0567) | [0x6ddc...788c](https://basescan.org/tx/0x6ddc84b295914a84f04903f77221fe329b15e393dd3039e1f33fed6afc4a788c) | [6a0ff821](https://github.com/whetstoneresearch/doppler/commit/6a0ff821) |
| RehypeDopplerHookInitializer | [0xbd54...45c5](https://basescan.org/address/0xbd54a9e1d2249185a27af097abaa930631ec45c5) | [0x29ec...4ecf](https://basescan.org/tx/0x29ec83dcc5845205ba0374b5c2e6d752a27ed0f822a68446c15e96dc65b24ecf) | [6a0ff821](https://github.com/whetstoneresearch/doppler/commit/6a0ff821) |
| RehypeDopplerHookMigrator    | [0x6607...c84b](https://basescan.org/address/0x660740d7d6fb2c8998fa3fff459cceb9ac12c84b) | [0x6ddc...788c](https://basescan.org/tx/0x6ddc84b295914a84f04903f77221fe329b15e393dd3039e1f33fed6afc4a788c) | [6a0ff821](https://github.com/whetstoneresearch/doppler/commit/6a0ff821) |
| StreamableFeesLockerV2       | [0xce32...3d47](https://basescan.org/address/0xce3212e6536f33cd6fbfee265224131353ca3d47) | [0xbe4b...b857](https://basescan.org/tx/0xbe4b618372426b8a31c8f0b79803ffbd129ce8ff8403a7ff86db29304bffb857) | [4ef84c4](https://github.com/whetstoneresearch/doppler/commit/4ef84c4)   |
| TimelockFactory              | [0xa636...CB33](https://basescan.org/address/0xa636bD5F9179E80e9EdB371Ed90956fA5ebdCB33) | [0x2ccf...4aa7](https://basescan.org/tx/0x2ccf65a48cf57faac39f96950367775d3e36f110b21f4421e6b5667333094aa7) | [92c5ec4](https://github.com/whetstoneresearch/doppler/commit/92c5ec4)   |
| TopUpDistributor             | [0x4353...d814](https://basescan.org/address/0x435312320c0330b1999746753551cdfbd83ad814) | [0xa80b...d3dd](https://basescan.org/tx/0xa80bc61388efdf2172a1f2996764798319eab1cf6336aabc366d26e995b4d3dd) | [a4390e7](https://github.com/whetstoneresearch/doppler/commit/a4390e7)   |
| UniswapV2Locker              | [0x0673...e495](https://basescan.org/address/0x06731e30df4b0209ed19c39ba8ed253e17b0e495) | [0x05dc...7209](https://basescan.org/tx/0x05dc33d6047800ef0d103fb39cfe0cb92ab92621cbb33ffdc006ab1e49b17209) | [63dc78fe](https://github.com/whetstoneresearch/doppler/commit/63dc78fe) |
| UniswapV2MigratorSplit       | [0xd7ab...12d3](https://basescan.org/address/0xd7aba5f1d80a330a6fe9e96f7ba122710e0912d3) | [0x05dc...7209](https://basescan.org/tx/0x05dc33d6047800ef0d103fb39cfe0cb92ab92621cbb33ffdc006ab1e49b17209) | [63dc78fe](https://github.com/whetstoneresearch/doppler/commit/63dc78fe) |
| UniswapV4Initializer         | [0x53b4...e8ad](https://basescan.org/address/0x53b4c21a6cb61d64f636abbfa6e8e90e6558e8ad) | [0x2c65...213d](https://basescan.org/tx/0x2c65e338366c8eedf942438b8183389527b0ef368aac782e5fb30f941b44213d) | [d223428](https://github.com/whetstoneresearch/doppler/commit/d223428)   |

### Solana Mainnet

| Program                | Address                                                                                                                            |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Initializer program    | [`4carc9eePfE7jKUXdCAYMhcPf4awEFpZPrz1sTykdss1`](https://explorer.solana.com/address/4carc9eePfE7jKUXdCAYMhcPf4awEFpZPrz1sTykdss1) |
| CPMM program           | [`5pXzd9UiWrVxATCYWmgo5EbfxzXqHYhfSKGdCPXPz7vK`](https://explorer.solana.com/address/5pXzd9UiWrVxATCYWmgo5EbfxzXqHYhfSKGdCPXPz7vK) |
| CPMM migrator program  | [`H71WD4tsiCCipro4urykWHySH1ryvLTmqEdNbHTGwb3o`](https://explorer.solana.com/address/H71WD4tsiCCipro4urykWHySH1ryvLTmqEdNbHTGwb3o) |
| Doppler Launch Hook v1 | [`BeyqffXEVgLpM3fQ1zjk8YnZzQN9sMVrCKtNKwSxNATr`](https://explorer.solana.com/address/BeyqffXEVgLpM3fQ1zjk8YnZzQN9sMVrCKtNKwSxNATr) |

## Testnet Deployments

### Base Sepolia (84532)

| Contract                     | Address                                                                                          | Transaction                                                                                                         | Commit                                                                   |
| ---------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Airlock                      | [0x3411...1c6e](https://sepolia.basescan.org/address/0x3411306ce66c9469bff1535ba955503c4bde1c6e) | [0x5508...1b5a](https://sepolia.basescan.org/tx/0x550857ce00eb6b050fbd0a089bbd516226b88ee05a052792c7d380acd7a61b5a) | [68b9f34](https://github.com/whetstoneresearch/doppler/commit/68b9f34)   |
| AirlockMultisigTestnet       | [0x0abc...4426](https://sepolia.basescan.org/address/0x0abcf819fd57c9f0141628410ffc273405e44426) | [0xb2c5...12a4](https://sepolia.basescan.org/tx/0xb2c5e7493f5da21edd21baeaf5c68b09075bd0722811ac3b5fe849385bd112a4) | [ee35a52](https://github.com/whetstoneresearch/doppler/commit/ee35a52)   |
| Bundler                      | [0x69db...0661](https://sepolia.basescan.org/address/0x69db7c20cdda49bed2bfb21e16fa218330c50661) | [0x8259...ff73](https://sepolia.basescan.org/tx/0x82593378e15de863bcc3eeb0f5a6fcf05e70cefa12dbf000193277129af2ff73) | [204d121](https://github.com/whetstoneresearch/doppler/commit/204d121)   |
| DN404Factory                 | [0x98b0...243a](https://sepolia.basescan.org/address/0x98b0aa2e0f134dbb3eb157b5646d387e6d55243a) | [0x31ef...433d](https://sepolia.basescan.org/tx/0x31effe0091f029a5696b34ece79d38c7396e8cb5e4b08f214f4c953fb1f5433d) | [4c89651f](https://github.com/whetstoneresearch/doppler/commit/4c89651f) |
| DopplerCreateXDeployer       | [0x0000...be60](https://sepolia.basescan.org/address/0x0000000000f13ab5b685f03a412a26719ab6be60) | [0x1729...70b1](https://sepolia.basescan.org/tx/0x1729dd8ed52904596144ee17f4c417d08e5aee22672fa258deb8a530bcdc70b1) | [9b99b8cf](https://github.com/whetstoneresearch/doppler/commit/9b99b8cf) |
| DopplerDeployer              | [0xb354...e421](https://sepolia.basescan.org/address/0xb35469ee64a87afd19b31615094fe3962d73e421) | [0x4457...a383](https://sepolia.basescan.org/tx/0x44576f889d9c6701ec89a1c044130ba9d3a9c17ed76ee30ef2087d59d08aa383) | [d223428](https://github.com/whetstoneresearch/doppler/commit/d223428)   |
| DopplerERC20V1               | [0xdb7b...be87](https://sepolia.basescan.org/address/0xdb7b520bb5c3a2c5d4871198081911359f93be87) | [0x2f93...6edc](https://sepolia.basescan.org/tx/0x2f93faf612ccc94dddfb9d1d2a6971f42479bcb0b14ace4ac48efc54715b6edc) | [fce45321](https://github.com/whetstoneresearch/doppler/commit/fce45321) |
| DopplerERC20V1Factory        | [0x89c2...5292](https://sepolia.basescan.org/address/0x89c261c05b5f9b6bcba07c199b8dee7cfad45292) | [0x2f93...6edc](https://sepolia.basescan.org/tx/0x2f93faf612ccc94dddfb9d1d2a6971f42479bcb0b14ace4ac48efc54715b6edc) | [fce45321](https://github.com/whetstoneresearch/doppler/commit/fce45321) |
| DopplerHookInitializer       | [0xbdf9...6544](https://sepolia.basescan.org/address/0xbdf938149ac6a781f94faa0ed45e6a0e984c6544) | [0xb7b3...7779](https://sepolia.basescan.org/tx/0xb7b3768a41620e2a46319093291897da1494d5be9f81081412f37250dac77779) | [0154a5f](https://github.com/whetstoneresearch/doppler/commit/0154a5f)   |
| DopplerHookMigrator          | [0x1e40...60c4](https://sepolia.basescan.org/address/0x1e40b0875dda35f41e15cfb475403859b8c860c4) | [0xc36d...4d7a](https://sepolia.basescan.org/tx/0xc36dec51ef6fcac957c1644e7d565afa756d887ee228ff53b7cf2ecb3cbf4d7a) | [5fe4eb1](https://github.com/whetstoneresearch/doppler/commit/5fe4eb1)   |
| DopplerLensQuoter            | [0x4a8d...47a7](https://sepolia.basescan.org/address/0x4a8d81db741248a36d9eb3bc6ef648bf798b47a7) | [0x403a...dc6d](https://sepolia.basescan.org/tx/0x403a8a37966866e14fa673221f07b770a764bf8fcae238882021e6e76912dc6d) | [68b9f34](https://github.com/whetstoneresearch/doppler/commit/68b9f34)   |
| GovernanceFactory            | [0x9dbf...2e20](https://sepolia.basescan.org/address/0x9dbfaadc8c0cb2c34ba698dd9426555336992e20) | [0xfb4b...c2cc](https://sepolia.basescan.org/tx/0xfb4b43d9ed92a62705b497a48668673ee0b5d35ea02075066a44a8e2d4bcc2cc) | [68b9f34](https://github.com/whetstoneresearch/doppler/commit/68b9f34)   |
| LaunchpadGovernanceFactory   | [0x0902...fbfa](https://sepolia.basescan.org/address/0x0902e7c7207df8ed6303aef4382bcab181b5fbfa) | [0x5b43...fc0f](https://sepolia.basescan.org/tx/0x5b43dfaf894e810839407622f0f68404677ead0f1d8d4713a6766e68e754fc0f) | [74867435](https://github.com/whetstoneresearch/doppler/commit/74867435) |
| LockableUniswapV3Initializer | [0x16ad...d53c](https://sepolia.basescan.org/address/0x16ada5be50c3c2d94af5feae6b539c40a78ad53c) | [0x029d...1c2e](https://sepolia.basescan.org/tx/0x029d8d87e753b383fc5afa33a8639bf28e32144dd8de519a38bde94e8b0b1c2e) | [3d77e8f](https://github.com/whetstoneresearch/doppler/commit/3d77e8f)   |
| NoOpGovernanceFactory        | [0x7bd7...5ea1](https://sepolia.basescan.org/address/0x7bd798fafc99a3b17e261f8308a8c11b56935ea1) | [0xf89e...d650](https://sepolia.basescan.org/tx/0xf89e9f684ba171864ec0bce988f5971732d7650e258f824c2294f5b8df1cd650) | [51f9aec](https://github.com/whetstoneresearch/doppler/commit/51f9aec)   |
| NoOpMigrator                 | [0xf110...2eb0](https://sepolia.basescan.org/address/0xf11066abbd329ac4bba39455340539322c222eb0) | [0xc184...95df](https://sepolia.basescan.org/tx/0xc184a1d61256f247e7ec65390c65056a4f3179adcde70c304702f7ca465895df) | [6a2dbfd](https://github.com/whetstoneresearch/doppler/commit/6a2dbfd)   |
| Quoter                       | [0xbb75...9a8f](https://sepolia.basescan.org/address/0xbb7504d7d17b15c8ae79273d653dba3ea88b9a8f) | [0x8b6b...ca2c](https://sepolia.basescan.org/tx/0x8b6b1fc85539c591ea1b1ed6bb774408d85758ecc99457385b6a7f5c85e5ca2c) | [6a0ff821](https://github.com/whetstoneresearch/doppler/commit/6a0ff821) |
| RehypeDopplerHookInitializer | [0x780f...fc6b](https://sepolia.basescan.org/address/0x780f88fb8d81cdcd5de8c5b7de810e362d9ffc6b) | [0xf5c6...97a4](https://sepolia.basescan.org/tx/0xf5c698c8ec16c62197450a491209cd23a3116bd06820c8800ca061f4cde797a4) | [6a0ff821](https://github.com/whetstoneresearch/doppler/commit/6a0ff821) |
| RehypeDopplerHookMigrator    | [0x5e2a...e90d](https://sepolia.basescan.org/address/0x5e2a91f8ebe288e0aa76a0c1a47877ded801e90d) | [0x8b6b...ca2c](https://sepolia.basescan.org/tx/0x8b6b1fc85539c591ea1b1ed6bb774408d85758ecc99457385b6a7f5c85e5ca2c) | [6a0ff821](https://github.com/whetstoneresearch/doppler/commit/6a0ff821) |
| StreamableFeesLockerV2       | [0xcE32...3D47](https://sepolia.basescan.org/address/0xcE3212e6536F33cD6fbFEE265224131353Ca3D47) | [0xbb9a...eb0b](https://sepolia.basescan.org/tx/0xbb9a6e9cc49d7f03afa05a440f187dac6098ebbd9981ba70474a37642aabeb0b) | [534c659](https://github.com/whetstoneresearch/doppler/commit/534c659)   |
| TimelockFactory              | [0x80F4...86a2](https://sepolia.basescan.org/address/0x80F42ECCc91177D39E20f1A88A54D231c1b086a2) | [0xfb4b...c2cc](https://sepolia.basescan.org/tx/0xfb4b43d9ed92a62705b497a48668673ee0b5d35ea02075066a44a8e2d4bcc2cc) | [92c5ec4](https://github.com/whetstoneresearch/doppler/commit/92c5ec4)   |
| TopUpDistributor             | [0x4353...d814](https://sepolia.basescan.org/address/0x435312320c0330b1999746753551cdfbd83ad814) | [0x6ace...8a0b](https://sepolia.basescan.org/tx/0x6aceb9b087abd9b08b758afc52446cc4dd9ccd6cbe7a63cfd18055f26a308a0b) | [a4390e7](https://github.com/whetstoneresearch/doppler/commit/a4390e7)   |
| UniswapV2Locker              | [0x0673...e495](https://sepolia.basescan.org/address/0x06731e30df4b0209ed19c39ba8ed253e17b0e495) | [0xd404...327c](https://sepolia.basescan.org/tx/0xd404fbf58189140785b11684c95a028d6d1d90e162cba2ff7ef3533ff706327c) | [63dc78fe](https://github.com/whetstoneresearch/doppler/commit/63dc78fe) |
| UniswapV2MigratorSplit       | [0xd7ab...12d3](https://sepolia.basescan.org/address/0xd7aba5f1d80a330a6fe9e96f7ba122710e0912d3) | [0xd404...327c](https://sepolia.basescan.org/tx/0xd404fbf58189140785b11684c95a028d6d1d90e162cba2ff7ef3533ff706327c) | [63dc78fe](https://github.com/whetstoneresearch/doppler/commit/63dc78fe) |
| UniswapV4Initializer         | [0x53b4...e8ad](https://sepolia.basescan.org/address/0x53b4c21a6cb61d64f636abbfa6e8e90e6558e8ad) | [0x0575...002e](https://sepolia.basescan.org/tx/0x0575286dfae7502987918a569f1bba9e118649c3883dc523af9b2d34a3cf002e) | [d223428](https://github.com/whetstoneresearch/doppler/commit/d223428)   |

### Solana Devnet

| Program                | Address                                                                                                                                           |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Initializer program    | [`4h3Dqyo5qmteJoMxXt3tdtfXELDB6pdRTPU9mWruiKp1`](https://explorer.solana.com/address/4h3Dqyo5qmteJoMxXt3tdtfXELDB6pdRTPU9mWruiKp1?cluster=devnet) |
| CPMM program           | [`9PSxVPoPfnbZ8Q1uQhgS6ZxvBjFboZtebNsu34umxkgQ`](https://explorer.solana.com/address/9PSxVPoPfnbZ8Q1uQhgS6ZxvBjFboZtebNsu34umxkgQ?cluster=devnet) |
| CPMM migrator program  | [`7WMUTNC41eMCo6eGH5Sy2xbgE3AycvLbFPo95AU9CSUd`](https://explorer.solana.com/address/7WMUTNC41eMCo6eGH5Sy2xbgE3AycvLbFPo95AU9CSUd?cluster=devnet) |
| Doppler Launch Hook v1 | [`HVsPNZh98TgChUXHwKrUG47SUqvGQHxUy5wZwcQLFD4i`](https://explorer.solana.com/address/HVsPNZh98TgChUXHwKrUG47SUqvGQHxUy5wZwcQLFD4i?cluster=devnet) |


# Metadata standards

Best practices for ecosystem support and compatability

Tokens created on Doppler should attempt to adhere to ecosystem best practices to ensure compatibility across as many supported interfaces as possible. We aim to make it as simple, and flexible, as possible, in order to support a wide range of applications and use cases.&#x20;

#### Requirements to be supported within the Doppler Indexer

1. Tokens must be created with a URI stored on IPFS that contains an image field
2. The image field in the URI must also contain another pointer to a valid IPFS CID&#x20;

{% hint style="info" %}
Have suggestions? File an issue on the documentation, or get in touch with the team!
{% endhint %}

### Examples

#### Example token created on Pure Markets

```
{
  "name": "8453",
  "symbol": "8453",
  "image": "ipfs://QmZEFVUKoQDxsUbsRTgrpdwhwxDM95HbMzc7kjUxjynLDk",
  "x": "",
  "telegram": "",
  "farcaster": "",
  "discord": ""
}
```

* tokenURI: <ipfs://QmTM59ZqHcJgv1EQVWs6e96Hchd3diHW9F9evLWmX2PQCU>
* Image: <ipfs://QmZEFVUKoQDxsUbsRTgrpdwhwxDM95HbMzc7kjUxjynLDk>&#x20;
* :link: <https://ipfs.io/ipfs/QmTM59ZqHcJgv1EQVWs6e96Hchd3diHW9F9evLWmX2PQCU>&#x20;

| Field     | Example                                                |
| --------- | ------------------------------------------------------ |
| name      | 8453                                                   |
| symbol    | 8453                                                   |
| image     | ipfs\://QmZEFVUKoQDxsUbsRTgrpdwhwxDM95HbMzc7kjUxjynLDk |
| x         |                                                        |
| telegram  |                                                        |
| farcaster |                                                        |
| discord   |                                                        |

Note: fields can be left empty. Pure.st will not attempt to load social links or icons for empty inputs.&#x20;

#### Example token created on Zora

<pre><code>{
  "name": "balajis",
  "ticker": "balajis",
  "image": "ipfs://bafybeibeeqdppfm2iizeaqiobdvn533xyuunjpdspdirju7xf4ykwyindq",
  "content": {
    "uri": "ipfs://bafybeibeeqdppfm2iizeaqiobdvn533xyuunjpdspdirju7xf4ykwyindq"
  }

<strong>
</strong></code></pre>

* tokenURI: <ipfs://bafybeiexri3tfd6fgjfume3tovyx6xdppgcapmvbjrqtraxec66pbtrus4>
* Image: <ipfs://bafybeibeeqdppfm2iizeaqiobdvn533xyuunjpdspdirju7xf4ykwyindq>
* :link: <https://ipfs.io/ipfs/QmTM59ZqHcJgv1EQVWs6e96Hchd3diHW9F9evLWmX2PQCU>&#x20;

| Field   | Example                                                             |
| ------- | ------------------------------------------------------------------- |
| name    | balajis                                                             |
| ticker  | balajis                                                             |
| image   | ipfs\://bafybeibeeqdppfm2iizeaqiobdvn533xyuunjpdspdirju7xf4ykwyindq |
| content | ipfs\://bafybeibeeqdppfm2iizeaqiobdvn533xyuunjpdspdirju7xf4ykwyindq |


# Legacy SDK migration guide


# v3


# Overview

Overview of Doppler V3

## Doppler V3 SDK Overview

### What is the Doppler V3 SDK?

The Doppler V3 SDK is a TypeScript library that provides developers with comprehensive tools to interact with the Doppler protocol's V3 implementation. It enables seamless integration with Doppler's token creation, price discovery, and liquidity migration systems across multiple EVM-compatible networks.

The SDK abstracts the complexity of smart contract interactions, providing type-safe methods for creating tokens, managing pools, and handling the complete lifecycle of assets on the Doppler protocol. It's designed to support both read-only operations for data querying and write operations for token deployment and pool management.

### How does Doppler v3 work?

Doppler v3 has a few configurations depending on an application's goals. It can be created to migrate post-bonding curve liquidity into Uniswap v2 to accumulate more fees overtime, Uniswap v4 to support application specific fee tiers, or left within positions on Uniswap v3. Here's an overview.

With "migration" to Uniswap v2

<figure><img src="/files/w9V1tKC32Ny7D2eaf93V" alt=""><figcaption></figcaption></figure>

With "migration" to Uniswap v4

<figure><img src="/files/SsUbtPLHGNlUjmRiFgOJ" alt=""><figcaption></figcaption></figure>

Without "migration"

<figure><img src="/files/mXfQKnfPTHSn6X1uUey5" alt=""><figcaption></figcaption></figure>

### What is Drift?

The Doppler V3 SDK is built on top of [Drift](https://delvtech.github.io/drift/), a modern blockchain interaction framework that simplifies Web3 development by:

* Providing unified interfaces for different wallet providers and RPC endpoints
* Handling transaction lifecycle management with automatic retry logic
* Offering type-safe contract interaction patterns
* Supporting both read and write operations with consistent error handling

### Core Concepts

#### Supported Networks

The V3 SDK supports multiple networks where Doppler protocol contracts are deployed:

| Network              | Chain ID | Environment | Purpose                 |
| -------------------- | -------- | ----------- | ----------------------- |
| **Base Mainnet**     | `8453`   | Production  | Live token deployments  |
| **Unichain Mainnet** | `130`    | Production  | Live token deployments  |
| **Ink**              | `57073`  | Production  | Live token deployments  |
| **Base Sepolia**     | `84532`  | Testnet     | Development and testing |

Network addresses and configurations are automatically managed through the `DOPPLER_V3_ADDRESSES` constant, making it easy to switch between networks.

#### Factory Classes

The SDK provides two main factory classes for different interaction patterns:

| Class              | Purpose                              | Use Cases                           |
| ------------------ | ------------------------------------ | ----------------------------------- |
| `ReadFactory`      | Query protocol state and read data   | Analytics, monitoring, data display |
| `ReadWriteFactory` | Create tokens and manage deployments | Token creation, pool management     |

#### Asset Lifecycle Management

The SDK handles the complete Doppler asset lifecycle:

1. **Token Creation**: Deploy new DERC20 tokens with customizable parameters
2. **Price Discovery**: Manage initial liquidity and price discovery phases
3. **Migration**: Handle liquidity migration to Uniswap V2 pools (or V4 with fee streaming)
4. **Pool Operations**: Interact with both price discovery and migrated pools

### Key Features

#### Token Creation & Configuration

The SDK provides comprehensive token creation capabilities:

* **Flexible Configuration**: Support for custom token parameters, sale configurations, and governance settings
* [**Configuration Types**](#default-configurations-and-customization): Utilize `DefaultConfigs` for predefined config types
* **Parameter Validation**: Automatic validation of creation parameters to prevent deployment errors
* **Gas Estimation**: Built-in simulation capabilities for accurate gas estimation

#### Pool Interaction

Comprehensive pool management and querying:

* **Pool State Queries**: Access to slot0, token information, and fee structures
* **Event Tracking**: Retrieve mint, burn, and swap events from pools
* **Multi-Protocol Support**: Handle both V3 price discovery pools and migrated V2/V4 pools
* **Real-time Data**: Live access to pool liquidity, prices, and trading activity

#### V4 Migration Support

Built-in support for migrating to Uniswap V4 with advanced features:

* **Fee Streaming Configuration**: Set up fee distribution to multiple beneficiaries
* **Beneficiary Management**: Automatic sorting and validation of beneficiary data
* **Migration Validation**: Ensure proper configuration before migration execution

### Contract Integration

#### Core Contracts

The SDK integrates with several key contract types:

| Contract Type       | Purpose                         | SDK Integration                    |
| ------------------- | ------------------------------- | ---------------------------------- |
| `Airlock`           | Main factory for token creation | `ReadFactory`, `ReadWriteFactory`  |
| `TokenFactory`      | DERC20 token deployment         | Automatic integration via factory  |
| `GovernanceFactory` | Governance contract creation    | Configurable through creation flow |
| `PoolInitializer`   | V3 pool initialization          | Automatic pool setup               |
| `LiquidityMigrator` | Handle pool migrations          | Migration flow management          |
| `UniswapV3Pool`     | Price discovery pools           | `ReadUniswapV3Pool` class          |

#### Event Handling

The SDK provides access to important protocol events:

* **Create Events**: Track new token deployments with full asset data
* **Migrate Events**: Monitor liquidity migrations between pool types
* **Pool Events**: Access mint, burn, and swap events from individual pools
* **Transfer Events**: Track token transfers and balance changes

***

### Default Configurations & Customization

#### **Sale Configuration**

Token sale parameters, pricing, and distribution

| Key               | Type   | Default       | Purpose                   |
| ----------------- | ------ | ------------- | ------------------------- |
| `initialSupply`   | bigint | 1,000,000,000 | Starting supply of tokens |
| `numTokensToSell` | bigint | 900,000,000   | Amount of tokens to sell  |

```typescript
import {
  DefaultConfigs,
  DEFAULT_INITIAL_SUPPLY_WAD,
  DEFAULT_NUM_TOKENS_TO_SELL_WAD
} from 'doppler-v3-sdk';

const saleConfig: DefaultConfigs['defaultSaleConfig'] = {
  initialSupply: DEFAULT_INITIAL_SUPPLY_WAD, // parseEther("1_000_000_000")
  numTokensToSell: DEFAULT_NUM_TOKENS_TO_SELL_WAD, // parseEther("900_000_000")
};
```

***

#### Pool Configuration

Fee tiers, tick spacing, and initial liquidity

| Key                 | Type   | Default     | Purpose                                  |
| ------------------- | ------ | ----------- | ---------------------------------------- |
| `startTick`         | number | 175,000     | Lower price bound                        |
| `endTick`           | number | 225,000     | Upper price bound                        |
| `numPositions`      | number | 15          | Uniswap v3 positions placed              |
| `maxSharesToBeSold` | bigint | 0.35        | percentage of supply used in the auction |
| `fee`               | number | 10,000 (1%) | swap fees                                |

```typescript
import {
  DefaultConfigs,
  DEFAULT_START_TICK,
  DEFAULT_END_TICK,
  DEFAULT_NUM_POSITIONS,
  DEFAULT_MAX_SHARE_TO_BE_SOLD,
  DEFAULT_FEE,
} from 'doppler-v3-sdk';

const poolConfig: DefaultConfigs['defaultV3PoolConfig'] = {
  startTick: DEFAULT_START_TICK, // 175_000
  endTick: DEFAULT_END_TICK, // 225_000
  numPositions: DEFAULT_NUM_POSITIONS, // 15
  maxShareToBeSold: DEFAULT_MAX_SHARE_TO_BE_SOLD, // parseEther("0.35")
  fee: DEFAULT_FEE, // 10_000, 1% fee tier
};
```

***

#### Governance Configuration

Voting parameters, proposal thresholds, and timelock settings

| Key                        | Type   | Default   | Purpose                              |
| -------------------------- | ------ | --------- | ------------------------------------ |
| `initialVotingDelay`       | number | 172,800   | when voting can begin                |
| `initialVotingPeriod`      | number | 1,209,600 | how long a vote lasts                |
| `initialProposalThreshold` | bigInt | 0         | required tokens to create a proposal |

```typescript
import {
  DefaultConfigs,
  DEFAULT_INITIAL_VOTING_DELAY,
  DEFAULT_INITIAL_VOTING_PERIOD,
  DEFAULT_INITIAL_PROPOSAL_THRESHOLD,
} from 'doppler-v3-sdk';

const governanceConfig: DefaultConfigs['defaultGovernanceConfig'] = {
  initialVotingDelay: DEFAULT_INITIAL_VOTING_DELAY, // 172_800
  initialVotingPeriod: DEFAULT_INITIAL_VOTING_PERIOD, // 1_209_600
  initialProposalThreshold: DEFAULT_INITIAL_PROPOSAL_THRESHOLD, // BigInt(0);
};
```

***

#### Vesting Configuration

Token vesting schedules and inflation parameters

| Key               | Type                            | Default                          | Purpose                           |
| ----------------- | ------------------------------- | -------------------------------- | --------------------------------- |
| `yearlyMintRate`  | bigint                          | 0.02 (2%)                        | annual token inflation            |
| `vestingDuration` | bigint                          | 31,536,000 (one year in seconds) | vesting cadence                   |
| `recipients`      | [Address\[\]](https://viem.sh/) | \[]                              | vesting recipients                |
| `amounts`         | bigint\[]                       | \[]                              | vesting amount for each recipient |

```typescript
import {
  DefaultConfigs,
  DEFAULT_YEARLY_MINT_RATE_WAD,
  DEFAULT_VESTING_DURATION,
} from 'doppler-v3-sdk';
import { parseEther } from "viem";

const vestingConfig: DefaultConfigs['defaultVestingConfig'] = {
  yearlyMintRate: DEFAULT_YEARLY_MINT_RATE_WAD, // parseEther("0.02")
  vestingDuration: DEFAULT_VESTING_DURATION, // BigInt(ONE_YEAR_IN_SECONDS)
  recipients: ["0x..."], // custom recipients
  amounts: [parseEther('50_000_000')], // custom amounts
};
```

***

### Error Handling & Validation

#### Built-in Validations

The SDK includes comprehensive validation for:

* **Parameter Consistency**: Ensure all configuration parameters are compatible
* **Network Compatibility**: Validate contract addresses for the target network
* **Mathematical Constraints**: Check that numerical parameters meet protocol requirements
* **Beneficiary Configuration**: Validate fee streaming beneficiary shares sum to 100%

#### Error Types

Common error scenarios handled by the SDK:

* **Configuration Errors**: Invalid parameter combinations or missing required fields
* **Network Errors**: RPC failures, transaction timeouts, and connection issues
* **Contract Errors**: Revert reasons, gas estimation failures, and execution errors
* **Validation Errors**: Parameter validation failures and constraint violations

## Next steps

For detailed implementation examples, see the [Getting Started Guide](/reference/legacy-sdks-and-migration-guides/v3/getting-started) and explore the comprehensive API documentation in the [Factory Reference](/reference/legacy-sdks-and-migration-guides/v3/factory).


# Get Started

Getting Started with Doppler V3 SDK

This section guides you through setting up and using the Doppler V3 SDK to interact with the Doppler protocol.

## Prerequisites

* **Node.js**: Version `18.14` or higher
* **npm** or **yarn**: Package manager for installing dependencies
* **Web3 Provider**: Access to Ethereum RPC endpoints (Infura, Alchemy, etc.)
* **Wallet**: MetaMask or similar wallet for transaction signing

## Installation

Install the Doppler V3 SDK, Viem, and Drift packages:

```bash
npm install doppler-v3-sdk viem @delvtech/drift @delvtech/drift-viem
# or
yarn add doppler-v3-sdk viem @delvtech/drift @delvtech/drift-viem
```

The SDK uses [Drift](https://github.com/delvtech/drift) for blockchain interactions.

## Required Environment Variables

```bash
# RPC Endpoint
RPC_URL="https://sepolia.base.org"

# Wallet Private Key (for automated transactions)
PRIVATE_KEY="your-private-key"

# Network Configuration
CHAIN_ID=84532
```

## Basic Setup

### 1. Import the SDK

```typescript
import { 
  ReadFactory, 
  ReadWriteFactory, 
  ReadUniswapV3Pool,
  DOPPLER_V3_ADDRESSES 
} from 'doppler-v3-sdk';
```

### 2. Initialize Drift Client

Set up your Drift client with both read and write capabilities:

#### Read-only operations

```typescript
import { createDrift } from "@delvtech/drift";

// For read-only operations
const drift = createDrift({
  rpcUrl: 'https://sepolia.base.org',
  chainId: 84532 // Base Sepolia
});
```

#### Read-write operations

```typescript
import { createDrift } from "@delvtech/drift";
import { createPublicClient, createWalletClient, http, PublicClient } from "viem";

const publicClient = createPublicClient({
  chain: baseSepolia,
  transport: http('https://sepolia.base.org'),
});

const walletClient = createWalletClient({
  chain: baseSepolia,
  transport: http('https://sepolia.base.org'),
  account: privateKeyToAccount(WALLET_PRIVATE_KEY),
});

// For read-write operations (with wallet)
const driftWithWallet = createDrift({
  adapter: viemAdapter({ publicClient, walletClient }),
});
```

### 3. Get Protocol Addresses

```typescript
// Get addresses for the current network
const chainId = 84532; // Base Sepolia (change to 8453 for Base Mainnet, 130 for Unichain Mainnet, etc.)
const addresses = DOPPLER_V3_ADDRESSES[chainId];
const airlockAddress = addresses.airlock;
```

## Core Concepts

### Factory Classes

The SDK provides two main factory classes:

* **`ReadFactory`**: For querying protocol state and reading data
* **`ReadWriteFactory`**: For creating tokens and interacting with pools

### Asset Lifecycle

1. **Token Creation**: Deploy new tokens through the factory
2. **Price Discovery**: Initial liquidity provision and price discovery phase
3. **Migration**: Move liquidity to standard Uniswap V2 pools (or V4 pools if using fee streaming)
4. **Trading**: Normal trading on migrated pools

## Quick Start Examples

### Reading Protocol Data

```typescript
// Create a read factory instance
const factory = new ReadFactory(
  airlockAddress,
  drift,
);

// Get information about a deployed asset
const assetData = await factory.getAssetData(tokenAddress);
console.log('Asset details:', {
  numeraire: assetData.numeraire,
  governance: assetData.governance,
  pool: assetData.pool,
  totalSupply: assetData.totalSupply.toString()
});

// Check module state
const moduleState = await factory.getModuleState(moduleAddress);
console.log('Module state:', moduleState);
```

### Creating a New Token

```typescript
const addresses = DOPPLER_V3_ADDRESSES[chainId];
const airlockAddress = addresses.airlock;
const bundlerAddress = addresses.bundler;

// Create a read-write factory instance
const factory = new ReadWriteFactory(
  airlockAddress,
  bundlerAddress,
  driftWithWallet,
);

// Define token creation parameters
const createParams = {
  integrator: integratorAddress,
  userAddress: userAddress,
  numeraire: numeraireAddress, // USDC, WETH, etc.
  contracts: {
    tokenFactory: addresses.tokenFactory,
    governanceFactory: addresses.governanceFactory,
    poolInitializer: addresses.poolInitializer,
    liquidityMigrator: addresses.liquidityMigrator
  },
  tokenConfig: {
    name: "My Token",
    symbol: "MTK",
    decimals: 18
  },
  vestingConfig: "default" // or custom configuration
};

// Encode the creation parameters
const { createParams: encodedParams } = factory.encode(createParams);

// Simulate the creation transaction
const simulation = await factory.simulateCreate(encodedParams);
console.log('Gas estimate:', simulation.gasEstimate);

// Execute the creation transaction
const txHash = await factory.create(encodedParams);
console.log('Transaction hash:', txHash);

// Wait for transaction confirmation and get the deployed asset address
const receipt = await drift.waitForTransactionReceipt({ hash: txHash });
const createEvent = receipt.logs.find(log => 
  log.topics[0] === '0x...' // Create event signature
);
const deployedTokenAddress = `0x${createEvent.topics[1].slice(26)}`;
console.log('Deployed token address:', deployedTokenAddress);
```

### Interacting with Your Created Pool

Once you've created a token, you can interact with its price discovery pool:

```typescript
// Get the asset data to find the price discovery pool address
const assetData = await factory.getAssetData(deployedTokenAddress);
const poolAddress = assetData.pool;

// Create a pool instance for the price discovery pool
const pool = new ReadUniswapV3Pool(poolAddress, drift);

// Get pool information
const slot0 = await pool.getSlot0();
const token0 = await pool.getToken0();
const token1 = await pool.getToken1();
const fee = await pool.getFee();

console.log('Price discovery pool info:', {
  sqrtPriceX96: slot0.sqrtPriceX96.toString(),
  tick: slot0.tick,
  token0,
  token1,
  fee
});

// Get pool events (will show the initial mint from token creation)
const mintEvents = await pool.getMintEvents();
const swapEvents = await pool.getSwapEvents();
console.log(`Found ${mintEvents.length} mint events and ${swapEvents.length} swap events`);

// Note: After migration, liquidity moves to a V2 pool (or V4 if using fee streaming)
// The migrationPool address can be found at assetData.migrationPool
```

### Network Support

The SDK supports multiple networks:

* **Base Sepolia** (chainId: 84532) - Testnet
* **Base Mainnet** (chainId: 8453) - Production
* **Unichain Mainnet** (chainId: 130) - Production
* **Unichain Sepolia** (chainId: 1301) - Testnet
* **Ink** (chainId: 57073) - Production

For complete network addresses and additional supported networks, see the [Contract Addresses](/reference/contract-addresses) documentation.

You can get free Base Sepolia ETH from the [Base Sepolia faucet](https://docs.base.org/tools/network-faucets) to test your applications.

## Error Handling

The SDK provides comprehensive error handling for common scenarios:

```typescript
try {
  const assetData = await factory.getAssetData(tokenAddress);
} catch (error) {
  if (error.message.includes('Asset not found')) {
    console.log('Token not deployed on Doppler');
  } else {
    console.error('Unexpected error:', error);
  }
}
```

## Next Steps

* Explore the [Factory Reference](/reference/legacy-sdks-and-migration-guides/v3/factory) for detailed API documentation
* Learn about [Token Operations](/reference/legacy-sdks-and-migration-guides/v3/token) for managing deployed tokens
* Understand [Quoter Usage](/reference/legacy-sdks-and-migration-guides/v3/quoter) for price calculations
* Check out [V4 Migration](/reference/legacy-sdks-and-migration-guides/v3/custom-fees) for upgrading to V4

## Support

For additional help and examples:

* Check the [Token Launch Examples](/reference/legacy-sdks-and-migration-guides/v4/examples) for comprehensive deployment scenarios
* Review the [Implementation Guide](broken://pages/2xkvOvHSEt9zfaWDOv5z) for protocol details
* Join the community for discussions and support


# Factory

Factory Class Reference

### Factory Overview

The factory is the main entrypoint for interacting with and creating tokens on the Doppler protocol. It is responsible for deploying new tokens, parameterizing pools, migrating existing tokens, and serving information about the state of the protocol.

#### ReadFactory

Read-only operations for the Doppler factory.

```typescript
const factory = new ReadFactory(address: Address, drift: Drift<ReadAdapter>);
```

Methods:

* `getModuleState(address: Address): Promise<ModuleState>`
  * Returns the state of a module (NotWhitelisted, TokenFactory, GovernanceFactory, HookFactory, Migrator)
* `getAssetData(asset: Address): Promise<AssetData>`
  * Returns information about a DERC20 token deployed by the airlock contract:

    ```typescript
    interface AssetData {
      numeraire: Address;
      timelock: Address;
      governance: Address;
      liquidityMigrator: Address;
      poolInitializer: Address;
      pool: Address;
      migrationPool: Address;
      numTokensToSell: bigint;
      totalSupply: bigint;
      integrator: Address;
    }
    ```
* `getCreateEvents(): Promise<Event[]>`
* `getMigrateEvents(): Promise<Event[]>`

#### ReadWriteFactory

Extends ReadFactory with write operations.

The primary use case for the ReadWriteFactory is to encode the parameters for, simulate, and invoke the `create` method. Proper utilization of the ReadWriteFactory involves the following steps:

1. Initialize a new `ReadWriteFactory` with the factory (airlock) address and a drift client with both a public and wallet client attached.
2. Pass `CreateV3PoolParams` to the `encode` method in order to build the calldata for the `create` method. Note that the `saleConfig`, `v3PoolConfig`, and `governanceConfig` are optional and can be omitted. Omission of these parameters will use the default values for each respective config. The default values are exported from the `doppler-v3-sdk` package under `defaultSaleConfig`, `defaultV3PoolConfig`, and `defaultGovernanceConfig`, and `defaultVestingConfig`. These default configs can be modified to customize the token creation process, but modifications should be made with caution as they may break some of the algebraic invariants of the Doppler protocol, or result in unexpected outcomes. For example, custom implementations should note that vesting is defined as a percentage of inflation relative to total token supply.

```typescript
export interface CreateV3PoolParams {
  integrator: Address;
  userAddress: Address;
  numeraire: Address;
  contracts: InitializerContractDependencies;
  tokenConfig: TokenConfig;
  saleConfig?: Partial<SaleConfig>;
  v3PoolConfig?: Partial<V3PoolConfig>;
  vestingConfig: VestingConfig | "default";
  governanceConfig?: Partial<GovernanceConfig>;
}

public encode(params: CreateV3PoolParams): {
    createParams: CreateParams;
    v3PoolConfig: V3PoolConfig;
}
```

`encode` returns the following payload, which is used to simulate and invoke the `create` method:

```typescript
interface CreateParams {
  initialSupply: bigint;
  numTokensToSell: bigint;
  numeraire: Address;
  tokenFactory: Address;
  tokenFactoryData: Hex;
  governanceFactory: Address;
  governanceFactoryData: Hex;
  poolInitializer: Address;
  poolInitializerData: Hex;
  liquidityMigrator: Address;
  liquidityMigratorData: Hex;
  integrator: Address;
  salt: Hex;
}
```

3. Simulate the `create` method to ensure the parameters are correct

```typescript
  public async simulateCreate(params: CreateParams): Promise<FunctionReturn<AirlockABI, "create">>
```

**Note:** The `simulateCreate` method is a simulation of the `create` method and does not consume any gas. It is recommended to simulate the `create` method before invoking it in order to ensure that the parameters are correct and that the method will succeed.

4. Invoke the `create` method

### Streamable V3 (Lockable V3 Initializer)

The SDK supports creating V3 pools with fee streaming capabilities through the lockable V3 initializer. When beneficiaries are specified in the `v3PoolConfig`, the pool will be permanently locked and stream trading fees to the specified beneficiaries.

To use the lockable V3 initializer:

1. **Specify the lockable initializer address** in your contracts configuration:

```typescript
const contracts = {
  tokenFactory: DOPPLER_V3_ADDRESSES[chainId].tokenFactory,
  governanceFactory: DOPPLER_V3_ADDRESSES[chainId].governanceFactory,
  v3Initializer: DOPPLER_V3_ADDRESSES[chainId].lockableV3Initializer, // Use lockable initializer
  liquidityMigrator: DOPPLER_V3_ADDRESSES[chainId].liquidityMigrator,
};
```

2. **Add beneficiaries to the v3PoolConfig**:

```typescript
const v3PoolConfig = {
  // ... standard pool config
  beneficiaries: [
    {
      beneficiary: ownerAddress, // Airlock owner must receive exactly 5%
      shares: parseEther("0.05")
    },
    {
      beneficiary: projectAddress,
      shares: parseEther("0.95") 
    }
  ]
};
```

**Important requirements:**

* Total beneficiary shares must equal 1e18 (100%)
* The Airlock owner must be included with exactly 5% shares
* Beneficiaries must be sorted by address in ascending order
* Pools with beneficiaries will be permanently locked and never migrate

For more detailed information about streamable V3 pools, see the [Streamable V3 documentation](https://github.com/whetstoneresearch/doppler-docs/blob/main/v3-sdk/streamable-v3.md).

### V4 Migrator Support

The ReadWriteFactory now includes helper functions for configuring V4 migration with fee streaming:

* `sortBeneficiaries(beneficiaries: BeneficiaryData[]): BeneficiaryData[]`
  * Sorts beneficiaries by address in ascending order (required by the V4 migrator contract)
* `encodeV4MigratorData(data: V4MigratorData): Hex`
  * Encodes V4 migrator configuration including fee tier, tick spacing, lock duration, and beneficiaries
  * Validates that beneficiaries are properly sorted and shares sum to exactly 1e18 (100%)

For detailed V4 migrator usage, see the [V4 Migrator documentation](/reference/legacy-sdks-and-migration-guides/v3/custom-fees).

```typescript
  public async create(
    params: CreateParams,
    options?: ContractWriteOptions & OnMinedParam
  ): Promise<Hex>
```

#### Pool Operations

**ReadUniswapV3Pool**

Read operations for Doppler V3 pools.

```typescript
const pool = new ReadUniswapV3Pool(address: Address, drift?: Drift<ReadAdapter>);
```

Methods:

* `getMintEvents(): Promise<Event[]>`
* `getBurnEvents(): Promise<Event[]>`
* `getSwapEvents(): Promise<Event[]>`
* `getSlot0(): Promise<{sqrtPriceX96: bigint, tick: number}>`
* `getToken0(): Promise<Address>`
* `getToken1(): Promise<Address>`
* `getFee(): Promise<number>`

#### Event Types

**Factory Events**

```typescript
interface CreateEvent {
  asset: Address;
  numeraire: Address;
  initializer: Address;
  poolOrHook: Address;
}

interface MigrateEvent {
  asset: Address;
  pool: Address;
}
```

**Pool Events**

```typescript
interface MintEvent {
  owner: Address;
  tickLower: number;
  tickUpper: number;
  amount: bigint;
  amount0: bigint;
  amount1: bigint;
}

interface BurnEvent {
  owner: Address;
  tickLower: number;
  tickUpper: number;
  amount: bigint;
  amount0: bigint;
  amount1: bigint;
}

interface SwapEvent {
  sender: Address;
  recipient: Address;
  amount0: bigint;
  amount1: bigint;
  sqrtPriceX96: bigint;
  liquidity: bigint;
  tick: number;
}
```

#### Network Configuration

```typescript
const DOPPLER_V3_ADDRESSES: { [chainId: number]: DopplerV3Addresses };
```

Supported Networks:

* Unichain Sepolia (chainId: 1301)
* Unichain (chainId: 130)

### Doppler v4 SDK API Reference

Coming Soon!


# Token

Token Class Reference

### Token Class Overview

The `Token` class is a javascript class representation of a DERC20 token deployed by the airlock contract. It is responsible for serving information about the state of the token, and for performing various actions on Doppler tokens.

### ReadDerc20

Read operations for Doppler tokens.

```typescript
const token = new ReadDerc20(address: Address, drift?: Drift<ReadAdapter>);
```

Methods:

DERC20 tokens extend the ERC20 interface, and so have the following methods:

* `getName(): Promise<string>`
* `getSymbol(): Promise<string>`
* `getDecimals(): Promise<number>`
* `getTokenURI(): Promise<string>`
* `getAllowance(owner: Address, spender: Address): Promise<bigint>`
* `getBalanceOf(account: Address): Promise<bigint>`
* `getTotalSupply(): Promise<bigint>`

In addition, DERC20 tokens have the following methods:

* `getPool(): Promise<Address>`
  * `getPool` returns the address of the Uniswap V2 pool that the DERC20 token is migrated to after the liquidity bootstrapping process is complete.
* `getIsPoolUnlocked(): Promise<boolean>`
  * `getIsPoolUnlocked` returns `true` if the Uniswap V2 pool is unlocked, and `false` otherwise.
* `getVestingData(account: Address): Promise<{totalAmount: bigint, releasedAmount: bigint}>`
  * `getVestingData` returns the total amount of tokens that have been vested for a given account, and the amount of tokens that have been released to the account.
* `getVestingDuration(): Promise<bigint>`
  * `getVestingDuration` returns the duration of the vesting period for the token.
* `getVestingStart(): Promise<bigint>`
  * `getVestingStart` returns the start time of the vesting period for the token.
* `getVestedTotalAmount(): Promise<bigint>`
  * `getVestedTotalAmount` returns the total amount of tokens that have been vested for the token.
* `getYearlyMintRate(): Promise<bigint>`
  * `getYearlyMintRate` returns the yearly mint rate for the token.

### ReadWriteDerc20

Extends ReadDerc20 with basic write operations.

```typescript
const token = new ReadWriteDerc20(address: Address, drift: Drift<ReadWriteAdapter>);
```

Methods:

* `approve(spender: Address, value: bigint): Promise<Hash>`


# Quoter

Quoter Class Reference

The Quoter provides price quoting functionality for both Uniswap V3 and V2 swaps. It enables simulation of exact input/output swaps without executing transactions, with proper decimal handling for amounts.

## ReadQuoter

```typescript
export class ReadQuoter {
  constructor(
    quoteV2Address: Address,
    univ2RouterAddress: Address,
    drift: Drift<ReadAdapter> = createDrift()
  )
```

Methods:

* **quoteExactInputV3** - Get price quote for exact input swap (Uniswap V3)

```typescript
async quoteExactInputV3(
  params: FunctionArgs<QuoterV2ABI, "quoteExactInputSingle">["params"]
): Promise<FunctionReturn<QuoterV2ABI, "quoteExactInputSingle">>
```

* **quoteExactOutputV3** - Get price quote for exact output swap (Uniswap V3)

```typescript
async quoteExactOutputV3(
  params: FunctionArgs<QuoterV2ABI, "quoteExactOutputSingle">["params"]
): Promise<FunctionReturn<QuoterV2ABI, "quoteExactOutputSingle">>
```

* **quoteExactInputV2** - Get price quote for exact input swap (Uniswap V2)

```typescript
async quoteExactInputV2(
  params: FunctionArgs<UniswapV2Router02ABI, "getAmountsOut">
): Promise<FunctionReturn<UniswapV2Router02ABI, "getAmountsOut">>
```

* **quoteExactOutputV2** - Get price quote for exact output swap (Uniswap V2)

```typescript
async quoteExactOutputV2(
  params: FunctionArgs<UniswapV2Router02ABI, "getAmountsIn">
): Promise<FunctionReturn<UniswapV2Router02ABI, "getAmountsIn">>
```

### Contract ABIs

```typescript
export type QuoterV2ABI = typeof quoterV2Abi;
export type UniswapV2Router02ABI = typeof uniswapV2Router02Abi;
```

## Example Usage

```typescript
const quoter = new ReadQuoter("0xQuoterV2Address", "0xUniV2RouterAddress");

// V3 Exact Input Quote
const v3InputQuote = await quoter.quoteExactInputV3({
  tokenIn: "0x...",
  tokenOut: "0x...",
  amountIn: 1000000n,
  fee: 3000,
  sqrtPriceLimitX96: 0n,
});

// V2 Exact Output Quote
const v2OutputQuote = await quoter.quoteExactOutputV2({
  amountOut: 500000n,
  path: ["0x...", "0x..."],
  to: "0x...",
  deadline: Math.floor(Date.now() / 1000) + 300,
});
```


# Custom Fees

Arbitrary migration to Uniswap v4 pools with customizable fees

The V3 SDK includes support for creating Doppler V3 pools that can migrate their liquidity to Uniswap V4 with customizable fee streaming. This allows protocols to distribute trading fees to multiple beneficiaries over time, and importantly, customize the post-graduation fee amounts.&#x20;

## Overview

The "V4 migrator" enables:

* Migration from v3 or v4 Doppler pools to Uniswap V4 pools
* Fee streaming to multiple beneficiaries with custom shares
* Time-locked liquidity with configurable duration
* Support for both standard and no-op governance models

## Usage with V3 SDK

Doppler v3 tokens can specify their usage of the Uniswap v4 migrator at the time of creation.&#x20;

### Integration with CreateV3PoolParams

The V3 SDK's `CreateV3PoolParams` interface now includes an optional `liquidityMigratorData` field:

```typescript
interface CreateV3PoolParams {
  // ... other parameters ...
  liquidityMigratorData?: Hex; // Encoded V4 migration configuration
}
```

### Step 1: Configure Beneficiaries

```typescript
import { BeneficiaryData, WAD } from "doppler-v3-sdk";

// Shares must sum to exactly WAD (1e18)
const beneficiaries: BeneficiaryData[] = [
  { 
    beneficiary: "0x...", // Treasury address
    shares: WAD * 70n / 100n  // 70% of fees
  },
  { 
    beneficiary: "0x...", // Development fund
    shares: WAD * 20n / 100n  // 20% of fees
  },
  { 
    beneficiary: "0x...", // Community rewards
    shares: WAD * 10n / 100n  // 10% of fees
  }
];
```

### Step 2: Sort and Validate Beneficiaries

Beneficiaries must be sorted by address in ascending order:

```typescript
import { ReadWriteFactory } from "doppler-v3-sdk";

const factory = new ReadWriteFactory(airlockAddress, bundlerAddress);
const sortedBeneficiaries = factory.sortBeneficiaries(beneficiaries);
```

### Step 3: Configure V4 Migrator

```typescript
import { V4MigratorData } from "doppler-v3-sdk";

const migratorConfig: V4MigratorData = {
  fee: 3000,                        // 0.3% fee tier
  tickSpacing: 60,                  // Tick spacing for the V4 pool
  lockDuration: 365 * 24 * 60 * 60, // 1 year lock
  beneficiaries: sortedBeneficiaries
};
```

### Step 4: Encode Configuration

```typescript
const encodedMigratorData = factory.encodeV4MigratorData(migratorConfig);
```

### Step 5: Create Pool with V4 Migration

```typescript
const createParams = await factory.buildConfig({
  integrator: "0x...",
  userAddress: userAddress,
  numeraire: addresses.unichain.weth,
  contracts: {
    tokenFactory: addresses.unichain.tokenFactory,
    governanceFactory: addresses.unichain.governanceFactory,
    poolInitializer: addresses.unichain.v3Initializer,
    liquidityMigrator: addresses.unichain.liquidityMigrator, // V4 migrator
  },
  tokenConfig: {
    name: "My Token",
    symbol: "MTK",
    tokenURI: "https://example.com/token-metadata.json"
  },
  // Pass the encoded migrator data
  liquidityMigratorData: encodedMigratorData,
  // ... other parameters
});

await factory.create(createParams);
```

## Querying Migrator State

Use the `ReadMigrator` class to query migrator configuration:

```typescript
import { ReadMigrator } from "doppler-v3-sdk";

const migrator = new ReadMigrator(migratorAddress);

// Get V4 pool configuration
const poolKey = await migrator.getAssetData(token0, token1);
console.log(`Fee tier: ${poolKey.fee}`);
console.log(`Tick spacing: ${poolKey.tickSpacing}`);

// Get contract addresses
const locker = await migrator.locker();           // StreamableFeesLocker
const poolManager = await migrator.poolManager(); // V4 PoolManager
const airlock = await migrator.airlock();         // Airlock address
```

## Important Considerations

### Beneficiary Requirements

1. **Sorted Order**: Beneficiaries must be sorted by address (ascending)
2. **Positive Shares**: All shares must be greater than 0
3. **Sum to WAD**: Total shares must equal exactly 1e18 (100%)

### Fee Tiers and Tick Spacing

Common V4 configurations:

* 0.01% fee → 1 tick spacing
* 0.05% fee → 10 tick spacing
* 0.3% fee → 60 tick spacing
* 1% fee → 200 tick spacing

### Lock Duration

* Minimum: No minimum (can be 0 for immediate unlocking)
* Maximum: No maximum (can be set to centuries for permanent locks)
* Typical: 1-4 years for protocol-owned liquidity

## Helper Functions

### sortBeneficiaries

```typescript
const sorted = factory.sortBeneficiaries(beneficiaries);
```

Sorts beneficiaries by address in ascending order (required by contract).

### encodeV4MigratorData

```typescript
const encoded = factory.encodeV4MigratorData(migratorConfig);
```

Encodes the V4 migrator configuration for use in pool creation. Validates:

* Beneficiaries are sorted
* All shares are positive
* Total shares equal WAD

## Migration Flow

1. **Pool Creation**: Doppler V3 pool is created with initial liquidity
2. **Trading Phase**: Users trade in the V3 pool during the sale period
3. **Migration Trigger**: When conditions are met (time/volume), migration begins
4. **Liquidity Exit**: All liquidity is removed from the V3 pool
5. **V4 Pool Creation**: New V4 pool is created with full-range liquidity
6. **Fee Streaming**: Trading fees accumulate and stream to beneficiaries

## Example: Multi-Beneficiary Setup

```typescript
// Example: Protocol with multiple stakeholders
const beneficiaries: BeneficiaryData[] = [
  { 
    beneficiary: "0x123...", // DAO Treasury
    shares: WAD * 40n / 100n // 40%
  },
  { 
    beneficiary: "0x456...", // Core Team
    shares: WAD * 25n / 100n // 25%
  },
  { 
    beneficiary: "0x789...", // Ecosystem Fund
    shares: WAD * 20n / 100n // 20%
  },
  { 
    beneficiary: "0xabc...", // Bug Bounty Reserve
    shares: WAD * 10n / 100n // 10%
  },
  { 
    beneficiary: "0xdef...", // Community Incentives
    shares: WAD * 5n / 100n  // 5%
  }
];

// Create configuration with 2-year lock
const config: V4MigratorData = {
  fee: 10000,              // 1% fee for exotic pair
  tickSpacing: 200,        // Corresponding tick spacing
  lockDuration: 2 * 365 * 24 * 60 * 60, // 2 years
  beneficiaries: factory.sortBeneficiaries(beneficiaries)
};
```

## See Also

* [StreamableFeesLocker Reference](/reference/legacy-sdks-and-migration-guides/v4/custom-fees)
* [Factory Reference](/reference/legacy-sdks-and-migration-guides/v3/factory)
* [V4 SDK Documentation](/reference/legacy-sdks-and-migration-guides/v4/factory)


# Governance Options

This guide explains how to configure different governance options when creating tokens with the Doppler V3 SDK, including using the NoOpGovernanceFactory for gas-efficient deployments.

## Overview

The Doppler V3 SDK supports optional governance with the following models.

1. **"Standard" Governance** - Full on-chain governance with timelock using OpenZeppelin Governor
2. **"No-Op" Governance** - Minimal governance for gas savings (sets governance to `0xdead`)

## Using NoOpGovernanceFactory

The NoOpGovernanceFactory creates tokens without active governance, significantly reducing deployment costs and complexity. This is ideal for projects that don't require on-chain governance.

### Prerequisites

Ensure the NoOpGovernanceFactory is deployed on your target chain. Currently available on:

* **Base Sepolia**: `0x916B8987E4aD325C10d58ED8Dc2036a6FF5EB228`

### Implementation

```typescript
import { ReadWriteFactory, CreateV3PoolParams } from 'doppler-v3-sdk';
import { DOPPLER_V3_ADDRESSES } from 'doppler-v3-sdk';

// Get addresses for your chain
const chainId = 84532; // Base Sepolia
const addresses = DOPPLER_V3_ADDRESSES[chainId];

// Check if NoOpGovernanceFactory is available
if (!addresses.noOpGovernanceFactory) {
  throw new Error('NoOpGovernanceFactory not deployed on this chain');
}

// Create parameters with NoOpGovernanceFactory
const createParams: CreateV3PoolParams = {
  integrator: '0x...', // Your integrator address
  userAddress: '0x...', // User creating the token
  numeraire: '0x...', // Base token (e.g., WETH)
  contracts: {
    tokenFactory: addresses.tokenFactory,
    // Use NoOpGovernanceFactory instead of standard governanceFactory
    governanceFactory: addresses.noOpGovernanceFactory,
    v3Initializer: addresses.v3Initializer,
    liquidityMigrator: addresses.liquidityMigrator,
  },
  tokenConfig: {
    name: 'My Token',
    symbol: 'MTK',
    tokenURI: 'https://example.com/metadata.json',
  },
  vestingConfig: 'default',
  // Optional: Configure V4 migration
  liquidityMigratorData: '0x...', // See V4 migrator docs
};

// Create the factory instance
const factory = new ReadWriteFactory(addresses.airlock, driftClient);

// Encode and create the token
const createData = await factory.encodeCreateData(createParams);
```

## Using Standard Governance

For tokens that require governance functionality:

```typescript
const createParams: CreateV3PoolParams = {
  // ... other parameters ...
  contracts: {
    tokenFactory: addresses.tokenFactory,
    // Use standard governanceFactory
    governanceFactory: addresses.governanceFactory,
    v3Initializer: addresses.v3Initializer,
    liquidityMigrator: addresses.liquidityMigrator,
  },
  // Optional: Customize governance parameters
  governanceConfig: {
    initialVotingDelay: 3600, // 1 hour
    initialVotingPeriod: 172800, // 48 hours
    initialProposalThreshold: 1000000n, // 1% of supply
  },
};
```

## Custom Governance Factory

To use a custom governance factory (must be whitelisted):

```typescript
const createParams: CreateV3PoolParams = {
  // ... other parameters ...
  contracts: {
    tokenFactory: addresses.tokenFactory,
    // Use your custom governance factory address
    governanceFactory: '0xYourCustomGovernanceFactory',
    v3Initializer: addresses.v3Initializer,
    liquidityMigrator: addresses.liquidityMigrator,
  },
};
```

## Fee Distribution

### Standard Governance (90/10 Split)

* 90% of liquidity → Timelock (controlled by governance)
* 10% of liquidity → StreamableFeesLocker (distributed to beneficiaries)

### No-Op Governance (100% Locked)

For permanent liquidity provision, set the recipient to `DEAD_ADDRESS`:

```typescript
import { DEAD_ADDRESS } from "doppler-v3-sdk";

// In no-op governance, all liquidity goes to the locker
const recipient = DEAD_ADDRESS; // 0x000...dEaD
```

## See Also

* [V4 Migrator Guide](/reference/legacy-sdks-and-migration-guides/v3/custom-fees) - Configure V4 migration with beneficiaries
* [Token Launch Examples](https://github.com/whetstoneresearch/doppler-docs/blob/main/doppler-v3-sdk-reference/token-launch-examples.md) - Complete examples
* [Contract Addresses](https://github.com/whetstoneresearch/doppler-docs/blob/main/doppler-v3-sdk-reference/contract-addresses.md) - Deployment addresses by chain


# Streamable V3

Streamable V3 (Lockable V3 Pools) Reference

## Overview

Streamable V3 pools use the `LockableUniswapV3Initializer` contract to create Uniswap V3 pools with fee streaming capabilities. This feature allows projects to distribute trading fees to multiple beneficiaries over time, similar to the fee streaming available in Doppler V4 pools.

## Key Concepts

### What Makes a Pool "Streamable"?

A streamable V3 pool is created when you specify beneficiaries during pool initialization. These pools have the following characteristics:

2. **Fee Distribution**: Trading fees are collected and distributed to beneficiaries based on their share percentages
3. **No Graduation**: Unlike standard V3 pools, streamable pools never "graduate" or migrate to another pool type
4. **Continuous Revenue**: Beneficiaries can claim their share of fees at any time by calling `collectFees()`

### When to Use Streamable V3

Consider using streamable V3 pools when:

* You want to distribute trading fees to multiple stakeholders
* You prefer a simpler, permanent pool structure without migration

## Technical Requirements

### Beneficiary Configuration

When creating a streamable V3 pool, you must configure beneficiaries with the following requirements:

```typescript
interface BeneficiaryData {
  beneficiary: Address;  // Recipient address
  shares: bigint;       // Share amount in WAD (1e18 = 100%)
}
```

**Requirements:**

1. **Total Shares**: Must sum to exactly 1e18 (100%)
2. **Protocol Fee**: The Airlock owner must receive exactly 5% (0.05e18) shares
3. **Ordering**: Beneficiaries must be sorted by address in ascending order
4. **Non-zero Shares**: Each beneficiary must have shares > 0

### Example Configuration

```typescript
import { parseEther } from 'viem';
import { ReadWriteFactory, DOPPLER_V3_ADDRESSES } from '@doppler-v3-sdk';

// Set up wallet (use your preferred method - WalletConnect, injected, etc.)
const walletClient = createWalletClient({
  // ... your wallet configuration
});

// Set up factory
const chainId = 8453; // Base mainnet
const factory = new ReadWriteFactory(
  DOPPLER_V3_ADDRESSES[chainId].airlock,
  DOPPLER_V3_ADDRESSES[chainId].bundler,
  drift // your configured drift instance
);

// Get the Airlock owner address
const airlockOwner = await factory.owner();

// Configure beneficiaries
const beneficiaries = [
  {
    beneficiary: airlockOwner,
    shares: parseEther("0.05")  // 5% protocol fee (required)
  },
  {
    beneficiary: "0xProjectTreasury...",
    shares: parseEther("0.50")  // 50% to project treasury
  },
  {
    beneficiary: "0xDevelopmentFund...",
    shares: parseEther("0.30")  // 30% to development
  },
  {
    beneficiary: "0xCommunityRewards...",
    shares: parseEther("0.15")  // 15% to community
  }
];

// Sort beneficiaries (required)
const sortedBeneficiaries = factory.sortBeneficiaries(beneficiaries);

// Create pool with streamable fees
const params = {
  // ... other parameters
  contracts: {
    tokenFactory: DOPPLER_V3_ADDRESSES[chainId].tokenFactory,
    governanceFactory: DOPPLER_V3_ADDRESSES[chainId].governanceFactory,
    v3Initializer: DOPPLER_V3_ADDRESSES[chainId].lockableV3Initializer, // Use lockable initializer
    liquidityMigrator: DOPPLER_V3_ADDRESSES[chainId].liquidityMigrator,
  },
  v3PoolConfig: {
    // ... standard config
    beneficiaries: sortedBeneficiaries // Add beneficiaries for streamable fees
  }
};

// Encode and create the pool
const { createParams } = factory.encode(params);
const txHash = await factory.create(createParams);
```

## Important Considerations

### No Migration Path

**Critical**: Pools created with beneficiaries will never migrate. This means:

* The pool remains on Uniswap V3 permanently
* Liquidity cannot be moved to V2 or V4
* The initial pool parameters cannot be changed
* Fee distribution continues indefinitely

### Fee Collection

Beneficiaries can collect their accumulated fees at any time by calling:

```typescript
const tx = await lockableV3Initializer.collectFees(poolAddress);
```

This will:

1. Collect all accumulated fees from the pool's liquidity positions
2. Distribute fees to each beneficiary according to their shares
3. Emit `Collect` events for each beneficiary

## Best Practices

1. **Plan Beneficiaries Carefully**: Once set, beneficiaries cannot be changed
2. **Test on Testnet**: Always test your beneficiary configuration on testnet first

## Migration from Standard V3 Pools

### Feature Comparison

| Feature                  | Standard V3 Pool                              | Streamable V3 Pool                           |
| ------------------------ | --------------------------------------------- | -------------------------------------------- |
| **Initializer Contract** | `UniswapV3Initializer`                        | `LockableUniswapV3Initializer`               |
| **Fee Distribution**     | Accumulated in pool until migration           | Continuously claimable by beneficiaries      |
| **Pool Migration**       | Graduates to V2 or V4 after conditions met    | Never migrates (permanent V3)                |
| **Beneficiaries**        | Not supported                                 | Multiple beneficiaries with custom shares    |
| **Protocol Fee**         | Collected on migration                        | 5% streamed to protocol continuously         |
| **Liquidity Management** | Full range + migration planning               | Full range permanently                       |
| **Use Case**             | Standard token launches with future migration | Projects needing continuous fee distribution |

### When to Use Each Approach

**Use Standard V3 Pools When:**

* You want the flexibility to migrate to Uniswap V2 or V4 later
* You prefer consolidated liquidity in a single graduated pool
* You don't need immediate fee distribution
* You want to follow the traditional Doppler launch pattern

**Use Streamable V3 Pools When:**

* You need continuous revenue distribution to multiple parties
* You want a "set and forget" pool structure
* You have stakeholders who need regular fee access
* You prefer simplicity over migration flexibility

### Key Implementation Differences

The main differences when implementing streamable V3 pools:

1. **Initializer Address**: Use `lockableV3Initializer` instead of `v3Initializer`
2. **Beneficiaries**: Must be configured with shares summing to exactly 100%
3. **Liquidity Migrator**: Set to `zeroAddress` (migration is disabled)
4. **Unlock Time**: Ignored by the contract (pools never unlock)

### Example: Converting Configuration

**Standard V3 Configuration:**

```typescript
const standardConfig = {
  contracts: {
    v3Initializer: addresses.v3Initializer,
    liquidityMigrator: addresses.v2Migrator, // Or v4Migrator
  },
  v3PoolConfig: {
    feeTier: 10000,
    initialEthLiquidity: parseEther("2"),
    unlockTime: futureTimestamp,
    shouldLaunch: true
  }
};
```

**Streamable V3 Configuration:**

```typescript
const streamableConfig = {
  contracts: {
    v3Initializer: addresses.lockableV3Initializer, // Changed
    liquidityMigrator: zeroAddress, // Changed - no migration
  },
  v3PoolConfig: {
    feeTier: 10000,
    initialEthLiquidity: parseEther("2"),
    shouldLaunch: true,
    beneficiaries: sortedBeneficiaries // Added - enables fee streaming
    // unlockTime removed (ignored by contract)
  }
};
```

### Migration Checklist

If you're moving from standard V3 to streamable V3 for new deployments:

* [ ] Update initializer address to `lockableV3Initializer`
* [ ] Define beneficiaries with shares summing to exactly 100%
* [ ] Include 5% protocol fee to Airlock owner
* [ ] Sort beneficiaries using `factory.sortBeneficiaries()`
* [ ] Set liquidityMigrator to `zeroAddress`
* [ ] Remove unlock time planning (parameter is ignored)
* [ ] Update fee collection processes to use `collectFees()`
* [ ] Communicate permanent pool structure to stakeholders

## FAQ

**Q: Can I change beneficiaries after pool creation?** A: No, beneficiaries are immutable once the pool is created.

**Q: Can I create a streamable pool without the 5% protocol fee?** A: No, the contract enforces that the Airlock owner receives exactly 5% of fees.

**Q: Can I convert an existing standard V3 pool to streamable?** A: No, pool types are determined at creation. You would need to create a new token.

**Q: What happens to tokens already launched with standard V3?** A: They continue to work as designed and will migrate according to their configuration.

**Q: Can I use streamable V3 with V4 migration for fee streaming?** A: No, streamable V3 pools never migrate. If you want V4 with fee streaming, use standard V3 + V4 migrator.


# v4


# Overview

Overview of the Doppler V4 SDK

## Doppler V4 SDK Overview

### What is the Doppler V4 SDK?

The Doppler V4 SDK is a next-generation TypeScript library that provides developers with advanced tools to interact with the Doppler protocol's V4 implementation. Built on the foundation of Uniswap V4's hook architecture, it enables sophisticated token creation with dynamic bonding curves, advanced pool management, and comprehensive analytics capabilities across multiple EVM-compatible networks.

### What is Drift?

The Doppler V4 SDK is built on top of [Drift](https://delvtech.github.io/drift/), a modern blockchain interaction framework that simplifies Web3 development by:

* Providing unified interfaces for different wallet providers and RPC endpoints
* Handling transaction lifecycle management with automatic retry logic
* Offering type-safe contract interaction patterns
* Supporting both read and write operations with consistent error handling

### Core Concepts

#### Supported Networks

The V4 SDK supports multiple production and testing networks:

| Network              | Chain ID | Environment | Purpose                 |
| -------------------- | -------- | ----------- | ----------------------- |
| **Base Mainnet**     | `8453`   | Production  | Live token deployments  |
| **Unichain Mainnet** | `130`    | Production  | Live token deployments  |
| **Ink**              | `57073`  | Production  | Live token deployments  |
| **Base Sepolia**     | `84532`  | Testnet     | Development and testing |

Network configurations are managed through `DOPPLER_V4_ADDRESSES`, providing seamless network switching and automatic contract address resolution.

#### Factory Architecture

The V4 SDK provides specialized factory classes optimized for advanced operations:

| Class              | Purpose                                   | Key Features                             |
| ------------------ | ----------------------------------------- | ---------------------------------------- |
| `ReadFactory`      | Query protocol state and analytics data   | Asset data, module states, events        |
| `ReadWriteFactory` | Deploy tokens with hooks and manage pools | Hook mining, config building, deployment |

#### Advanced Asset Lifecycle

The V4 SDK manages sophisticated asset lifecycles with enhanced capabilities:

1. **Token Creation**: Deploy DERC20 tokens with custom hooks and dynamic bonding curves
2. **Price Discovery**: Advanced price discovery with hook-based customization
3. **Trading**: Enhanced trading with V4 features and custom logic

### Key Features

#### Dynamic Bonding Curves

Revolutionary approach to token pricing and liquidity:

* **Custom Hook Integration**: Deploy tokens with specialized hooks that implement dynamic bonding curves
* **Adaptive Pricing**: Hooks can adjust pricing based on market conditions, trading volume, and other parameters
* **Flexible Logic**: Support for complex mathematical models and custom trading algorithms

#### Advanced Configuration System

Comprehensive configuration management for complex deployments:

* [**Pre-deployment Configuration**](#doppler-predeployment-configuration-parameters): `DopplerPreDeploymentConfig` for complete setup specification
* **Hook Mining**: Automatic discovery of optimal hook addresses with required flags
* **Parameter Validation**: Advanced validation ensuring configuration consistency and mathematical correctness
* **Gas Optimization**: Intelligent gas estimation and optimization for complex transactions

#### Doppler Lens Integration

Powerful analytics and data querying system through the DopplerLensQuoter:

* **Pool State Queries**: Real-time pool price, tick, and liquidity information
* **Virtual Position Data**: Access to combined liquidity across all position types
* **Simulation-Based**: Gas-free state queries using revert-based simulation
* **Price Discovery Support**: Essential for monitoring active Dutch auctions

### Contract Integration

#### Core V4 Contracts

The SDK integrates with advanced V4 contract architecture:

| Contract Type       | Purpose                             | V4 Enhancements                           |
| ------------------- | ----------------------------------- | ----------------------------------------- |
| `Airlock`           | Advanced factory for token creation | Hook support, complex configurations      |
| `TokenFactory`      | V4DERC20 token deployment           | Hook integration, dynamic features        |
| `GovernanceFactory` | Enhanced governance systems         | Advanced voting mechanisms                |
| `PoolInitializer`   | V4 pool initialization with hooks   | Hook mining, custom logic setup           |
| `DopplerLensQuoter` | Advanced data querying system       | Pool state queries, virtual position data |
| `UniswapV4Pool`     | Hook-enabled trading pools          | Custom trading logic, dynamic fees        |

#### Hook System

Revolutionary hook-based architecture for custom trading logic:

* **Hook Flags**: Configure which lifecycle events trigger custom logic
* **Address Mining**: Automatic discovery of valid hook addresses with required properties
* **Custom Logic**: Support for complex trading algorithms, dynamic fees, and custom behaviors
* **Validation**: Comprehensive validation of hook configurations and compatibility

### Advanced Capabilities

#### Configuration Building

Sophisticated configuration management system:

```typescript
// Advanced configuration building
const preDeploymentConfig: DopplerPreDeploymentConfig = { ... };
const { createParams, hook, token } = factory.buildConfig(
  preDeploymentConfig, 
  addresses
);
```

Key capabilities include:

* **Parameter Optimization**: Automatic optimization of configuration parameters
* **Compatibility Checking**: Validation of parameter combinations and constraints
* **Hook Integration**: Seamless integration of custom hooks with token logic
* **Gas Estimation**: Accurate gas estimation for complex deployments

#### Doppler Predeployment Configuration Parameters

| Key                       | Type                                                    | Purpose                                                            |
| ------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------ |
| `name`                    | string                                                  | Name of the token being deployed                                   |
| `symbol`                  | string                                                  | Symbol representing the token                                      |
| `totalSupply`             | bigint                                                  | Total supply of tokens available                                   |
| `numTokensToSell`         | bigint                                                  | Amount of tokens to sell                                           |
| `tokenURI`                | string                                                  | URI pointing to the token's metadata                               |
| `blockTimestamp`          | number                                                  | Timestamp of the block when the configuration is set               |
| `startTimeOffset`         | number                                                  | Offset in **days** from the current time to the start of the sale  |
| `duration`                | number                                                  | Duration of the sale in **days**                                   |
| `epochLength`             | number                                                  | Length of each epoch in **seconds**                                |
| `numeraire`               | Address **(optional)**                                  | Address of the numéraire token, defaults to native if not provided |
| `tickRange` (TickRange)   | { startTick: number, endTick: number } **(optional)**   | Range of ticks for price adjustments                               |
| `priceRange` (PriceRange) | { startPrice: number, endPrice: number } **(optional)** | Range of prices for the token                                      |
| `tickSpacing`             | number                                                  | Spacing between ticks for price adjustments                        |
| `gamma`                   | number **(optional)**                                   | Gamma value for price calculations                                 |
| `fee`                     | number                                                  | Transaction fee in basis points (bips)                             |
| `minProceeds`             | bigint                                                  | Minimum range for auction target                                   |
| `maxProceeds`             | bigint                                                  | Maximum range for auction target                                   |
| `numPdSlugs`              | number **(optional)**                                   | Number of price discovery slugs                                    |
| `yearlyMintRate`          | bigint                                                  | Rate at which tokens are minted annually                           |
| `vestingDuration`         | bigint                                                  | Duration of the vesting period in seconds                          |
| `recipients`              | Address\[]                                              | List of addresses receiving vested tokens                          |
| `amounts`                 | bigint\[]                                               | Amounts of tokens allocated to each recipient                      |
| `liquidityMigratorData`   | Hex **(optional)**                                      | Encoded data for liquidity migration                               |
| `integrator`              | Address                                                 | Address of the integrator managing the deployment                  |

#### Data Analytics

Comprehensive analytics through the DopplerLensQuoter system:

* **Pool State**: Real-time price, tick, and total liquidity information
* **Virtual Positions**: Combined token amounts across all position types
* **Simulation Queries**: Gas-free state access using revert-based simulation
* **Price Discovery**: Real-time monitoring of Dutch auction states

#### Error Handling & Validation

Advanced error handling for complex operations:

* **Configuration Validation**: Comprehensive validation of complex parameter sets
* **Hook Validation**: Verification of hook logic and compatibility
* **Network Resilience**: Robust handling of network issues and transaction failures
* **Gas Management**: Intelligent gas estimation and optimization

## Next steps

For detailed implementation guidance, explore the [Getting Started Guide](/reference/legacy-sdks-and-migration-guides/v4/getting-started), [Factory Reference](/reference/legacy-sdks-and-migration-guides/v4/factory), and [Doppler Lens Documentation](/reference/legacy-sdks-and-migration-guides/v4/lens) for comprehensive API coverage.


# Get Started

Getting Started with Doppler V4 SDK

This section guides you through setting up and using the Doppler V4 SDK to interact with the latest version of the Doppler protocol.

## Prerequisites

* **Node.js**: Version `18.14` or higher
* **npm** or **yarn**: Package manager for installing dependencies
* **Web3 Provider**: Access to Ethereum RPC endpoints (Infura, Alchemy, etc.)
* **Wallet**: MetaMask or similar wallet for transaction signing

## Installation

Install the Doppler V4 SDK, Viem, and Drift packages:

```bash
npm install doppler-v4-sdk viem @delvtech/drift @delvtech/drift-viem
# or
yarn add doppler-v4-sdk viem @delvtech/drift @delvtech/drift-viem
```

The SDK uses [Drift](https://github.com/delvtech/drift) for blockchain interactions.

## Required Environment Variables

```bash
# RPC Endpoint
RPC_URL="https://sepolia.base.org"

# Wallet Private Key (for automated transactions)
PRIVATE_KEY="your-private-key"

# Network Configuration
CHAIN_ID=84532
```

## Basic Setup

### 1. Import the SDK

```typescript
import { 
  ReadFactory, 
  ReadWriteFactory, 
  Lens,
  DOPPLER_V4_ADDRESSES 
} from 'doppler-v4-sdk';
```

### 2. Initialize Drift Client

Set up your Drift client with both read and write capabilities:

#### Read-only operations

```typescript
import { Drift } from 'drift';

// For read-only operations
const drift = new Drift({
  rpcUrl: 'https://sepolia.base.org',
  chainId: 84532 // Base Sepolia
});
```

#### Read-write operations

```typescript
import { createDrift } from "@delvtech/drift";
import { createPublicClient, createWalletClient, http, PublicClient } from "viem";

const publicClient = createPublicClient({
  chain: baseSepolia,
  transport: http('https://sepolia.base.org'),
});

const walletClient = createWalletClient({
  chain: baseSepolia,
  transport: http('https://sepolia.base.org'),
  account: privateKeyToAccount(WALLET_PRIVATE_KEY),
});

// For read-write operations (with wallet)
const driftWithWallet = createDrift({
  adapter: viemAdapter({ publicClient, walletClient }),
});
```

### 3. Get Protocol Addresses

```typescript
// Get addresses for the current network
const chainId = 84532; // Base Sepolia (change to 8453 for Base Mainnet, 130 for Unichain Mainnet, etc.)
const addresses = DOPPLER_V4_ADDRESSES[chainId];
const airlockAddress = addresses.airlock;
```

## Core Concepts

### Factory Classes

The V4 SDK provides two main factory classes:

* **`ReadFactory`**: For querying protocol state and reading data
* **`ReadWriteFactory`**: For creating tokens with hooks

### Key V4 Features

* **Dynamic bonding curves**: Custom hooks supporting dynamic bonding curves

### Asset Lifecycle

1. **Token Creation**: Deploy new tokens with custom hooks
2. **Price Discovery**: Initial liquidity provision and price discovery phase
3. **Trading**: Enhanced trading with V4 features

## Quick Start Examples

### Reading Protocol Data

```typescript
// Create a read factory instance
const factory = new ReadFactory(
  airlockAddress,
  drift,
);

// Get information about a deployed asset
const assetData = await factory.getAssetData(tokenAddress);
console.log('Asset details:', {
  numeraire: assetData.numeraire,
  governance: assetData.governance,
  pool: assetData.pool,
  migrationPool: assetData.migrationPool,
  totalSupply: assetData.totalSupply.toString()
});

// Check module state
const moduleState = await factory.getModuleState(moduleAddress);
console.log('Module state:', moduleState);
```

### Creating a New Token with Hooks

```typescript
const addresses = DOPPLER_V4_ADDRESSES[chainId];
const airlockAddress = addresses.airlock;
const bundlerAddress = addresses.bundler;
const tokenURI = "https://example.com/token-metadata.json";
const integrator: Address = "0x...";

// Create a read-write factory instance
const factory = new ReadWriteFactory(
  airlockAddress,
  bundlerAddress,
  driftWithWallet
);

// Define pre-deployment configuration
const preDeploymentConfig: DopplerPreDeploymentConfig = {
  name: tokenName,
  symbol: tokenSymbol,
  totalSupply: parseEther('1_000_000_000'),
  numTokensToSell: parseEther('600_000_000'),
  tokenURI,
  blockTimestamp: Math.floor(Date.now() / 1000),
  startTimeOffset: 1,
  duration: 1 / 4,
  epochLength: 200,
  gamma: 800,
  tickRange: {
    startTick: 174_312,
    endTick: 186_840,
  },
  tickSpacing: 2,
  fee: 20_000, // 2%
  minProceeds: parseEther('2'),
  maxProceeds: parseEther('4'),
  yearlyMintRate: 0n,
  vestingDuration: BigInt(24 * 60 * 60 * 365), // Seconds in a year
  recipients: [wallet.account.address],
  amounts: [parseEther('50_000_000')],
  numPdSlugs: 15,
  integrator,
};

// Build the complete configuration
const { createParams, hook, token } = factory.buildConfig(
  preDeploymentConfig, 
  addresses
);

// Simulate the creation transaction
const simulation = await factory.simulateCreate(createParams);
console.log('Gas estimate:', simulation.gasEstimate);

// Execute the creation transaction
const txHash = await factory.create(createParams);
console.log('Transaction hash:', txHash);

// Wait for transaction confirmation and get the deployed asset address
const receipt = await drift.waitForTransactionReceipt({ hash: txHash });
const createEvent = receipt.logs.find(log => 
  log.topics[0] === '0x...' // Create event signature
);
const deployedTokenAddress = `0x${createEvent.topics[1].slice(26)}`;
console.log('Deployed token address:', deployedTokenAddress);
```

### Using the Doppler Lens for Advanced Queries

After creating your token, you can use the DopplerLensQuoter to get detailed information about your pool:

```typescript
// Get the asset data to find the pool information
const assetData = await factory.getAssetData(deployedTokenAddress);

// Create a doppler lens instance
const lens = new ReadDopplerLens(addresses.dopplerLensQuoter, drift);

// Define pool key for the created token
const poolKey = {
  currency0: assetData.numeraire,
  currency1: deployedTokenAddress,
  fee: 20_000, // 2% fee tier
  tickSpacing: 2,
  hooks: assetData.hook // Hook address from asset data
};

// Get comprehensive pool state data
const poolData = await lens.quoteDopplerLensData({
  poolKey,
  zeroForOne: true,
  exactAmount: 1n, // Minimal amount for state query
  hookData: "0x"
});

console.log('Pool state:', {
  sqrtPriceX96: poolData.sqrtPriceX96.toString(),
  tick: poolData.tick,
  totalToken0: poolData.amount0.toString(),
  totalToken1: poolData.amount1.toString()
});
```

### Network Support

The V4 SDK supports multiple networks:

* **Base Sepolia** (chainId: 84532) - Testnet
* **Base Mainnet** (chainId: 8453) - Production
* **Unichain Mainnet** (chainId: 130) - Production
* **Unichain Sepolia** (chainId: 1301) - Testnet
* **Ink** (chainId: 57073) - Production

For complete network addresses and additional supported networks, see the [Contract Addresses](/reference/contract-addresses) documentation.

You can get free Base Sepolia ETH from the [Base Sepolia faucet](https://docs.base.org/tools/network-faucets) to test your applications.

## Error Handling

The SDK provides comprehensive error handling for V4-specific scenarios:

```typescript
try {
  const { createParams } = factory.buildConfig(config, addresses);
} catch (error) {
  if (error.message.includes('Invalid tick range')) {
    console.log('Tick range is invalid for the specified fee tier');
  } else if (error.message.includes('Hook mining failed')) {
    console.log('Could not find suitable hook address');
  } else {
    console.error('Unexpected error:', error);
  }
}
```

## Next Steps

* Explore the [Factory Reference](/reference/legacy-sdks-and-migration-guides/v4/factory) for detailed API documentation
* Learn about [Doppler Lens Usage](/reference/legacy-sdks-and-migration-guides/v4/lens) for advanced data querying
* Understand [Quoter Usage](/reference/legacy-sdks-and-migration-guides/v4/quoter) for price calculations
* Review [Token Launch Examples](/reference/legacy-sdks-and-migration-guides/v4/examples) for comprehensive deployment scenarios

## Support

For additional help and examples:

* Check the [Token Launch Examples](/reference/legacy-sdks-and-migration-guides/v4/examples) for comprehensive deployment scenarios
* Review the [Implementation Guide](broken://pages/2xkvOvHSEt9zfaWDOv5z) for protocol details
* Join the community for discussions and support


# Examples

This guide provides examples for using Doppler V4 with Custom Fees and various optional govenance

> **Note on startTimeOffset**: The `startTimeOffset` parameter is included in the type definitions but is not currently used by the SDK implementation. All pools will start 30 seconds after the transaction is confirmed. This will be addressed in a future update.

## Prerequisites

```typescript
import { 
  ReadWriteFactory,
  BeneficiaryData,
  V4MigratorData,
  DEAD_ADDRESS,
  DOPPLER_V4_ADDRESSES
} from 'doppler-v4-sdk';
import { createPublicClient, createWalletClient, http, parseEther } from 'viem';
import { base } from 'viem/chains';
import { Drift } from '@delvtech/drift';
import { viemAdapter } from '@delvtech/drift-viem';

// Setup clients
const publicClient = createPublicClient({
  chain: base,
  transport: http()
});

const walletClient = createWalletClient({
  chain: base,
  transport: http(),
  account: privateKeyToAccount('0x...') // Your private key
});

// Setup Drift
import { createDrift } from '@delvtech/drift';

const drift = createDrift({ adapter: viemAdapter({ publicClient, walletClient }) });

// Get addresses for your chain
const addresses = DOPPLER_V4_ADDRESSES[base.id];

// Initialize factory
const factory = new ReadWriteFactory(addresses.airlock, drift);
```

## Example 1: Standard Token Launch with Governance

This example launches a token with standard governance, where 90% of liquidity goes to the timelock and 10% to the StreamableFeesLocker.

```typescript
async function launchTokenWithGovernance() {
  // 1. Define your token parameters
  const tokenName = "Community Token";
  const tokenSymbol = "COMM";
  const totalSupply = parseEther("1000000"); // 1M tokens
  const numTokensToSell = parseEther("700000"); // 700k for sale
  
  // 2. Set up beneficiaries for the 10% locked liquidity
  const beneficiaries: BeneficiaryData[] = [
    {
      beneficiary: addresses.airlock, // Protocol: 5%
      shares: BigInt(0.05e18), // 5% in WAD (1e18 = 100%)
    },
    {
      beneficiary: '0x123...', // REPLACE with your integrator address: 5%
      shares: BigInt(0.05e18), // 5% in WAD (1e18 = 100%)
    },
    {
      beneficiary: '0x456...', // REPLACE with team/treasury address: 90%
      shares: BigInt(0.9e18), // 90% in WAD (1e18 = 100%)
    }
  ];
  
  // 3. Sort beneficiaries (required by contract)
  const sortedBeneficiaries = factory.sortBeneficiaries(beneficiaries);
  
  // 4. Configure V4 migrator
  const v4Config: V4MigratorData = {
    fee: 3000, // 0.3% pool fee
    tickSpacing: 60, // Standard for 0.3% pools
    lockDuration: 180 * 24 * 60 * 60, // 180 days lock
    beneficiaries: sortedBeneficiaries
  };
  
  // 5. Encode migrator data
  const liquidityMigratorData = factory.encodeV4MigratorData(v4Config);
  
  // 6. Configure vesting (optional)
  const vestingRecipients = [
    '0x789...', // Team member 1
    '0xabc...', // Team member 2
  ];
  const vestingAmounts = [
    parseEther("50000"), // 50k tokens
    parseEther("50000"), // 50k tokens
  ];
  
  // 7. Build complete configuration
  const { createParams, hook, token } = await factory.buildConfig({
    // Token details
    name: tokenName,
    symbol: tokenSymbol,
    totalSupply: totalSupply,
    numTokensToSell: numTokensToSell,
    tokenURI: "https://api.example.com/token/metadata",
    
    // Timing
    blockTimestamp: Math.floor(Date.now() / 1000),
    startTimeOffset: 1, // Start in 1 day (NOTE: Currently not used by SDK - uses fixed 30 second offset)
    duration: 30, // 30 day sale
    epochLength: 3600, // 1 hour epochs
    
    // Price configuration
    priceRange: { 
      startPrice: 0.0001, // Starting price in ETH
      endPrice: 0.01     // Maximum price in ETH
    },
    tickSpacing: 60,
    fee: 3000, // 0.3%
    
    // Sale parameters
    minProceeds: parseEther("10"), // Minimum 10 ETH
    maxProceeds: parseEther("1000"), // Maximum 1000 ETH
    
    // Vesting
    yearlyMintRate: parseEther("100000"), // 100k tokens/year inflation
    vestingDuration: BigInt(4 * 365 * 24 * 60 * 60), // 4 years
    recipients: vestingRecipients,
    amounts: vestingAmounts,
    
    // Migration
    liquidityMigratorData: liquidityMigratorData,
    
    // Integrator
    integrator: '0x123...', // REPLACE with your actual integrator address to receive fees
  }, addresses, {
    useGovernance: true // Standard governance (default)
  });
  
  // 8. Log deployment details
  console.log("Token will be deployed at:", token);
  console.log("Hook will be deployed at:", hook);
  console.log("Sale will start at:", new Date((Math.floor(Date.now() / 1000) + 86400) * 1000));
  
  // 9. Create the pool
  const tx = await factory.create(createParams);
  console.log("Transaction hash:", tx);
  
  // 10. Wait for confirmation
  const receipt = await publicClient.waitForTransactionReceipt({ hash: tx });
  console.log("Pool created in block:", receipt.blockNumber);
  
  return { token, hook, tx };
}
```

## Example 2: No-Op Governance Launch (100% Locked Liquidity)

This example launches a token with no-op governance, where 100% of liquidity is permanently locked in the StreamableFeesLocker.

```typescript
async function launchTokenNoOpGovernance() {
  // 1. Define token parameters (similar to above)
  const tokenName = "Perpetual Fee Token";
  const tokenSymbol = "PFT";
  const totalSupply = parseEther("10000000"); // 10M tokens
  const numTokensToSell = parseEther("5000000"); // 5M for sale
  
  // 2. Set up beneficiaries for 100% of liquidity
  // These beneficiaries will receive fees forever
  const beneficiaries: BeneficiaryData[] = [
    {
      beneficiary: '0xDAO...', // DAO Treasury: 40%
      shares: BigInt(0.4e18),
    },
    {
      beneficiary: '0xDEV...', // Development Fund: 30%
      shares: BigInt(0.3e18),
    },
    {
      beneficiary: '0xCOM...', // Community Rewards: 20%
      shares: BigInt(0.2e18),
    },
    {
      beneficiary: addresses.airlock, // Protocol: 10%
      shares: BigInt(0.1e18),
    }
  ];
  
  // 3. Sort beneficiaries
  const sortedBeneficiaries = factory.sortBeneficiaries(beneficiaries);
  
  // 4. Configure V4 migrator for no-op governance
  const v4Config: V4MigratorData = {
    fee: 10000, // 1% pool fee (higher for more fees)
    tickSpacing: 200, // Wider spacing for 1% pool
    lockDuration: 0, // Duration is ignored for no-op governance (permanent lock)
    beneficiaries: sortedBeneficiaries
  };
  
  // 5. Encode migrator data
  const liquidityMigratorData = factory.encodeV4MigratorData(v4Config);
  
  // 6. Build configuration with no-op governance
  const { createParams, hook, token } = await factory.buildConfig({
    // Token details
    name: tokenName,
    symbol: tokenSymbol,
    totalSupply: totalSupply,
    numTokensToSell: numTokensToSell,
    tokenURI: "ipfs://QmXxx...", // IPFS metadata
    
    // Timing
    blockTimestamp: Math.floor(Date.now() / 1000),
    startTimeOffset: 0.5, // Start in 12 hours (NOTE: Currently not used by SDK - uses fixed 30 second offset)
    duration: 14, // 14 day sale
    epochLength: 1800, // 30 minute epochs
    
    // Price configuration with ETH as quote
    priceRange: { 
      startPrice: 0.00001, // Lower starting price
      endPrice: 0.001      // Lower max price
    },
    tickSpacing: 200,
    fee: 10000, // 1%
    
    // Sale parameters
    minProceeds: parseEther("50"), // Minimum 50 ETH
    maxProceeds: parseEther("500"), // Maximum 500 ETH
    
    // No vesting for no-op governance
    yearlyMintRate: BigInt(0),
    vestingDuration: BigInt(0),
    recipients: [],
    amounts: [],
    
    // Migration
    liquidityMigratorData: liquidityMigratorData,
    
    // Integrator
    integrator: '0x123...', // REPLACE with your actual integrator address to receive fees
  }, addresses, {
    useGovernance: false // No-op governance - CRITICAL!
  });
  
  // The migration will automatically set recipient to DEAD_ADDRESS
  console.log("No-op governance: liquidity will be locked forever");
  console.log("Beneficiaries will receive fees in perpetuity");
  
  // 7. Create the pool
  const tx = await factory.create(createParams);
  console.log("Transaction hash:", tx);
  
  return { token, hook, tx };
}
```

## Example 3: Custom Quote Token Launch

This example shows launching with a custom quote token (not ETH).

```typescript
async function launchTokenCustomQuote() {
  const usdcAddress = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; // USDC on mainnet
  
  // 1. Set up beneficiaries
  const beneficiaries: BeneficiaryData[] = [
    {
      beneficiary: addresses.airlock,
      shares: BigInt(0.1e18), // 10% to protocol
    },
    {
      beneficiary: '0xTREASURY...',
      shares: BigInt(0.9e18), // 90% to treasury
    }
  ];
  
  const sortedBeneficiaries = factory.sortBeneficiaries(beneficiaries);
  
  // 2. Configure V4 migrator
  const v4Config: V4MigratorData = {
    fee: 500, // 0.05% for stable pairs
    tickSpacing: 10, // Tight spacing for stables
    lockDuration: 90 * 24 * 60 * 60, // 90 days
    beneficiaries: sortedBeneficiaries
  };
  
  const liquidityMigratorData = factory.encodeV4MigratorData(v4Config);
  
  // 3. Build configuration with USDC as quote
  const { createParams, hook, token } = await factory.buildConfig({
    name: "Stable Token",
    symbol: "STBL",
    totalSupply: parseEther("100000000"), // 100M tokens
    numTokensToSell: parseEther("50000000"), // 50M for sale
    tokenURI: "",
    
    blockTimestamp: Math.floor(Date.now() / 1000),
    startTimeOffset: 2, // Start in 2 days (NOTE: Currently not used by SDK - uses fixed 30 second offset)
    duration: 7, // 7 day sale
    epochLength: 7200, // 2 hour epochs
    
    // IMPORTANT: Use numeraire for custom quote token
    numeraire: usdcAddress,
    
    // Price in USDC (6 decimals)
    priceRange: { 
      startPrice: 0.1,  // $0.10
      endPrice: 1.0     // $1.00
    },
    tickSpacing: 10,
    fee: 500,
    
    // Proceeds in USDC (6 decimals)
    minProceeds: BigInt(100000 * 1e6), // 100k USDC
    maxProceeds: BigInt(10000000 * 1e6), // 10M USDC
    
    yearlyMintRate: BigInt(0),
    vestingDuration: BigInt(0),
    recipients: [],
    amounts: [],
    
    liquidityMigratorData: liquidityMigratorData,
    integrator: '0x123...',
  }, addresses);
  
  console.log("Token paired with USDC:", usdcAddress);
  
  const tx = await factory.create(createParams);
  return { token, hook, tx };
}
```

## Example 4: Multicurve Token Launch

This example shows launching with [Doppler Multicurve](https://doppler.lol/multicurve.pdf).

<pre class="language-typescript"><code class="lang-typescript"><strong>async function multicurveLaunch() {
</strong>  const chainId = 84532 // Base Sepolia
  const addresses = DOPPLER_V4_ADDRESSES[chainId]

  // For building parameters, a default drift instance is sufficient.
  const drift = createDrift()
  const factory = new ReadWriteFactory(addresses.airlock, drift as any)

  const config = {
    name: 'My Multicurve Token',
    symbol: 'MMT',
    totalSupply: parseEther('1000000'),
    numTokensToSell: parseEther('600000'),
    tokenURI: 'ipfs://example/token.json',
    // Use WETH as numeraire on Base Sepolia
    numeraire: '0x4200000000000000000000000000000000000006' as Address,
    pool: {
      // Example: two evenly weighted curves aligned to tickSpacing
      fee: 3000,         // 0.3%
      tickSpacing: 60,   // ensure ranges are multiples of this
      curves: [
        // width = 600 ticks; divisible by tickSpacing (60) and numPositions (10)
        { tickLower: 174_300, tickUpper: 174_900, numPositions: 10, shares: parseEther('0.5') },
        { tickLower: 174_900, tickUpper: 175_500, numPositions: 10, shares: parseEther('0.5') },
      ],
      // Optional beneficiaries for lockable fees
      lockableBeneficiaries: [],
    },
    integrator: zeroAddress as Address,
  }

  const { createParams } = factory.buildMulticurveCreateParams(config as any, addresses, { useGovernance: true })
  console.log('CreateParams prepared:')
  console.log({
    initialSupply: createParams.initialSupply.toString(),
    numTokensToSell: createParams.numTokensToSell.toString(),
    numeraire: createParams.numeraire,
    poolInitializer: createParams.poolInitializer,
  })
}
</code></pre>

## Post-Launch Operations

After launching, you can interact with the StreamableFeesLocker:

```typescript
async function distributeAndClaimFees(tokenId: bigint) {
  // 1. Anyone can distribute fees
  const distributeTx = await walletClient.writeContract({
    address: addresses.streamableFeesLocker,
    abi: streamableFeesLockerAbi,
    functionName: 'distributeFees',
    args: [tokenId]
  });
  
  console.log("Fees distributed:", distributeTx);
  
  // 2. Check claimable balance for a beneficiary
  const claimable = await publicClient.readContract({
    address: addresses.streamableFeesLocker,
    abi: streamableFeesLockerAbi,
    functionName: 'beneficiariesClaims',
    args: [beneficiaryAddress, currencyAddress]
  });
  
  console.log("Claimable amount:", claimable);
  
  // 3. Beneficiary claims fees
  const claimTx = await walletClient.writeContract({
    address: addresses.streamableFeesLocker,
    abi: streamableFeesLockerAbi,
    functionName: 'releaseFees',
    args: [tokenId]
  });
  
  console.log("Fees claimed:", claimTx);
}
```

## Important Notes

1. **Governance Choice**:
   * `useGovernance: true` (default) = 90% to timelock, 10% to locker (this split is automatic and handled by the V4Migrator contract)
   * `useGovernance: false` = 100% to locker, permanent lock
2. **Beneficiary Requirements**:
   * Must be sorted by address (use `sortBeneficiaries()`)
   * Shares must sum to exactly 1e18
   * Cannot have duplicate addresses
3. **Price Ranges**:
   * For ETH pairs: prices in ETH (18 decimals)
   * For custom pairs: prices in quote token decimals
4. **Testing Recommendations**:
   * Test on testnet first (Base Sepolia, Unichain Sepolia, etc.)
   * Verify beneficiary addresses
   * Double-check share calculations
   * Ensure sufficient quote token liquidity exists


# Factory

Factory Class Reference

The Doppler V4 SDK provides two factory classes for interacting with the airlock contract:

* **`ReadFactory`** - Read-only operations for querying deployed pools and modules
* **`ReadWriteFactory`** - Extends ReadFactory with deployment and migration capabilities

## ReadFactory

The `ReadFactory` class provides read-only operations for the Doppler V4 airlock contract. It handles queries and data retrieval from deployed Doppler pools and their associated contracts.

### Constructor

```typescript
new ReadFactory(address: Address, drift: Drift<ReadAdapter>)
```

**Parameters:**

* `address` - The address of the airlock contract
* `drift` - Drift instance with read adapter (creates default if not provided)

### Methods

#### getModuleState

Retrieves the current state/type of a module in the Doppler system. Modules serve different roles and must be whitelisted before use.

```typescript
async getModuleState(module: Address): Promise<ModuleState>
```

**Parameters:**

* `module` - The address of the module to check

**Returns:** Promise resolving to the module's current state

**Module States:**

* `NotWhitelisted` (0) - Module is not approved for use
* `TokenFactory` (1) - Module can create tokens
* `GovernanceFactory` (2) - Module can create governance contracts
* `HookFactory` (3) - Module can create hooks
* `Migrator` (4) - Module can migrate liquidity

#### getAssetData

Retrieves comprehensive deployment data for a Doppler asset, including all contract addresses and configuration.

```typescript
async getAssetData(asset: Address): Promise<AssetData>
```

**Parameters:**

* `asset` - The address of the deployed asset token

**Returns:** Promise resolving to complete asset deployment data including:

* Numeraire (quote token) used for pricing
* Timelock and governance contracts
* Liquidity migrator for post-discovery trading
* Pool initializer and pool addresses
* Number of tokens being sold
* Integrator information

### ReadFactory Example

```typescript
import { ReadFactory, ModuleState } from "doppler-v4-sdk";

// Create factory instance
const factory = new ReadFactory(airlockAddress);

// Check module state
const state = await factory.getModuleState(moduleAddress);
if (state === ModuleState.TokenFactory) {
  console.log("Module is a valid token factory");
}

// Get asset information
const assetData = await factory.getAssetData(tokenAddress);
console.log("Asset details:", {
  numeraire: assetData.numeraire,
  governance: assetData.governance,
  pool: assetData.pool,
  migrationPool: assetData.migrationPool,
  totalSupply: assetData.totalSupply.toString(),
  tokensForSale: assetData.numTokensToSell.toString(),
});
```

## ReadWriteFactory

The `ReadWriteFactory` class extends `ReadFactory` with comprehensive deployment and migration capabilities for creating tokens with Doppler.

## Key Features

* **Pool Creation**: Deploy requisite doppler contracts, with tokens, hooks, and governance
* **Hook Mining**: Find optimal hook addresses with required flags
* **Asset Migration**: Move liquidity from price discovery to standard trading
* **Parameter Validation**: Automatic validation and optimization of deployment parameters
* **Gamma Calculation**: Compute optimal price movement parameters

## Constructor

```typescript
new ReadWriteFactory(address: Address, drift: Drift<ReadWriteAdapter>)
```

**Parameters:**

* `address` - The address of the airlock contract
* `drift` - A Drift instance with read-write adapter capabilities

## Core Methods

### buildConfig

Builds complete configuration for creating a new Doppler pool. This method validates parameters and tick ranges, optionally computes a valid gamma, mines hook addresses, and encodes factory data.

```typescript
buildConfig(
  params: DopplerPreDeploymentConfig,
  addresses: DopplerV4Addresses
): {
  createParams: CreateParams;
  hook: Hex;
  token: Hex;
}
```

**Parameters:**

* `params` - Pre-deployment configuration parameters ([DopplerPreDeploymentConfig](/reference/legacy-sdks-and-migration-guides/v4/overview#doppler-predeployment-configuration-parameters))
* `addresses` - Addresses of required Doppler V4 contracts

**Returns:** Object containing creation parameters, hook address, and token address

### create

Creates a new Doppler pool with token, hook, migrator, and governance. This is the main deployment method that sets up the complete ecosystem.

```typescript
async create(
  params: CreateParams,
  options?: TransactionOptions
): Promise<Hex>
```

**Parameters:**

* `params` - Complete creation parameters from `buildConfig()`
* `options` - Optional transaction options (gas, value, etc.)

**Returns:** Promise resolving to the transaction hash

### simulateCreate

Simulates a pool creation transaction without executing it. Useful for gas estimation, parameter validation, and testing configurations.

```typescript
async simulateCreate(
  params: CreateParams
): Promise<FunctionReturn<AirlockABI, 'create'>>
```

**Parameters:**

* `params` - Complete creation parameters from `buildConfig()`

**Returns:** Promise resolving to simulation results including gas estimates

### migrate

Migrates liquidity for an existing asset from the current pool to the migration pool. Triggers the migration process for assets that have completed price discovery.

```typescript
async migrate(
  asset: Address,
  options?: TransactionOptions
): Promise<Hex>
```

**Parameters:**

* `asset` - The address of the asset token to migrate
* `options` - Optional transaction options

**Returns:** Promise resolving to the transaction hash

## Example Usage

```typescript
import { ReadWriteFactory } from "doppler-v4-sdk";

// Create factory instance
const factory = new ReadWriteFactory(airlockAddress, drift);

// Build complete configuration
const { createParams, hook, token } = factory.buildConfig(
  {
    name: "MyToken",
    symbol: "MYT",
    totalSupply: parseEther("1000000000"), // 1b tokens
    numTokensToSell: parseEther("600000000"), // 600m tokens
    tokenURI: "some_ipfs_cid",
    blockTimestamp: Math.floor(Date.now() / 1000),
    startTimeOffset: 1,
    duration: 1 / 4, // 6 hour duration, must be divisible by epochLength
    epochLength: 200, // 200 seconds
    gamma: 800,
    tickRange: {
      startTick: 174_312,
      endTick: 186_840,
    },
    tickSpacing: 2,
    fee: 20_000, // 2%
    minProceeds: parseEther("2"),
    maxProceeds: parseEther("4"),
    yearlyMintRate: 0n,
    vestingDuration: BigInt(24 * 60 * 60 * 365),
    recipients: [someRecipientAddress],
    amounts: [parseEther("50000000")], // 5% of totalSupply
    numPdSlugs: 15,
    integrator: yourIntegratorAddress,
  },
  addresses
);

// Simulate creation to estimate gas
const simulation = await factory.simulateCreate(createParams);
console.log(`Estimated gas: ${simulation.request.gas}`);

// Create the pool
const txHash = await factory.create(createParams, {
  gasLimit: 5000000n,
});

// Later, migrate liquidity after price discovery ends
const migrationTx = await factory.migrate(token);
```

## Configuration Parameters

The [`DopplerPreDeploymentConfig`](/reference/legacy-sdks-and-migration-guides/v4/overview#doppler-predeployment-configuration-parameters) includes:

* **Token Details**: `name`, `symbol`, `totalSupply`, `tokenURI`
* **Sale Parameters**: `numTokensToSell`, `duration`, `epochLength`
* **Price Discovery**: `tickRange`, `tickSpacing`, `gamma`, `fee`
* **Proceeds**: `minProceeds`, `maxProceeds`
* **Governance**: `yearlyMintRate`, `vestingDuration`, `recipients`, `amounts`
* **Integration**: `integrator`, `liquidityMigratorData`

## Validation Rules

* Name and symbol are required and non-empty
* Total supply and tokens to sell must be positive
* Tick range must be valid (startTick < endTick)
* Duration and epoch length must be positive
* Tick spacing must be positive and divide gamma evenly
* Epoch length must divide total duration evenly

## Workflow Optimization

The factory automatically:

* Mines optimal hook addresses with required flags
* Validates parameter compatibility before deployment
* Provides gas estimation through simulation
* Optimizes gamma calculation for efficient price discovery


# Quoter

Quoter Class Reference

The `ReadQuoter` class provides a read-only interface to the Uniswap V4 Quoter contract for getting price quotes without executing transactions.

## Constructor

```typescript
new ReadQuoter(quoteV4Address: Address, drift: Drift<ReadAdapter>)
```

**Parameters:**

* `quoteV4Address` - Contract address of the V4 Quoter
* `drift` - Drift instance for blockchain interaction (defaults to new instance)

## Methods

### quoteExactInputV4

Get a price quote for swapping an exact amount of input tokens.

```typescript
async quoteExactInputV4(
  params: FunctionArgs<V4QuoterABI, 'quoteExactInputSingle'>['params']
): Promise<FunctionReturn<V4QuoterABI, 'quoteExactInputSingle'>>
```

**Parameters:**

* `params` - Arguments for the quoteExactInputSingle contract method

**Returns:** Promise resolving to raw contract return values

### quoteExactOutputV4

Get a price quote for receiving an exact amount of output tokens.

```typescript
async quoteExactOutputV4(
  params: FunctionArgs<V4QuoterABI, 'quoteExactOutputSingle'>['params']
): Promise<FunctionReturn<V4QuoterABI, 'quoteExactOutputSingle'>>
```

**Parameters:**

* `params` - Arguments for the quoteExactOutputSingle contract method

**Returns:** Promise resolving to raw contract return values

## Example Usage

```typescript
import { ReadQuoter } from "doppler-v4-sdk";

// Create quoter instance
const quoter = new ReadQuoter("0x...");

// Get quote for exact input
const inputQuote = await quoter.quoteExactInputV4({
  tokenIn: "0x...",
  tokenOut: "0x...",
  amountIn: 1000000n,
  fee: 3000,
  sqrtPriceLimitX96: 0n,
});

// Get quote for exact output
const outputQuote = await quoter.quoteExactOutputV4({
  tokenIn: "0x...",
  tokenOut: "0x...",
  amountOut: 1000000n,
  fee: 3000,
  sqrtPriceLimitX96: 0n,
});
```

## Key Features

* **Price Quotes**: Get accurate price quotes for both exact input and output swaps
* **No Gas Required**: Simulate swap outcomes without executing transactions
* **V4 Compatible**: Designed specifically for Uniswap V4 pools
* **Type Safety**: Full TypeScript support with proper type definitions


# Lens

Quoter Class Reference

The `ReadDopplerLens` class provides read-only access to the Doppler Lens contract, which fetches virtual updates to the Doppler Dutch auction that aren't reflected in the current chain state. This is essential for getting accurate real-time pricing and liquidity information during active price discovery phases.

## Overview

The Doppler Lens serves as a sophisticated quoter that can simulate swaps against Doppler pools and return detailed information about:

* Current pool state (price, tick, liquidity)
* Virtual token amounts in each position
* Position data for lower, upper, and price discovery slugs
* Real-time pricing without executing transactions

## Constructor

```typescript
new ReadDopplerLens(address: Hex, drift: Drift<ReadAdapter>)
```

**Parameters:**

* `address` - The address of the DopplerLensQuoter contract
* `drift` - Drift instance with read adapter (creates default if not provided)

## Core Methods

### poolManager

Retrieves the address of the Uniswap V4 pool manager used by the lens.

```typescript
async poolManager(): Promise<Address>
```

**Returns:** Promise resolving to the pool manager address

### stateView

Retrieves the address of the state view contract used for reading pool state.

```typescript
async stateView(): Promise<Address>
```

**Returns:** Promise resolving to the state view contract address

### quoteDopplerLensData

The main method for getting comprehensive Doppler pool data. This simulates a swap to extract current pool state and position information.

```typescript
async quoteDopplerLensData(
  params: QuoteExactSingleParams
): Promise<DopplerLensReturnData>
```

**Parameters:**

* `params` - Quote parameters containing:
  * `poolKey` - The pool identifier (tokens, fee, tick spacing, hooks)
  * `zeroForOne` - Direction of the swap (token0 → token1 or vice versa)
  * `exactAmount` - Amount to simulate swapping
  * `hookData` - Additional data for the hook (usually empty)

**Returns:** Promise resolving to:

* `sqrtPriceX96` - Current pool price in sqrt format
* `amount0` - Total amount of token0 across all positions
* `amount1` - Total amount of token1 across all positions
* `tick` - Current tick of the pool

## Types

### QuoteExactSingleParams

```typescript
interface QuoteExactSingleParams {
  poolKey: PoolKey;
  zeroForOne: boolean;
  exactAmount: bigint;
  hookData: Hex;
}
```

### PoolKey

```typescript
interface PoolKey {
  currency0: Address;
  currency1: Address;
  fee: number;
  tickSpacing: number;
  hooks: Address;
}
```

### DopplerLensReturnData

```typescript
interface DopplerLensReturnData {
  sqrtPriceX96: bigint;
  amount0: bigint;
  amount1: bigint;
  tick: number;
}
```

## Example Usage

```typescript
import { ReadDopplerLens } from "doppler-v4-sdk";

// Create lens instance
const lens = new ReadDopplerLens(lensAddress);

// Get pool manager and state view addresses
const poolManager = await lens.poolManager();
const stateView = await lens.stateView();

// Quote current pool state
const poolKey = {
  currency0: numeraireAddress,
  currency1: tokenAddress,
  fee: 20_000,
  tickSpacing: 2,
  hooks: dopplerHookAddress,
};

const quoteData = await lens.quoteDopplerLensData({
  poolKey,
  zeroForOne: true,
  exactAmount: 1, // Simulate swapping 1 wei
  hookData: "0x",
});

console.log("Current pool state:", {
  price: quoteData.sqrtPriceX96,
  tick: quoteData.tick,
  token0Amount: quoteData.amount0.toString(),
  token1Amount: quoteData.amount1.toString(),
});
```

## Use Cases

### Real-time Price Monitoring

```typescript
// Monitor price changes during Dutch auction
async function monitorPrice() {
  const data = await lens.quoteDopplerLensData({
    poolKey: myPoolKey,
    zeroForOne: true,
    exactAmount: parseEther("1"),
    hookData: "0x",
  });

  // Convert sqrt price to human-readable price
  const price = (Number(data.sqrtPriceX96) / 2 ** 96) ** 2;
  console.log(`Current price: ${price}`);
}
```

### Liquidity Analysis

```typescript
// Analyze total liquidity across all positions
async function analyzeLiquidity() {
  const data = await lens.quoteDopplerLensData({
    poolKey: myPoolKey,
    zeroForOne: false,
    exactAmount: 1n, // Minimal amount for state query
    hookData: "0x",
  });

  console.log("Total liquidity:", {
    totalToken0: formatEther(data.amount0),
    totalToken1: formatEther(data.amount1),
    tick: data.tick,
  });
}
```

## Key Features

* **Real-time State**: Get current pool state without waiting for blockchain updates
* **Virtual Positions**: See combined liquidity across lower, upper, and price discovery positions
* **Simulation Based**: Uses revert-based simulation for gas-free queries
* **Price Discovery**: Essential for monitoring active Dutch auctions
* **Non-view Functions**: Handles complex state calculations that require simulation

## Technical Notes

The lens contract uses a revert-based approach where it:

1. Simulates a swap to update internal state
2. Calculates position data across all Doppler slugs (lower, upper, price discovery)
3. Reverts with the calculated data to return results
4. Parses the revert data to extract meaningful information

This approach allows complex calculations that wouldn't be possible with pure view functions while maintaining gas efficiency for off-chain queries.


# Custom Fees

The V4 SDK includes support for creating Doppler V4 pools that can migrate their liquidity to other Uniswap V4 pools with customizable fee streaming. This allows protocols to distribute trading fees to multiple beneficiaries over time, and importantly, customize the post-graduation fee amounts.&#x20;

## Overview

The StreamableFeesLocker is a contract that:

* Locks Uniswap V4 positions for a specified duration
* Streams trading fees to multiple beneficiaries
* Supports perpetual fee collection for no-op governance
* **Compatible with both Doppler V3 and V4 pools** when migrating to Uniswap V4

## Basic Usage

### 1. Setting Up Beneficiaries

```typescript
import { BeneficiaryData, WAD, DEAD_ADDRESS } from 'doppler-v4-sdk';

// Define beneficiaries with their share percentages
// WAD is a constant equal to 1e18, representing 100% in fixed-point arithmetic
const beneficiaries: BeneficiaryData[] = [
  {
    beneficiary: '0x...protocol', // Protocol treasury
    shares: BigInt(0.05e18), // 5% in WAD (1e18 = 100%)
  },
  {
    beneficiary: '0x...integrator', // Integrator
    shares: BigInt(0.05e18), // 5% in WAD (1e18 = 100%)
  },
  {
    beneficiary: '0x...team', // Team/DAO
    shares: BigInt(0.9e18), // 90% in WAD
  },
];

// Sort beneficiaries (required for contract validation)
const sortedBeneficiaries = factory.sortBeneficiaries(beneficiaries);
```

### 2. Creating V4 Migrator Data

```typescript
import { V4MigratorData } from 'doppler-v4-sdk';

const v4MigratorConfig: V4MigratorData = {
  fee: 3000, // 0.3% in bips
  tickSpacing: 60,
  lockDuration: 30 * 24 * 60 * 60, // 30 days in seconds
  beneficiaries: sortedBeneficiaries,
};

// Encode the migrator data
const liquidityMigratorData = factory.encodeV4MigratorData(v4MigratorConfig);
```

###

## Post-Migration Operations

### 1. Distributing Fees

Anyone can call `distributeFees` to collect and distribute trading fees:

```typescript
import { streamableFeesLockerAbi } from 'doppler-v4-sdk';
import { createPublicClient, createWalletClient } from 'viem';

const client = createWalletClient({
  // ... client config
});

// Distribute fees for a position
const hash = await client.writeContract({
  address: addresses.streamableFeesLocker,
  abi: streamableFeesLockerAbi,
  functionName: 'distributeFees',
  args: [tokenId],
});
```

### 2. Claiming Fees (Beneficiaries)

Beneficiaries can claim their accumulated fees:

```typescript
const hash = await client.writeContract({
  address: addresses.streamableFeesLocker,
  abi: streamableFeesLockerAbi,
  functionName: 'releaseFees',
  args: [tokenId],
});
```

### 3. Updating Beneficiary Address

Beneficiaries can update their address:

```typescript
const hash = await client.writeContract({
  address: addresses.streamableFeesLocker,
  abi: streamableFeesLockerAbi,
  functionName: 'updateBeneficiary',
  args: [tokenId, newBeneficiaryAddress],
});
```

## Complete Examples

For detailed, production-ready examples of launching tokens with StreamableFeesLocker:

* [**Token Launch Examples**](/reference/legacy-sdks-and-migration-guides/v4/examples) - Complete guide with:
  * Standard governance launch (90/10 split)
  * No-op governance launch (100% locked)
  * Custom quote token launch
  * Post-launch fee operations

## Quick Example

```typescript
import { 
  ReadWriteFactory, 
  BeneficiaryData, 
  V4MigratorData,
  DEAD_ADDRESS
} from 'doppler-v4-sdk';

// 1. Set up beneficiaries
const beneficiaries: BeneficiaryData[] = [
  { beneficiary: protocolAddress, shares: BigInt(0.1e18) }, // 10%
  { beneficiary: teamAddress, shares: BigInt(0.9e18) }, // 90%
];

// 2. Create V4 migrator config
const v4Config: V4MigratorData = {
  fee: 3000,
  tickSpacing: 60,
  lockDuration: 180 * 24 * 60 * 60, // 180 days
  beneficiaries: factory.sortBeneficiaries(beneficiaries),
};

// 3. Build and launch
const config = await factory.buildConfig({
  // ... token parameters
  liquidityMigratorData: factory.encodeV4MigratorData(v4Config),
}, addresses, {
  useGovernance: false // For no-op governance
});

const tx = await factory.create(config.createParams);
```

## Key Points

1. **Beneficiary Validation**:
   * Beneficiaries must be sorted by address (ascending)
   * Total shares must equal exactly 1e18 (WAD)
   * All shares must be positive
2. **Lock Duration**:
   * Standard governance: Position unlocks after duration
   * No-op governance: Position locked forever (recipient = DEAD\_ADDRESS). The lockDuration value is ignored since the position is permanently locked
3. **Fee Distribution**:
   * Standard governance: 90% of liquidity goes to timelock, 10% to StreamableFeesLocker (automatic split by V4Migrator contract)
   * No-op governance: 100% goes to StreamableFeesLocker permanently
4. **Migration Types**:
   * Standard: Creates 2 NFTs, locks 10% in StreamableFeesLocker
   * No-op: Creates 1 NFT, locks 100% in StreamableFeesLocker permanently
5. **Pool Compatibility**:
   * **Doppler V3 Pools**: Use `UniswapV3Initializer` + `UniswapV4Migrator` to get fee streaming
   * **Doppler V4 Pools**: Use `UniswapV4Initializer` + `UniswapV4Migrator` to get fee streaming
   * Both pool types can leverage the StreamableFeesLocker when migrating to Uniswap V4


# Governance Options

This guide explains how to configure different governance options when creating tokens with the Doppler V4 SDK, including using the NoOpGovernanceFactory for gas-efficient deployments.

## Overview

The Doppler V4 SDK supports optional governance with the following models.

1. **"Standard" Governance** - Full on-chain governance with timelock using OpenZeppelin Governor
2. **"No-Op" Governance** - Minimal governance for gas savings (sets governance to `0xdead`)

## Using NoOpGovernanceFactory

The NoOpGovernanceFactory creates tokens without active governance, significantly reducing deployment costs and complexity. This is ideal for projects that don't require on-chain governance.

### Examples

### 1. Standard Governance Configuration

```typescript
const config = await factory.buildConfig({
  // ... other parameters
  liquidityMigratorData,
  integrator: '0x...integrator',
}, addresses);

// Create the pool
const txHash = await factory.create(config.createParams);
```

### 2. No-Op Governance Configuration

For no-op governance (permanent liquidity lock with perpetual fee streaming):

```typescript
// Configure for no-op governance
const config = await factory.buildConfig({
  // ... other parameters
  liquidityMigratorData,
  integrator: '0x...integrator',
}, addresses, {
  useGovernance: false // This uses the noOpGovernanceFactory
});

// The migration will automatically set recipient to DEAD_ADDRESS (0xdead)
// This ensures the position is permanently locked
```


# Historical context

### Context

Doppler was initially released with two SDKs — one for the v3 version of the protocol that facilitates static bonding curves, and another for the v4 version of the protocol, facilitating dynamic bonding curves with dutch auctions. This decision was inspired by Uniswap having different SDKs and versioning for different versions of the Uniswap protocol.

Doppler has decided in an effort to optimize developer experience and ease of switching between different versions of the protocol to *consolidate the two SDKs* into a single package and interface. Additionally we have chosen to implement a friendly and intuitive builder pattern, making it even easier to configure auctions, governance, vesting, liquidity migration, and quotes/swapping.

## Status

The original v3/v4 Doppler SDKs are officially deprecated and no longer supported. Teams using these SDKs are encouraged to migrate as soon as possible using this migration guide to the latest SDK. The legacy SDKs are hosted at <https://github.com/whetstoneresearch/doppler-sdk-legacy>. The new, officially supported SDK (previously referred to as the "alpha" SDK) is now hosted at [https://github.com/whetstoneresearch/doppler-sdk ](https://github.com/whetstoneresearch/doppler-sdk)and released in Beta. This is currently used in the Doppler Application and various third party applications, making it the official recommendation.


# Legacy SDK migration guide

Migrate from the (legacy) Doppler v3 or v4 SDK to the latest

## Migration Guide

This guide helps you migrate from `doppler-v3-sdk` or `doppler-v4-sdk` to the unified `@whetstone-research/doppler-sdk` using the builder pattern.

### Overview of Changes

The new SDK consolidates both V3 and V4 functionality into a single package with clearer terminology:

* **V3 → Static Auctions**: Fixed price range liquidity bootstrapping
* **V4 → Dynamic Auctions**: Gradual Dutch auctions with Uniswap V4 hooks
* **Unified API**: Single SDK instance handles both auction types
* **Improved Types**: Discriminated unions for type-safe configurations
* **viem Integration**: Replaced ethers.js with viem for better performance

### Installation

Remove the old packages and install the new unified SDK:

```bash
# Remove old packages
npm uninstall @doppler/v3-sdk @doppler/v4-sdk ethers

# Install new packages
npm install @whetstone-research/doppler-sdk viem
```

### Initialization Changes

#### Before (V3 SDK)

```typescript
import { ReadWriteFactory } from '@doppler/v3-sdk';
import { ethers } from 'ethers';

const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
const signer = new ethers.Wallet(privateKey, provider);

const factory = new ReadWriteFactory(signer, chainId);
```

#### Before (V4 SDK)

```typescript
import { ReadWriteFactory } from '@doppler/v4-sdk';
import { ethers } from 'ethers';

const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
const signer = new ethers.Wallet(privateKey, provider);

const factory = new ReadWriteFactory(signer, chainId);
```

#### After (Unified SDK)

```typescript
import { DopplerSDK } from '@whetstone-research/doppler-sdk';
import { createPublicClient, createWalletClient, http } from 'viem';
import { base } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';

const publicClient = createPublicClient({
  chain: base,
  transport: http(rpcUrl),
});

const walletClient = createWalletClient({
  chain: base,
  transport: http(rpcUrl),
  account: privateKeyToAccount(privateKey),
});

const sdk = new DopplerSDK({
  publicClient,
  walletClient,
  chainId: base.id,
});
```

### Creating Auctions

#### Static Auctions (Previously V3)

**Before**

```typescript
// Manually encode migration data
const liquidityMigratorData = await factory.encodeV4MigratorData({
  fee: 3000,
  tickSpacing: 60,
  lockDuration: 365 * 24 * 60 * 60,
  beneficiaries: sortedBeneficiaries,
});

const { poolAddress, tokenAddress } = await factory.create({
  name: 'My Token',
  symbol: 'MTK',
  tokenURI: 'https://example.com/token',
  vestingDuration: 0,
  yearlyMintRate: 0,
  totalSupply: parseEther('1000000'),
  numTokensToSell: parseEther('500000'),
  startTick: -92103,
  endTick: -69080,
  fee: 3000,
  numeraire: wethAddress,
  initialRecipients: [],
  initialAmounts: [],
  contracts: {
    governor: governorAddress,
    tokenFactory: addresses.tokenFactory,
    poolInitializer: addresses.v3Initializer,
    liquidityMigrator: addresses.v4Migrator,
    airlock: addresses.airlock,
  },
  integrator: integratorAddress,
  liquidityMigratorData,
});
```

**After (Builder pattern)**

```typescript
import { StaticAuctionBuilder } from '@whetstone-research/doppler-sdk'

const params = new StaticAuctionBuilder()
  .tokenConfig({ name: 'My Token', symbol: 'MTK', tokenURI: 'https://example.com/token' })
  .saleConfig({
    initialSupply: parseEther('1000000'),
    numTokensToSell: parseEther('500000'),
    numeraire: wethAddress,
  })
  .poolByTicks({ startTick: -92103, endTick: -69080, fee: 3000 })
  .withMigration({
    type: 'uniswapV4',
    fee: 3000,
    tickSpacing: 60,
    streamableFees: { lockDuration: 365 * 24 * 60 * 60, beneficiaries: sortedBeneficiaries },
  })
  .withVesting({ duration: 0n })
  .withIntegrator(integratorAddress)
  .withUserAddress(governorAddress)
  .build()

const result = await sdk.factory.createStaticAuction(params)
```

#### Dynamic Auctions (Previously V4)

**Before**

```typescript
// Calculate gamma manually
const gamma = calculateGamma(...);

// Mine hook address
const minedAddress = await hookAddressMiner.mine({
  dopplerDeployer: addresses.dopplerDeployer,
  prefix: '0x00',
});

const { hookAddress, tokenAddress } = await factory.create({
  name: 'My Token',
  symbol: 'MTK',
  tokenURI: 'https://example.com/token',
  // ... many parameters
  poolInitializerData: encodePoolInitializerData({
    minimumProceeds,
    maximumProceeds,
    startingTime,
    endingTime,
    startingTick,
    endingTick,
    epochLength,
    gamma,
    isToken0,
    numPDSlugs,
    fee,
    tickSpacing,
  }),
});
```

**After (Builder pattern)**

```typescript
import { DynamicAuctionBuilder } from '@whetstone-research/doppler-sdk'

const params = new DynamicAuctionBuilder()
  .tokenConfig({ name: 'My Token', symbol: 'MTK', tokenURI: 'https://example.com/token' })
  .saleConfig({ initialSupply: parseEther('1000000'), numTokensToSell: parseEther('500000'), numeraire: wethAddress })
  .poolConfig({ fee: 3000, tickSpacing: 60 })
  .auctionByPriceRange({
    priceRange: { startPrice: 0.0001, endPrice: 0.01 },
    minProceeds: parseEther('50'),
    maxProceeds: parseEther('500'),
    duration: 7 * 24 * 60 * 60,  // 7 days in seconds
    epochLength: 43200,  // 12 hours (default)
  })
  .withMigration({
    type: 'uniswapV4',
    fee: 3000,
    tickSpacing: 60,
    streamableFees: { lockDuration: 365 * 24 * 60 * 60, beneficiaries: [...] },
  })
  .withUserAddress(governorAddress)
  .build()

const result = await sdk.factory.createDynamicAuction(params)
```

### Interacting with Auctions

#### Before (Both SDKs)

```typescript
// Manual contract calls with ethers
const pool = new ethers.Contract(poolAddress, poolAbi, provider);
const slot0 = await pool.slot0();
const liquidity = await pool.liquidity();
```

#### After (Unified SDK)

```typescript
// Static Auction
const staticAuction = await sdk.getStaticAuction(poolAddress);
const poolInfo = await staticAuction.getPoolInfo();
const hasGraduated = await staticAuction.hasGraduated();

// Dynamic Auction
const dynamicAuction = await sdk.getDynamicAuction(hookAddress);
const hookInfo = await dynamicAuction.getHookInfo();
const currentEpoch = await dynamicAuction.getCurrentEpoch();
```

### Token Interactions

#### Before

```typescript
import { Derc20 } from 'doppler-v3-sdk';

const token = new Derc20(tokenAddress, signer);
const balance = await token.balanceOf(address);
```

#### After (Unified SDK)

```typescript
import { Derc20 } from '@whetstone-research/doppler-sdk';

const token = new Derc20(publicClient, walletClient, tokenAddress);
const balance = await token.getBalanceOf(address);
const vestingData = await token.getVestingData(address);
```

### Quoter Changes

#### Before

```typescript
import { Quoter } from 'doppler-v3-sdk';

const quoter = new Quoter(signer, chainId);
const quote = await quoter.quoteExactInputSingle({
  tokenIn,
  tokenOut,
  fee,
  amountIn,
  sqrtPriceLimitX96,
});
```

#### After

```typescript
const quoter = sdk.quoter;
const quote = await quoter.quoteV3ExactInputSingle({
  tokenIn,
  tokenOut,
  fee,
  amountIn,
  sqrtPriceLimitX96: 0n,
});
```


# Security & bug bounties

## Audits

Doppler has been audited multiple times by top teams.&#x20;

:link: [OpenZeppelin Audit](https://drive.google.com/drive/folders/1cgY4UDtQ9j2v1t4XvFBraJqNCUfrdFxq?dmr=1\&ec=wgc-drive-globalnav-goto) - Nov. 2024

:link: [Certora Audit](https://drive.google.com/drive/folders/1cgY4UDtQ9j2v1t4XvFBraJqNCUfrdFxq?dmr=1\&ec=wgc-drive-globalnav-goto) - Nov. 204

Additionally, Doppler had a public audit contest done via Cantina.&#x20;

:link: [Doppler Cantina Audit Contest](https://cantina.xyz/competitions/57b00aab-8f8b-4d62-9378-41b6460ce6aa)

## Bug Bounty & Issues

Doppler has an active bug bounty program being ran in collaboration with Cantina.&#x20;

Learn more & report bugs: <https://cantina.xyz/bounties/2c7af549-c36c-4432-bae6-3f4b1fa6b217>&#x20;

Please follow best practices for responsible disclosure as there could be user funds at risk.

### Questions?&#x20;

Contact <security@whetstone.cc>&#x20;


# Roadmap

A high level overview of where Doppler is planning to go...

<figure><img src="/files/P22I7Z8cD2PxDeAko7fw" alt=""><figcaption></figcaption></figure>

Notably this does not include every designed or planned feature, improvement, nor integration. Rather it aims to provide directional insight to communities interested in building with Doppler so they can make more informed decisions about their own integration roadmaps. Additionally this serves as a vehicle by which teams can request other items they’d love to see prioritized.

***It is entirely subject to change.***

**Completed** :white\_check\_mark:

* ~~Protocol & Unichain Network Launch~~ - [Read the Launch announcement](https://x.com/aadams/status/1889362777791168877)
* ~~Ink Network Support~~ - [Read the Ink announcement](https://x.com/dopplerprotocol/status/1905273707817316510)
* ~~Base Network Support~~ - [Read the Base announcement](https://x.com/dopplerprotocol/status/1907810739315761221)
* ~~Doppler Core~~ - [Read the Core announcement](https://x.com/dopplerprotocol/status/1933169128166334882)
* ~~Doppler Multicurve~~ - [Read the Multicurve announcement](https://x.com/dopplerprotocol/status/1973770809165639855)
* ~~Monad Network Support~~ - [Read the Monad announcement](https://x.com/dopplerprotocol/status/1992986760205480204)
* ~~Doppler 404~~ - [Read the DN404 announcement](https://x.com/dopplerprotocol/status/1963646130732081406)
* ~~Doppler Optimized for Mainnet Ethereum (DOME)~~ - [Read the Ethereum announcement](https://x.com/dopplerprotocol/status/2020928888671699410)
* ~~Doppler Hooks~~
* ~~Fee Rehypothecation Hook~~ - [Read the Rehype announcement](https://x.com/dopplerprotocol/status/2019832104746700846)
* ~~Doppler Frontend Application~~ - [Read the Doppler App announcement](https://x.com/dopplerprotocol/status/2021305070030053586)
* ~~Decaying Launch Fees~~ - [Read the Decaying Fees announcement](https://x.com/dopplerprotocol/status/2021989921133830359)

**Coming soon:**

* Fee vesting
* Multi recipient cliff and vesting management
* Orchestrator contract(s) to manage assets from other contracts
* Multiple address vesting and cliff management
* External authentication hooks

**Always ongoing:**

* Additional application integrations
* Deployments to popular Layer-1's
* Deployments to popular Layer-2's
* Developer experience improvements

{% hint style="info" %}
If you work on project and are interested in integrations or collaborations with Doppler, such as deploying to your protocol, please get in touch at <contact@whetstone.cc> or to a team member directly.
{% endhint %}

The [Doppler Airlock](https://docs.doppler.lol/how-it-works/airlock-and-modules) makes it seamless to optionally integrate other other smart contract protocols that benefit the token issuance lifecycle. Some of these may include novel sybil resistance mechanisms that can reduce spam or MEV during the price discovery auction. A significant portion of the Doppler roadmap aims to provide a seamless, programmable experience for the initial distribution of assets, as well as their ongoing governance and long term alignment between ecosystem participants.

**Future plans (not in any particular order):**

* New novel auction dynamics
* Sybil resistance modules
* Airdrop protocol modules
* Lending protocol modules
* More flexible governance structures
* Customizable fee & MEV redistribution

{% hint style="info" %}
If you have questions, feedback, or there are other things you would like to see in the Roadmap, please reach out to <contact@whetstone.cc>, a team member directly, or join the [Doppler Telegram](https://doppler.lol/telegram).
{% endhint %}


