> ## 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 풀 배포하기

> 새로운 CLMM 풀을 생성한 후 초기 집중된 포지션을 열어주는 복사-붙여넣기용 Node 스크립트입니다. 공식 raydium-sdk-V2-demo 흐름을 그대로 따릅니다.

<Info>
  **이 페이지는 AI 자동 번역입니다. 모든 내용은 영문판을 기준으로 합니다.**

  [영문판 보기 →](/quick-start/deploy-clmm-pool)
</Info>

<Info>
  **이것이 하는 일.** 선택한 수수료 티어에서 새로운 CLMM 풀을 생성한 다음, 초기 집중된 포지션을 엽니다. 두 개의 트랜잭션, 하나의 스크립트입니다. 코드는 [`raydium-sdk-V2-demo/src/clmm`](https://github.com/raydium-io/raydium-sdk-V2-demo/tree/master/src/clmm)의 공식 데모에서 가져온 것이며, 단일 Node 실행 파일로 조정되었습니다.
</Info>

## 설정

[빠른 시작 필수 사항](/ko/quick-start)을 읽었는지 확인하고 `RPC_URL`, `KEYPAIR`, 및 종속성이 설치되어 있어야 합니다.

CLMM 풀 생성에는 일회성 수수료와 초기 포지션에 대한 틱 배열 렌트가 필요합니다. 또한 두 시드 민트가 지갑에 있어야 합니다. 가격이 선택한 범위 내에 있을 때 포지션을 열려면 양쪽 모두에 유동성이 필요합니다.

## 단계 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 theme={null}
// config.ts
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 theme={null}
// createPool.ts
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 theme={null}
// createPosition.ts
import {
  ApiV3PoolInfoConcentratedItem,
  TickUtils,
  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];

  const { tick: lowerTick } = TickUtils.getPriceAndTick({
    poolInfo,
    price: new Decimal(startPrice),
    baseIn: true,
  });
  const { tick: upperTick } = TickUtils.getPriceAndTick({
    poolInfo,
    price: new Decimal(endPrice),
    baseIn: true,
  });

  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 theme={null}
// utils.ts
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 theme={null}
# create the pool first
RPC_URL="https://api.mainnet-beta.solana.com" \
KEYPAIR="$HOME/.config/solana/id.json" \
npx tsx createPool.ts

# then open an initial position against the new pool id
# (edit poolId at the top of createPosition.ts to point at your new pool)
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` (민트 바이트 순서로 정렬),
* `observation` 링 버퍼,
* 인라인 틱 배열 비트맵,

그리고 `initialPrice`에서 초기 `sqrt_price_x64`를 설정했습니다.

**트랜잭션 2 — `raydium.clmm.openPositionFromBase`** 집중된 포지션 개설:

* 지갑에 포지션 NFT를 발행했습니다 (NFT *는* 포지션이고, NFT를 전송하면 포지션이 전송됨),
* 하한과 상한에서 틱 배열을 할당했습니다 (이 범위의 첫 포지션인 경우 일회성 렌트; 틱 배열은 프로그램에서 닫히지 않으므로 같은 배열의 후속 포지션은 추가 렌트를 지불하지 않음),
* `mint1`의 `inputAmount`와 일치하는 `mint2`의 쌍 금액을 예치했습니다 (`PoolUtils.getLiquidityAmountOutFromAmountIn`에서 계산).
* 포지션에 범위 너비에 비례하는 유동성을 크레딧했습니다.

범위가 좁을수록 TVL 달러당 자본 효율성이 높아지고, 가격이 범위를 벗어날 때 무상손실이 더 심해집니다. 위에서 사용한 범위 (`[0.000001, 100000]`)는 실제로 풀 범위이고, 현재 스팟 근처 수수료를 집중하려면 범위를 좁혀야 합니다.

## 수수료 티어 선택

`clmmConfigs[0]`은 최저 수수료 티어입니다. 전체 세트는 `GET https://api-v3.raydium.io/main/clmm-config`에서 발행됩니다:

| 지수 | `tradeFeeRate`   | 틱 간격 | 사용할 때                         |
| -- | ---------------- | ---- | ----------------------------- |
| 0  | `100` (1bp)      | 1    | 안정적/안정적, 무상손실 매우 낮음 예상        |
| 1  | `500` (5bp)      | 10   | 높은 상관관계 자산 (예: 액체 스테이킹 vs 기초) |
| 2  | `2_500` (25bp)   | 60   | 표준 토큰 쌍, 블루칩 + 안정             |
| 3  | `10_000` (1.00%) | 120  | 무상손실 위험이 높은 변동성 또는 얇은 쌍       |

전체 의사 결정 매트릭스는 [`user-flows/choosing-a-pool-type`](/ko/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]` 범위를 벗어나면, 포지션은 전적으로 하나의 자산으로 주차되고 재조정할 때까지 아무것도 얻지 못합니다.
* **틱 배열 렌트는 일방적입니다.** 처음 초기화되지 않은 틱 배열에 접근하는 첫 번째 포지션이 렌트를 지불합니다. 프로그램은 틱 배열을 닫을 경로를 노출하지 않으므로 해당 렌트는 영구적입니다. 같은 배열의 후속 포지션은 무료입니다.

## 다음

* [`products/clmm/overview`](/ko/products/clmm/overview) — 전체 CLMM 메커니즘.
* [`products/clmm/ticks-and-positions`](/ko/products/clmm/ticks-and-positions) — 틱의 수학.
* [`algorithms/impermanent-loss`](/ko/algorithms/impermanent-loss) — CLMM 무상손실 증폭 정량화.
* [`user-flows/create-clmm-pool`](/ko/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)
