Claim and split partner fees

Claim and split partner fees

Once your pool has traded a bit, partner fees accrue to the pool config's partner account. This recipe shows how to read the balance, decide on a split, and atomically claim plus disburse to up to ten recipients in a single transaction.

The big idea: instead of claiming and then sending separate transfers (three round-trips, three failure modes, three ways for someone to grief you mid-flow), a single fee-claim call with kind: "split" builds one VersionedTransaction that does the claim and all the SPL transfers together.

DBC vs DAMM v2 — pick the right kind

All four claim variants live behind one route: POST /v2/pools/{address}/fee-claims, with a kind discriminator selecting the variant.

StateTrading onkind
Pre-migration (bonding curve still active)DBC"dbc"
Post-migration (graduated to constant product)DAMM v2"damm_v2"
Post-migration leftover quote reserves(either)"surplus"
Atomic claim + N-way disbursement(either)"split"

kind: "split" works on both DBC and DAMM v2 pools — the server inspects the pool address and routes the claim to the correct underlying program. If you want to claim without splitting, pick dbc or damm_v2 yourself.

The one-shot surplus only exists immediately post-migration if not all reserves were LP'd. Withdraw it once with kind: "surplus". See Fees for the full lifecycle.

For this recipe we'll assume a still-bonding pool with non-zero partner fees and a 3-way split.

Prerequisites

  • The pool address.
  • A wallet that can pay the transaction fee (payer). It does not have to be the pool creator.
  • Each split recipient must already have an Associated Token Account (ATA) for the quote mint — GOLD, GoLDDqDRHcGZBiGPeXAYi5ougndqBNQSNXdNeT3re6gr. The split tx does not create ATAs for you; pre-create any missing ones with a separate tx before calling split, or the transaction will fail at simulate time.
export PIRATE_API_KEY="pk_live_..."
export POOL_ADDRESS="POOL_ADDRESS"
export PAYER="YOUR_WALLET_PUBKEY"

1. Read what's claimable

curl https://api.piratecrew.fun/v2/pools/$POOL_ADDRESS/fee-metrics \
  -H "Authorization: Bearer $PIRATE_API_KEY"
{
  "data": {
    "pool_address": "POOL_ADDRESS",
    "partner_quote_fee": "184523000",
    "partner_base_fee":  "0",
    "surplus":           "0",
    "migrated": false
  },
  "meta": { "request_id": "req_…" }
}

partner_quote_fee is in raw token units of GOLD (six decimals, so divide by 1e6 for display). migrated: false confirms you're still on the DBC side. If it were true, claim with kind: "damm_v2" — the request and response shapes are identical to kind: "dbc".

If partner_quote_fee is "0", there's nothing to claim — stop here.

2. Decide on the splits

Splits use basis points (1 bp = 0.01%) and must sum to exactly 10000. Between 2 and 10 recipients per call — if you only have one recipient, claim with kind: "dbc" or kind: "damm_v2" instead.

A common 3-way split:

RecipientbpsShare
Creator500050%
KOL300030%
Treasury200020%
Total10000100%

3. Build the split transaction

curl -X POST https://api.piratecrew.fun/v2/pools/$POOL_ADDRESS/fee-claims \
  -H "Authorization: Bearer $PIRATE_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "split",
    "payer": "YOUR_WALLET_PUBKEY",
    "splits": [
      { "recipient": "CREATOR_PUBKEY",  "bps": 5000 },
      { "recipient": "KOL_PUBKEY",      "bps": 3000 },
      { "recipient": "TREASURY_PUBKEY", "bps": 2000 }
    ],
    "mode": "unsigned"
  }'
{
  "data": {
    "mode": "unsigned",
    "transaction": "AQABAo...",
    "pool_address": "POOL_ADDRESS",
    "estimated_quote": "184523000",
    "splits": [
      { "recipient": "CREATOR_PUBKEY",  "bps": 5000, "estimated_amount": "92261500" },
      { "recipient": "KOL_PUBKEY",      "bps": 3000, "estimated_amount": "55356900" },
      { "recipient": "TREASURY_PUBKEY", "bps": 2000, "estimated_amount": "36904600" }
    ]
  },
  "meta": { "request_id": "req_…" }
}

