ローンチタイプ

ボンディングカーブ スワップ統合

Last updated April 9, 2026

Genesis SDKを使用してボンディングカーブの状態を読み取り、スワップ見積もりを計算し、売買トランザクションをオンチェーンで実行し、スリッページを処理し、クリエイター手数料を請求します。

構築する内容

このガイドでは以下をカバーします:

  • BondingCurveBucketV2 アカウント状態の取得と解釈
  • isSwappableisSoldOutisGraduated でのライフサイクルステータスの確認
  • getSwapResult による正確なスワップ見積もりの取得
  • applySlippage によるユーザーの保護
  • swapBondingCurveV2 での売買トランザクションの構築
  • カーブとグラデュエーション後のRaydiumプールからのクリエイター手数料の請求

Summary

ボンディングカーブのスワップはGenesis SDKを使用して BondingCurveBucketV2 オンチェーンアカウントを操作します — SOLを受け取りトークンを返す(購入)、またはトークンを受け取りSOLを返す(売却)定積AMMです。価格計算の基礎数学については動作理論を参照してください。

  • 送信前に見積もりを取得getSwapResult で正確な手数料調整後の入出力金額を取得する
  • スリッページ保護applySlippageminAmountOutScaled を導出し、インストラクションに渡す
  • wSOLは手動 — スワップインストラクションはネイティブSOLをラップ・アンラップしない。呼び出し元が自分でwSOL ATAを処理する必要がある
  • プログラムID — Solanaメインネット上の GNS1S5J5AspKXgpjz6SvKL66kPaKWAhaGRhCqPRxii2B

クイックスタート

ジャンプ: インストール · セットアップ · カーブの取得 · ライフサイクルヘルパー · 見積もり · スリッページ · スワップの実行 · クリエイター手数料 · エラー · APIリファレンス

  1. パッケージをインストールして genesis() プラグインでUmiインスタンスを設定する
  2. BondingCurveBucketV2Pda を導出してアカウントを取得する
  3. isSwappable(bucket) を確認 — falseの場合は中止する
  4. getSwapResult(bucket, amountIn, SwapDirection.Buy) で手数料調整済み見積もりを取得する
  5. applySlippage(quote.amountOut, slippageBps) を適用して minAmountOutScaled を取得する
  6. wSOLのラッピングを手動で処理してから swapBondingCurveV2 を送信して確認する

前提条件

  • Node.js 18+ — ネイティブBigIntサポートに必要
  • Solanaウォレット — トランザクション手数料とスワップ入力のためのSOLが入金されている
  • SolanaのRPCエンドポイント(mainnet-betaまたはdevnet)
  • Umiフレームワークとasync/awaitパターンへの慣れ

テスト済み構成

ツールバージョン
@metaplex-foundation/genesis1.x
@metaplex-foundation/umi1.x
@metaplex-foundation/umi-bundle-defaults1.x
Node.js18+

インストール

Terminal
npm install @metaplex-foundation/genesis \
@metaplex-foundation/umi \
@metaplex-foundation/umi-bundle-defaults

UmiとGenesisプラグインのセットアップ

SDK関数を呼び出す前に、Umiインスタンスを設定して genesis() プラグインを登録します。

setup.ts
1import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
2import { genesis } from '@metaplex-foundation/genesis';
3import { keypairIdentity } from '@metaplex-foundation/umi';
4import { readFileSync } from 'fs';
5
6const keypairFile = JSON.parse(readFileSync('/path/to/keypair.json', 'utf-8'));
7
8const umi = createUmi('https://api.mainnet-beta.solana.com')
9 .use(genesis());
10
11const keypair = umi.eddsa.createKeypairFromSecretKey(Uint8Array.from(keypairFile));
12umi.use(keypairIdentity(keypair));

ボンディングカーブBucketV2の取得

すでに知っていることに応じて、3つの検出戦略が利用可能です。

既知のGenesisアカウントから取得する

