> ## Documentation Index
> Fetch the complete documentation index at: https://docs.raydium.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 部署 CLMM 流动性池

> 四文件 TypeScript 流程，创建新的 CLMM 池，然后开启初始集中头寸。使用 npx tsx 运行；镜像官方 raydium-sdk-V2-demo 流程。

<Info>
  **本页内容由 AI 自动翻译，所有内容以英文版本为准。**

  [查看英文版 →](/quick-start/deploy-clmm-pool)
</Info>

<Info>
  **这个脚本做什么。** 在你选择的费用等级创建新的 CLMM 池，然后开启初始集中头寸。跨四个文件的两笔交易 — `config.ts`、`createPool.ts`、`createPosition.ts` 和 `utils.ts` — 使用 `npx tsx` 运行。代码取自官方演示 [`raydium-sdk-V2-demo/src/clmm`](https://github.com/raydium-io/raydium-sdk-V2-demo/tree/master/src/clmm)，并适配为独立运行。
</Info>

## 设置

确保你已阅读 [快速开始前置条件](/zh/quick-start)，并已安装 `RPC_URL`、`KEYPAIR` 和相关依赖。

除了快速开始中的依赖外，你还需要：

```bash icon="terminal" theme={null}
npm install decimal.js bs58 tsx
```

* **`decimal.js`** — `createPool.ts` 和 `createPosition.ts` 中的价格和 tick 数学运算。
* **`bs58`** — 如果使用 `KEYPAIR_BS58` 而非 JSON 密钥文件，在 `config.ts` 中解码 base58 编码的密钥对。
* **`tsx`** — 通过 `npx tsx` 直接运行下面的 `.ts` 文件；与交换和 CPMM 脚本不同，这个不是用 `node` 运行的普通 `.mjs` 文件。

CLMM 池创建有一次性费用加上初始头寸的 per-tick-array 租金。你还需要在钱包中拥有两个种子代币 — 当价格位于选定范围内时开启头寸需要两侧都有流动性。

**在 devnet 上运行。** 仅在 `config.ts` 中将 `cluster` 改为 `"devnet"` 是不够的。`createPool.ts` 传递显式的 `programId: CLMM_PROGRAM_ID`，这是一个不跟随 `cluster` 的固定主网常数 — 将其替换为上面注释掉的 `DEVNET_PROGRAM_ID.CLMM_PROGRAM_ID` 行。`createPosition.ts` 也硬编码了主网 `poolId`（RAY-USDC 池）；将其替换为你在第 2 步创建的 devnet 池的 ID。

## 第 1 步 — `config.ts`

保存为 `config.ts`。这与演示仓库的 [`src/config.ts.template`](https://github.com/raydium-io/raydium-sdk-V2-demo/blob/master/src/config.ts.template) 形状相同 — `disableFeatureCheck` 被强制设为 `true`（对任何非平凡的集成都推荐，这样 SDK 不会在启动特性检测调用时阻塞）：

```ts config.ts icon="js" theme={null}
import { Raydium, TxVersion, parseTokenAccountResp } from "@raydium-io/raydium-sdk-v2";
import { Connection, Keypair, clusterApiUrl } from "@solana/web3.js";
import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token";
import bs58 from "bs58";
import fs from "node:fs";

export const owner: Keypair = Keypair.fromSecretKey(
  // accept either a JSON-array keypair file (same shape Solana CLI writes) or a bs58 secret in env
  process.env.KEYPAIR_BS58
    ? bs58.decode(process.env.KEYPAIR_BS58)
    : new Uint8Array(JSON.parse(fs.readFileSync(process.env.KEYPAIR!, "utf8"))),
);

export const connection = new Connection(
  process.env.RPC_URL ?? clusterApiUrl("mainnet-beta"),
  "confirmed",
);
export const txVersion = TxVersion.V0;
const cluster = "mainnet" as "mainnet" | "devnet";

let raydium: Raydium | undefined;
export const initSdk = async (params?: { loadToken?: boolean }) => {
  if (raydium) return raydium;
  raydium = await Raydium.load({
    owner,
    connection,
    cluster,
    disableFeatureCheck: true,
    disableLoadToken: !params?.loadToken,
    blockhashCommitment: "finalized",
  });
  return raydium;
};

export const fetchTokenAccountData = async () => {
  const solAccountResp = await connection.getAccountInfo(owner.publicKey);
  const tokenAccountResp = await connection.getTokenAccountsByOwner(owner.publicKey, {
    programId: TOKEN_PROGRAM_ID,
  });
  const token2022Req = await connection.getTokenAccountsByOwner(owner.publicKey, {
    programId: TOKEN_2022_PROGRAM_ID,
  });
  return parseTokenAccountResp({
    owner: owner.publicKey,
    solAccountResp,
    tokenAccountResp: {
      context: tokenAccountResp.context,
      value: [...tokenAccountResp.value, ...token2022Req.value],
    },
  });
};
```

## 第 2 步 — `createPool.ts`

与 `config.ts` 一起保存。来源：[`src/clmm/createPool.ts`](https://github.com/raydium-io/raydium-sdk-V2-demo/blob/master/src/clmm/createPool.ts)。

```ts createPool.ts icon="js" theme={null}
import { CLMM_PROGRAM_ID, DEVNET_PROGRAM_ID } from "@raydium-io/raydium-sdk-v2";
import { PublicKey } from "@solana/web3.js";
import Decimal from "decimal.js";
import { initSdk, txVersion } from "./config";

export const createPool = async () => {
  const raydium = await initSdk({ loadToken: true });

  // RAY
  const mint1 = await raydium.token.getTokenInfo("4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R");
  // USDT
  const mint2 = await raydium.token.getTokenInfo("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB");

  // Fee tiers come from the live API. On devnet the published `id` field is wrong;
  // re-derive the PDA before passing it to the SDK.
  const clmmConfigs = await raydium.api.getClmmConfigs();

  const { execute } = await raydium.clmm.createPool({
    programId: CLMM_PROGRAM_ID,
    // programId: DEVNET_PROGRAM_ID.CLMM_PROGRAM_ID,
    mint1,
    mint2,
    ammConfig: {
      ...clmmConfigs[0],
      id: new PublicKey(clmmConfigs[0].id),
      fundOwner: "",
      description: "",
    },
    initialPrice: new Decimal(1),
    txVersion,
    // optional: set up priority fee here
    // computeBudgetConfig: { units: 600000, microLamports: 46591500 },
  });

  const { txId } = await execute({ sendAndConfirm: true });
  console.log("clmm pool created:", { txId: `https://explorer.solana.com/tx/${txId}` });
  process.exit();
};

createPool();
```

## 第 3 步 — `createPosition.ts`

来源：[`src/clmm/createPosition.ts`](https://github.com/raydium-io/raydium-sdk-V2-demo/blob/master/src/clmm/createPosition.ts)。

```ts createPosition.ts icon="js" theme={null}
import {
  ApiV3PoolInfoConcentratedItem,
  TickUtil,
  PoolUtils,
  ClmmKeys,
} from "@raydium-io/raydium-sdk-v2";
import BN from "bn.js";
import Decimal from "decimal.js";
import { initSdk, txVersion } from "./config";
import { isValidClmm } from "./utils";

export const createPosition = async () => {
  const raydium = await initSdk();

  let poolInfo: ApiV3PoolInfoConcentratedItem;
  // RAY-USDC pool
  const poolId = "61R1ndXxvsWXXkWSyNkCxnzwd3zUNB8Q2ibmkiLPC8ht";
  let poolKeys: ClmmKeys | undefined;

  if (raydium.cluster === "mainnet") {
    const data = await raydium.api.fetchPoolById({ ids: poolId });
    poolInfo = data[0] as ApiV3PoolInfoConcentratedItem;
    if (!isValidClmm(poolInfo.programId)) throw new Error("target pool is not CLMM pool");
  } else {
    const data = await raydium.clmm.getPoolInfoFromRpc(poolId);
    poolInfo = data.poolInfo;
    poolKeys = data.poolKeys;
  }

  // Optional: pull on-chain real-time price to avoid slippage errors from a stale API quote.
  // const rpcData = await raydium.clmm.getRpcClmmPoolInfo({ poolId: poolInfo.id });
  // poolInfo.price = rpcData.currentPrice;

  const inputAmount = 0.000001; // RAY amount
  const [startPrice, endPrice] = [0.000001, 100000];

  // The class is TickUtil (singular), and getPriceAndTick takes the two mint
  // decimals plus tickSpacing — not poolInfo, and the direction flag is zeroForOne.
  const { tick: lowerTick } = TickUtil.getPriceAndTick({
    price: new Decimal(startPrice),
    mintADecimals: poolInfo.mintA.decimals,
    mintBDecimals: poolInfo.mintB.decimals,
    zeroForOne: true,
    tickSpacing: poolInfo.config.tickSpacing,
  });
  const { tick: upperTick } = TickUtil.getPriceAndTick({
    price: new Decimal(endPrice),
    mintADecimals: poolInfo.mintA.decimals,
    mintBDecimals: poolInfo.mintB.decimals,
    zeroForOne: true,
    tickSpacing: poolInfo.config.tickSpacing,
  });

  const epochInfo = await raydium.fetchEpochInfo();
  const res = await PoolUtils.getLiquidityAmountOutFromAmountIn({
    poolInfo,
    slippage: 0,
    inputA: true,
    tickUpper: Math.max(lowerTick, upperTick),
    tickLower: Math.min(lowerTick, upperTick),
    amount: new BN(new Decimal(inputAmount || "0").mul(10 ** poolInfo.mintA.decimals).toFixed(0)),
    add: true,
    amountHasFee: true,
    epochInfo,
  });

  const { execute, extInfo } = await raydium.clmm.openPositionFromBase({
    poolInfo,
    poolKeys,
    tickUpper: Math.max(lowerTick, upperTick),
    tickLower: Math.min(lowerTick, upperTick),
    base: "MintA",
    ownerInfo: { useSOLBalance: true },
    baseAmount: new BN(new Decimal(inputAmount || "0").mul(10 ** poolInfo.mintA.decimals).toFixed(0)),
    otherAmountMax: res.amountSlippageB.amount,
    txVersion,
    computeBudgetConfig: { units: 600000, microLamports: 100000 },
  });

  const { txId } = await execute({ sendAndConfirm: true });
  console.log("clmm position opened:", { txId, nft: extInfo.nftMint.toBase58() });
  process.exit();
};

createPosition();
```

## 第 4 步 — `utils.ts`

来源：[`src/clmm/utils.ts`](https://github.com/raydium-io/raydium-sdk-V2-demo/blob/master/src/clmm/utils.ts)。

```ts utils.ts icon="js" theme={null}
import { CLMM_PROGRAM_ID, DEVNET_PROGRAM_ID } from "@raydium-io/raydium-sdk-v2";

const VALID_PROGRAM_IDS = new Set<string>([
  CLMM_PROGRAM_ID.toBase58(),
  DEVNET_PROGRAM_ID.CLMM_PROGRAM_ID.toBase58(),
]);

export const isValidClmm = (programId: string) => VALID_PROGRAM_IDS.has(programId);
```

## 运行

```bash icon="terminal" theme={null}
# 先创建池
RPC_URL="https://api.mainnet-beta.solana.com" \
KEYPAIR="$HOME/.config/solana/id.json" \
npx tsx createPool.ts

# 然后针对新池 ID 开启初始头寸
# （编辑 createPosition.ts 顶部的 poolId 指向你的新池）
RPC_URL="https://api.mainnet-beta.solana.com" \
KEYPAIR="$HOME/.config/solana/id.json" \
npx tsx createPosition.ts
```

## 刚才发生了什么

**交易 1 — `raydium.clmm.createPool`** 初始化了：

* 池状态在 `(mint1, mint2, ammConfig)` 的规范 PDA，
* `token_0_vault` 和 `token_1_vault`（按 mint 字节顺序排序），
* `observation` 环形缓冲区，
* 内联 tick 数组位图，

并从你的 `initialPrice` 设置初始 `sqrt_price_x64`。

**交易 2 — `raydium.clmm.openPositionFromBase`** 开启了集中头寸：

* 向你的钱包铸造了头寸 NFT（NFT *就是*头寸；转移它就转移了头寸），
* 在下界和上界分配了 tick 数组（如果是这些范围内的第一个头寸则是一次性租金；tick 数组永远不会被程序关闭，所以同一数组中的后续头寸不需要额外租金），
* 存入了 `inputAmount` 的 `mint1` 和匹配的 `mint2` 对应金额（由 `PoolUtils.getLiquidityAmountOutFromAmountIn` 计算），
* 根据范围宽度向头寸记入流动性。

范围越窄，每美元 TVL 的资本效率越高 — 当价格漂出范围时无常损失也越痛苦。上面使用的范围（`[0.000001, 100000]`）实际上是全范围；收紧它以在当前现货附近集中费用。

## 选择费用等级

`clmmConfigs[0]` 是最低费用等级。完整集合发布在 `GET https://api-v3.raydium.io/main/clmm-config`：

| 索引 | `tradeFeeRate` | Tick 间距 | 使用场景 |
| - | - | - | - |
| 0 | `100`（1bp） | 1 | 稳定币对稳定币，预期无常损失极低 |
| 1 | `500`（5bp） | 10 | 高度相关资产（如流动性质押 vs 基础资产） |
| 2 | `2_500`（25bp） | 60 | 标准代币对，蓝筹 + 稳定币 |
| 3 | `10_000`（1.00%） | 120 | 波动性大或流动性薄的对，IL 风险高 |

参见 [`user-flows/choosing-a-pool-type`](/zh/user-flows/choosing-a-pool-type) 了解完整决策矩阵。

## 常见错误

* **`Pool already exists for this config`** — 此 `(mint1, mint2, ammConfig)` 三元组已存在 CLMM 池。查找现有池 ID 并跳过第 2 步。
* **`Insufficient funds for amount B`** — 你的钱包有请求的 `mintA` 金额但没有匹配的 `mintB`。当价格位于范围内时开启头寸需要两侧都有流动性。
* **`Tick out of range`** — 你的 `lowerPrice` 或 `upperPrice` 超出可表示的价格范围。使用相对于当前价格更合理的范围。
* **过期价格** — API 的报价可能有 5–60 秒的延迟。如果 `executePosition` 因滑点失败，取消注释 `createPosition.ts` 中的 `getRpcClmmPoolInfo` 块以在签名前重新获取实时价格。

## 注意事项

* **头寸 NFT 是你唯一的句柄。** 丢失 NFT 或转移它，就失去了对头寸的访问权。把它当作密钥对待。
* **超出范围的头寸不赚取费用。** 如果价格移出 `[lowerPrice, upperPrice]`，你的头寸完全停泊在一种资产中，直到你重新平衡才能赚取任何费用。
* **Tick 数组租金是单向的。** 第一个接触从未初始化的 tick 数组的头寸支付其租金；程序不公开关闭 tick 数组的路径，所以该租金是永久的。同一数组中的后续头寸是免费的。

## 下一步

* [`products/clmm/overview`](/zh/products/clmm/overview) — 完整 CLMM 机制。
* [`products/clmm/ticks-and-positions`](/zh/products/clmm/ticks-and-positions) — tick 背后的数学。
* [`algorithms/impermanent-loss`](/zh/algorithms/impermanent-loss) — 量化 CLMM IL 放大。
* [`user-flows/create-clmm-pool`](/zh/user-flows/create-clmm-pool) — 通过 Raydium UI 的相同流程。

来源：

* [`raydium-sdk-V2-demo/src/clmm/createPool.ts`](https://github.com/raydium-io/raydium-sdk-V2-demo/blob/master/src/clmm/createPool.ts)
* [`raydium-sdk-V2-demo/src/clmm/createPosition.ts`](https://github.com/raydium-io/raydium-sdk-V2-demo/blob/master/src/clmm/createPosition.ts)
* [`raydium-sdk-V2-demo/src/clmm/utils.ts`](https://github.com/raydium-io/raydium-sdk-V2-demo/blob/master/src/clmm/utils.ts)
* [`raydium-sdk-V2-demo/src/config.ts.template`](https://github.com/raydium-io/raydium-sdk-V2-demo/blob/master/src/config.ts.template)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.