estimated_amount is the raw-units amount each recipient will receive assuming the on-chain balance doesn't change between build and submit. Treat it as a preview, not a guarantee — if more trades happen before your tx lands, the actual amounts move proportionally.

4. Sign and submit

The response is a base64 VersionedTransaction. Sign locally with the payer's keypair and submit:

import { Keypair, VersionedTransaction } from "@solana/web3.js";
import bs58 from "bs58";

const payer = Keypair.fromSecretKey(bs58.decode(process.env.WALLET_SECRET_KEY!));
const tx = VersionedTransaction.deserialize(Buffer.from(response.transaction, "base64"));
tx.sign([payer]);
const signed = Buffer.from(tx.serialize()).toString("base64");
curl -X POST https://api.piratecrew.fun/v2/transactions \
  -H "Authorization: Bearer $PIRATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "transaction": "<base64 signed tx>" }'
{
  "data": { "signature": "5kJ2...", "lifecycle": "submitted" },
  "meta": { "request_id": "req_…" }
}

5. Poll for confirmation

curl https://api.piratecrew.fun/v2/transactions/$SIGNATURE \
  -H "Authorization: Bearer $PIRATE_API_KEY"
{
  "data": {
    "signature": "5kJ2...",
    "lifecycle": "confirmed",
    "confirmation_status": "confirmed",
    "err": null,
    "slot": 271234567
  },
  "meta": { "request_id": "req_…" }
}

Loop until lifecycle is "confirmed" (or "finalized" if you need stronger guarantees). After confirmation, re-read GET /v2/pools/$POOL_ADDRESS/fee-metricspartner_quote_fee should be back to 0.

When to use mode: "signed"

If your payer is a Privy-managed wallet on our platform, you can skip the local signing entirely:

curl -X POST https://api.piratecrew.fun/v2/pools/$POOL_ADDRESS/fee-claims \
  -H "Authorization: Bearer $PIRATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "split",
    "payer": "PRIVY_WALLET_PUBKEY",
    "splits": [ ... ],
    "mode": "signed"
  }'
{
  "data": { "mode": "signed", "signature": "5kJ2..." },
  "meta": { "request_id": "req_…" }
}

The signature is already confirmed at confirmed commitment when it lands in the response. This path requires the fees:claim scope on your API key — without it you get 403 missing_scope. For first-party integrations where you control the payer wallet, stick with mode: "unsigned"; it's simpler and doesn't need the scope.

Gotchas

  • ATAs must exist. The split tx does not include create-ATA instructions for the recipients — it'd otherwise blow past compute and account-key limits. If any recipient is missing the GOLD ATA, the tx fails at simulate. Pre-create them in a separate batch, or have each recipient open the GOLD account themselves once.
  • bps sum must equal 10000. Off-by-one errors here are common — the endpoint returns 400 invalid_split_bps if the sum is wrong. Split rounding (e.g. 33/33/34) is fine as long as the sum is exact.
  • Idempotency. Replay-safe via Idempotency-Key. If the network drops your response, retry with the same key and you'll get the same transaction back — never two claims on the same balance.
  • Migrated pool, DBC kind. Calling fee-claims with kind: "dbc" on a migrated pool returns 400 pool_migrated. Switch to kind: "damm_v2". kind: "split" handles both automatically.
  • Race with trading. Between build and submit, trades can push partner_quote_fee up or down. The on-chain instruction always uses the live balance, so estimated_amount is approximate.

Next steps

  • Fees — the full reference, including surplus withdrawal and DAMM v2 specifics.
  • Run a merkle airdrop end-to-end — distribute claimed fees to thousands of wallets with one merkle root.
  • Transaction Modes — when to use signed vs unsigned and how scopes gate it.