1import {
2 findBondingCurveBucketV2Pda,
3 fetchBondingCurveBucketV2,
4 genesis,
5} from '@metaplex-foundation/genesis';
6import { publicKey } from '@metaplex-foundation/umi';
7import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
8
9const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis());
10
11const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT_PUBKEY');
12
13const [bucketPda] = findBondingCurveBucketV2Pda(umi, {
14 genesisAccount,
15 bucketIndex: 0,
16});
17
18const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);

トークンミントから取得する

fetch-from-mint.ts
1import {
2 findGenesisAccountV2Pda,
3 findBondingCurveBucketV2Pda,
4 fetchBondingCurveBucketV2,
5} from '@metaplex-foundation/genesis';
6import { publicKey } from '@metaplex-foundation/umi';
7
8const baseMint = publicKey('TOKEN_MINT_PUBKEY');
9
10const [genesisAccount] = findGenesisAccountV2Pda(umi, {
11 baseMint,
12 genesisIndex: 0,
13});
14
15const [bucketPda] = findBondingCurveBucketV2Pda(umi, {
16 genesisAccount,
17 bucketIndex: 0,
18});
19
20const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);

ボンディングカーブBucketV2の状態を読み取る

フィールド説明
baseTokenBalancebigintカーブに残っているトークン。ゼロは売り切れを意味する。
baseTokenAllocationbigint作成時にこのカーブに割り当てられた総トークン数。
quoteTokenDepositTotalbigint購入者が入金した実際のSOL(ラムポート)。0から始まる。
virtualSolbigint初期化時に追加された仮想SOLリザーブ(価格計算のみ)。
virtualTokensbigint初期化時に追加された仮想トークンリザーブ(価格計算のみ)。
depositFeenumberすべてのスワップのSOL側に適用されるプロトコル手数料率。
withdrawFeenumber売却のSOL出力側に適用されるプロトコル手数料率。
creatorFeeAccruedbigint最後の請求以降に蓄積したクリエイター手数料(ラムポート)。
creatorFeeClaimedbigint今日までの累計請求済みクリエイター手数料(ラムポート)。
swapStartConditionobject取引が許可される前に満たされなければならない条件。
swapEndConditionobjectトリガーされると取引を終了させる条件。

virtualSolvirtualTokens は価格計算にのみ存在します — 実際にはオンチェーンにリアルアセットとして入金されません。仮想リザーブが定積カーブをどのように形成するかについては動作理論を参照してください。

ボンディングカーブライフサイクルヘルパー

5つのヘルパー関数が追加のRPC呼び出しなしにカーブの状態を確認します(isGraduated を除く)。

lifecycle-helpers.ts
1import {
2 isSwappable,
3 isFirstBuyPending,
4 isSoldOut,
5 getFillPercentage,
6 isGraduated,
7} from '@metaplex-foundation/genesis';
8
9const canSwap = isSwappable(bucket);
10const firstBuyPending = isFirstBuyPending(bucket);
11const soldOut = isSoldOut(bucket);
12const fillPercent = getFillPercentage(bucket);
13const graduated = await isGraduated(umi, bucket); // 非同期RPC呼び出し
ヘルパー非同期戻り値説明
isSwappable(bucket)いいえbooleanパブリックトレードを受け付けているとき true
isFirstBuyPending(bucket)いいえboolean指定されたファーストバイがまだ完了していないとき true
isSoldOut(bucket)いいえbooleanbaseTokenBalance === 0n のとき true
getFillPercentage(bucket)いいえnumber売却済み割り当ての0〜100パーセント
isGraduated(umi, bucket)はいbooleanRaydium CPMMプールがオンチェーンに存在するとき true

スワップ見積もりの取得

getSwapResult(bucket, amountIn, swapDirection, isFirstBuy?) はトランザクションを送信せずに、スワップの正確な手数料調整済み金額を計算します。

{ amountIn, fee, creatorFee, amountOut } を返します:

  • amountIn — 調整後の実際の入力金額
  • fee — 請求されるプロトコル手数料(ラムポート単位)
  • creatorFee — 請求されるクリエイター手数料(ラムポート単位、クリエイター手数料未設定の場合は0)
  • amountOut — 受け取るトークン(購入)またはSOL(売却)

購入見積もり(SOLからトークン)

