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.
| State | Trading on | kind |
|---|---|---|
| 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:
| Recipient | bps | Share |
|---|---|---|
| Creator | 5000 | 50% |
| KOL | 3000 | 30% |
| Treasury | 2000 | 20% |
| Total | 10000 | 100% |
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-metrics — partner_quote_fee should be back to 0.
When to use mode: "signed"
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-ATAinstructions 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_bpsif 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 returns400 pool_migrated. Switch tokind: "damm_v2".kind: "split"handles both automatically. - Race with trading. Between build and submit, trades can push
partner_quote_feeup or down. The on-chain instruction always uses the live balance, soestimated_amountis 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
signedvsunsignedand how scopes gate it.