1import {
2 genesis,
3 findBondingCurveBucketV2Pda,
4 fetchBondingCurveBucketV2,
5 getSwapResult,
6} from '@metaplex-foundation/genesis';
7import { publicKey } from '@metaplex-foundation/umi';
8import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
9
10const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis());
11
12const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT_PUBKEY');
13const [bucketPda] = findBondingCurveBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 });
14const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);
15
16const SOL_IN = 1_000_000_000n; // 1 SOL in lamports
17
18const buyQuote = getSwapResult(bucket, SOL_IN, 'buy');
19
20console.log('SOL input: ', buyQuote.amountIn.toString(), 'lamports');
21console.log('Total fee: ', buyQuote.fee.toString(), 'lamports');
22console.log('Tokens out: ', buyQuote.amountOut.toString());
23
24// SOL input: 1000000000 lamports
25// Total fee: 10000000 lamports
26// Tokens out: <calculated token amount>

売却見積もり(トークンからSOL)

1import {
2 genesis,
3 findBondingCurveBucketV2Pda,
4 fetchBondingCurveBucketV2,
5 getSwapResult,
6} from '@metaplex-foundation/genesis';
7import { publicKey } from '@metaplex-foundation/umi';
8import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
9
10const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis());
11
12const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT_PUBKEY');
13const [bucketPda] = findBondingCurveBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 });
14const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);
15
16const TOKENS_IN = 500_000_000_000n; // 500 tokens (9 decimals)
17
18const sellQuote = getSwapResult(bucket, TOKENS_IN, 'sell');
19
20console.log('Tokens input: ', sellQuote.amountIn.toString());
21console.log('Total fee: ', sellQuote.fee.toString(), 'lamports');
22console.log('SOL out: ', sellQuote.amountOut.toString(), 'lamports');
23
24// Tokens input: 500000000000
25// Total fee: <fee in lamports>
26// SOL out: <calculated SOL amount>

ファーストバイ手数料免除

手数料を免除したファーストバイを見積もるには、4番目の引数として true を渡します:

first-buy-quote.ts
1const firstBuyQuote = getSwapResult(bucket, SOL_IN, SwapDirection.Buy, true);
2console.log('Fee (waived): ', firstBuyQuote.fee.toString()); // 0n

現在の価格ヘルパー

current-price.ts
1import {
2 getCurrentPrice,
3 getCurrentPriceQuotePerBase,
4 getCurrentPriceComponents,
5} from '@metaplex-foundation/genesis';
6
7const tokensPerSol = getCurrentPrice(bucket); // bigint
8const lamportsPerToken = getCurrentPriceQuotePerBase(bucket); // bigint
9const { baseReserves, quoteReserves } = getCurrentPriceComponents(bucket);

スリッページ保護

applySlippage(expectedAmountOut, slippageBps) は期待される出力をスリッページ許容範囲で減らします。結果を minAmountOutScaled としてスワップインストラクションに渡します — オンチェーンプログラムは実際の出力がこの値を下回った場合にトランザクションを拒否します。

slippage.ts
1import { getSwapResult, applySlippage, SwapDirection } from '@metaplex-foundation/genesis';
2
3const quote = getSwapResult(bucket, 1_000_000_000n, SwapDirection.Buy);
4const minAmountOutScaled = applySlippage(quote.amountOut, 100); // 1%スリッページ

applySlippage から導出した minAmountOutScaled なしでスワップを送信しないでください。ボンディングカーブの価格はすべての取引で変化します。スリッページ保護なしでは、ユーザーが見積もりより大幅に少ないトークンを受け取る可能性があります。

一般的な値:安定した条件では50 bps(0.5%)、ボラティリティの高いローンチ中は200 bps(2%)。

スワップトランザクションの構築

swapBondingCurveV2(umi, accounts) はスワップインストラクションを構築します。呼び出し元はトランザクションの前後でラップドSOL(wSOL)の処理を担当します。

購入トランザクション(SOLからトークン)

1import {
2 genesis,
3 findBondingCurveBucketV2Pda,
4 fetchBondingCurveBucketV2,
5 getSwapResult,
6 applySlippage,
7 swapBondingCurveV2,
8 isSwappable,
9} from '@metaplex-foundation/genesis';
10import { findAssociatedTokenPda } from '@metaplex-foundation/mpl-toolbox';
11import { publicKey } from '@metaplex-foundation/umi';
12import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
13
14const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis());
15
16const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT_PUBKEY');
17const baseMint = publicKey('TOKEN_MINT_PUBKEY');
18const quoteMint = publicKey('So11111111111111111111111111111111111111112'); // wSOL
19
20const [bucketPda] = findBondingCurveBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 });
21const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);
22
23if (!isSwappable(bucket)) throw new Error('Curve is not currently accepting swaps');
24
25const SOL_IN = 1_000_000_000n; // 1 SOL in lamports
26const quote = getSwapResult(bucket, SOL_IN, 'buy');
27const minAmountOut = applySlippage(quote.amountOut, 100); // 1% slippage
28
29const [userBaseTokenAccount] = findAssociatedTokenPda(umi, { mint: baseMint, owner: umi.identity.publicKey });
30const [userQuoteTokenAccount] = findAssociatedTokenPda(umi, { mint: quoteMint, owner: umi.identity.publicKey });
31
32// NOTE: Fund the wSOL ATA before this call — see the wSOL Wrapping Note on this page.
33const result = await swapBondingCurveV2(umi, {
34 genesisAccount,
35 bucketPda,
36 baseMint,
37 quoteMint,
38 userBaseTokenAccount,
39 userQuoteTokenAccount,
40 amountIn: quote.amountIn,
41 minAmountOut,
42 direction: 'buy',
43}).sendAndConfirm(umi);
44
45console.log('Buy confirmed:', result.signature);
46
47// Buy confirmed: <base58 transaction signature>

売却トランザクション(トークンからSOL)

1import {
2 genesis,
3 findBondingCurveBucketV2Pda,
4 fetchBondingCurveBucketV2,
5 getSwapResult,
6 applySlippage,
7 swapBondingCurveV2,
8} from '@metaplex-foundation/genesis';
9import { findAssociatedTokenPda } from '@metaplex-foundation/mpl-toolbox';
10import { publicKey } from '@metaplex-foundation/umi';
11import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
12
13const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis());
14
15const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT_PUBKEY');
16const baseMint = publicKey('TOKEN_MINT_PUBKEY');
17const quoteMint = publicKey('So11111111111111111111111111111111111111112'); // wSOL
18
19const [bucketPda] = findBondingCurveBucketV2Pda(umi, { genesisAccount, bucketIndex: 0 });
20const bucket = await fetchBondingCurveBucketV2(umi, bucketPda);
21
22const [userBaseTokenAccount] = findAssociatedTokenPda(umi, { mint: baseMint, owner: umi.identity.publicKey });
23const [userQuoteTokenAccount] = findAssociatedTokenPda(umi, { mint: quoteMint, owner: umi.identity.publicKey });
24
25const TOKENS_IN = 500_000_000_000n; // 500 tokens (9 decimals)
26const quote = getSwapResult(bucket, TOKENS_IN, 'sell');
27const minAmountOut = applySlippage(quote.amountOut, 100); // 1% slippage
28
29const result = await swapBondingCurveV2(umi, {
30 genesisAccount,
31 bucketPda,
32 baseMint,
33 quoteMint,
34 userBaseTokenAccount,
35 userQuoteTokenAccount,
36 amountIn: quote.amountIn,
37 minAmountOut,
38 direction: 'sell',
39}).sendAndConfirm(umi);
40
41// NOTE: Close the wSOL ATA after confirming to unwrap back to native SOL.
42console.log('Sell confirmed:', result.signature);
43
44// Sell confirmed: <base58 transaction signature>

wSOLラッピングに関する注意

wSOLの手動処理が必要

swapBondingCurveV2 はクォートトークンとしてラップドSOL(wSOL)を使用し、ネイティブSOLを自動的にラップ・アンラップしません

購入の場合: wSOL ATAを作成し、必要なラムポートを転送して、スワップを送信する前に syncNative を呼び出してください。

売却の場合: スワップが確認された後にwSOL ATAを閉じてネイティブSOLにアンラップしてください。

現在のバージョンでは、クォートトークンとしてwSOLのみが受け入れられます。

wsol-wrap-unwrap.ts
1import {
2 findAssociatedTokenPda,
3 createAssociatedTokenAccountIdempotentInstruction,
4 syncNative,
5 closeToken,
6} from '@metaplex-foundation/mpl-toolbox';
7import { transactionBuilder, sol, publicKey } from '@metaplex-foundation/umi';
8
9const wSOL = publicKey('So11111111111111111111111111111111111111112');
10const [wSolAta] = findAssociatedTokenPda(umi, { mint: wSOL, owner: umi.identity.publicKey });
11
12// --- 購入前にSOLをラップする ---
13const wrapBuilder = transactionBuilder()
14 .add(createAssociatedTokenAccountIdempotentInstruction(umi, {
15 mint: wSOL,
16 owner: umi.identity.publicKey,
17 }))
18 .add(syncNative(umi, { account: wSolAta }));
19
20await wrapBuilder.sendAndConfirm(umi);
21
22// --- 売却後にSOLをアンラップする ---
23const unwrapBuilder = closeToken(umi, {
24 account: wSolAta,
25 destination: umi.identity.publicKey,
26 authority: umi.identity,
27});
28
29await unwrapBuilder.sendAndConfirm(umi);

クリエイター手数料の請求

クリエイター手数料はスワップごとに直接転送されるのではなく、バケット(creatorFeeAccrued)に蓄積されます。カーブがアクティブな間はパーミッションレスの claimBondingCurveCreatorFeeV2 インストラクションで、グラデュエーション後は claimRaydiumCreatorFeeV2 で回収します。

蓄積残高の確認方法やグラデュエーション後のRaydium LP手数料の処理を含む完全な請求フローについては、クリエイター手数料を参照してください。

エラーハンドリング

エラー原因対処法
BondingCurveInsufficientFunds残りのトークン(購入)またはSOL(売却)が不足しているバケットを再取得して再見積もりする。カーブがほぼ売り切れの可能性がある
InsufficientOutputAmount実際の出力が minAmountOutScaled を下回ったslippageBps を増やすか、すぐに再試行する
InvalidSwapDirectionswapDirection の値が無効@metaplex-foundation/genesis インポートから SwapDirection.Buy または SwapDirection.Sell を渡す
BondingCurveNotStartedswapStartCondition がまだ満たされていないbucket.swapStartCondition を確認して待つ
BondingCurveEndedカーブが売り切れまたはグラデュエーション済みユーザーをRaydium CPMMプールに誘導する
error-handling.ts
1async function executeBuy(bucket, amountIn: bigint, slippageBps: number) {
2 if (!isSwappable(bucket)) {
3 if (isSoldOut(bucket)) throw new Error('Token sold out. Trade on Raydium.');
4 throw new Error('Curve not yet active. Check the start time.');
5 }
6
7 const quote = getSwapResult(bucket, amountIn, SwapDirection.Buy);
8 const minAmountOutScaled = applySlippage(quote.amountOut, slippageBps);
9
10 try {
11 return await swapBondingCurveV2(umi, {
12 amount: quote.amountIn,
13 minAmountOutScaled,
14 swapDirection: SwapDirection.Buy,
15 // ... アカウント
16 }).sendAndConfirm(umi);
17 } catch (err: any) {
18 if (err.message?.includes('InsufficientOutputAmount'))
19 throw new Error('Price moved. Try again with higher slippage.');
20 if (err.message?.includes('BondingCurveInsufficientFunds'))
21 throw new Error('Not enough tokens remaining. Reduce amount.');
22 throw err;
23 }
24}

Notes

  • 本番環境ではスワップごとにバケットを再取得してください — 他のユーザーの取引ごとに価格が変わります
  • virtualSolvirtualTokens はカーブ作成後は不変です — キャッシュしてください。リアルリザーブフィールドのみスワップごとに変化します
  • isGraduated は呼び出しのたびにRPC呼び出しを行います — インデクサーで結果をキャッシュしてください
  • isSoldOuttrue を返してから isGraduatedtrue を返すまでの間、カーブは売り切れてもRaydiumはまだ資金調達されていません。isGraduated がプールの存在を確認するまでユーザーをRaydiumに誘導しないでください
  • イベントのデコードとライフサイクルのインデックス作成についてはインデックス作成とイベントを参照してください
  • すべての手数料金額はラムポート(SOL側)です。現在の手数料率についてはプロトコル手数料を参照してください

APIリファレンス

見積もりと価格関数

関数非同期戻り値説明
getSwapResult(bucket, amountIn, swapDirection, isFirstBuy?)いいえ{ amountIn, fee, creatorFee, amountOut }手数料調整済みスワップ見積もり
getCurrentPrice(bucket)いいえbigintSOL単位あたりのベーストークン(整数除算)
getCurrentPriceQuotePerBase(bucket)いいえbigintベーストークン単位あたりのラムポート(整数除算)
getCurrentPriceComponents(bucket)いいえ{ baseReserves, quoteReserves }bigintとして結合した仮想+リアルリザーブ

ライフサイクル関数

関数非同期戻り値説明
isSwappable(bucket)いいえbooleanパブリックトレードを受け付けているとき true
isFirstBuyPending(bucket)いいえboolean指定されたファーストバイがまだ完了していないとき true
isSoldOut(bucket)いいえbooleanbaseTokenBalance === 0n のとき true
getFillPercentage(bucket)いいえnumber売却済み割り当ての0〜100パーセント
isGraduated(umi, bucket)はいbooleanRaydium CPMMプールがオンチェーンに存在するとき true

スリッページ

関数戻り値説明
applySlippage(amountOut, slippageBps)bigintamountOutslippageBps / 10_000 で減らす

スワップインストラクションアカウント

アカウント書き込み可能署名者説明
genesisAccountはいいいえGenesis調整PDA
bucketはいいいえBondingCurveBucketV2 PDA
baseMintいいえいいえSPLトークンミント
quoteMintいいえいいえwSOLミント
baseTokenAccountはいいいえユーザーのベーストークンATA
quoteTokenAccountはいいいえユーザーのwSOL ATA
payerはいはいトランザクション手数料の支払者

アカウント検出

関数戻り値説明
findBondingCurveBucketV2Pda(umi, { genesisAccount, bucketIndex })[PublicKey, bump]バケットPDAを導出する
findGenesisAccountV2Pda(umi, { baseMint, genesisIndex })[PublicKey, bump]genesisアカウントPDAを導出する
fetchBondingCurveBucketV2(umi, pda)BondingCurveBucketV2アカウントを取得してデシリアライズする

FAQ

isSwappableとisSoldOutの違いは何ですか?

isSwappable はカーブがアクティブにパブリックトレードを受け付けているときのみ true を返します。isSoldOutbaseTokenBalance がゼロになった瞬間に true を返し、取引が終了してグラデュエーションがトリガーされます。カーブは売り切れでもまだグラデュエーションしていない場合があります。

swapBondingCurveV2を呼び出す前にSOLをラップする必要がありますか?

はい。ボンディングカーブはクォートトークンとしてwSOLを使用し、swapBondingCurveV2 はネイティブSOLを自動的にラップ・アンラップしません。wSOLラッピングに関する注意を参照してください。

getSwapResultは何を返し、手数料はどのように処理されますか?

getSwapResult{ amountIn, fee, creatorFee, amountOut } を返します。購入の場合、手数料はAMM公式が実行される前にSOL入力から差し引かれます。売却の場合、手数料はAMM実行後にSOL出力から差し引かれます。ファーストバイ手数料免除のシミュレーション(すべての手数料をゼロにする)には、4番目の引数として true を渡してください。

スリッページから保護するにはどうすればよいですか?

applySlippage(quote.amountOut, slippageBps) を呼び出して minAmountOutScaled を導出し、swapBondingCurveV2minAmountOutScaled フィールドとして渡してください。オンチェーンプログラムは実際の出力がこの値を下回った場合にトランザクションを拒否します。