@piratecrewfun/pirate-sdk
v2.0.0
Published
TypeScript SDK for the PirateCrew Blockchain API (v2). Noun-based REST, snake_case wire shapes, cursor pagination, polymorphic resources, typed errors, webhook signature verification.
Readme
@piratecrewfun/pirate-sdk
TypeScript client for the PirateCrew Blockchain API (v2). Noun-based REST, response/error envelopes, cursor pagination, polymorphic resources, typed errors, and a webhook signature verifier. Talks to api.piratecrew.fun; full reference at docs.piratecrew.fun.
Heads-up: 2.0 is a breaking rewrite. v1.x (
/v1/*) is no longer wrapped. See Migrating from 1.x below.
Install
npm install @piratecrewfun/pirate-sdkNo peer dependencies. Pure ESM, Node 20+.
Quick start
import { PirateCrew } from "@piratecrewfun/pirate-sdk";
const pc = new PirateCrew({ apiKey: process.env.PIRATE_API_KEY! });
// Fetch a pool — polymorphic across DBC and DAMM v2; check `pool.type`.
const pool = await pc.pools.get("7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU");
// Cursor-paginate every airdrop, transparently following next_cursor.
for await (const airdrop of pc.airdrops.iterate({ limit: 50 })) {
console.log(airdrop.id, airdrop.status);
}
// Build an unsigned claim tx for an end-user wallet.
const claim = await pc.airdrops.claims.create("N3hL…AsU:0", {
claimer: "Bw5K…fG2",
platform_id: "main",
amount: "1000000000",
platform_fee: 0,
swap_to_sol: false,
mode: "unsigned",
});
// claim.transaction is a base64 VersionedTransaction — sign client-side, then:
// await pc.transactions.submit({ transaction: signedBase64, bundle: false });Options
new PirateCrew({
apiKey: "pk_live_…", // required
baseUrl: "https://api.piratecrew.fun", // override for staging / local
timeout: 30_000, // ms, default 30s
fetch: globalThis.fetch, // inject a custom fetch
defaultHeaders: { "X-Tenant": "acme" }, // applied to every request
});Namespaces
| Namespace | Endpoints |
|---|---|
| pc.pools | list / iterate / create / get / verify / curveProgress / feeMetrics / createFeeClaim / listFeeClaims / iterateFeeClaims |
| pc.airdrops | list / iterate / create / get + .claims.create + .vaultWithdrawals.create |
| pc.merkleTrees | create / getProof |
| pc.stakes | create / release / get |
| pc.nfts | create / burn |
| pc.goldLocks | create / release |
| pc.tokens | create / get + .authorities.revoke(mint, "mint" \| "freeze", …) |
| pc.transactions | submit / get + .simulations.create |
| pc.swaps | quote / submit |
| pc.pdas | platform / treasury / airdropConfig / claimAirdrop / userNftConfig / goldLockConfig / userLockConfig / goldVault / veGoldMint / userVeGold |
| pc.accounts | get / batchFetch |
| pc.webhookSubscriptions | list / iterate / create / get / update / delete / test / eventTypes |
Every method returns the unwrapped data field from the response envelope. The meta field (for request_id, external_uid, metadata) is currently exposed only on list() (via ListPage). Reach into pc.client for raw { data, meta } access if you need the request id.
Snake_case + u64-safe amounts
All wire shapes use snake_case, mirroring the OpenAPI spec verbatim:
await pc.swaps.quote({
input_mint: "So11111111111111111111111111111111111111112",
output_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
amount: "1000000000", // string — u64-safe, may exceed Number.MAX_SAFE_INTEGER
slippage_bps: 50,
swap_mode: "ExactIn",
});Token / lamport quantities (amount, lamports, partner_quote_fee, …) are decimal strings, not numbers — they routinely exceed Number.MAX_SAFE_INTEGER.
Idempotency, request IDs, metadata
Every write method accepts an optional extras argument:
await pc.tokens.create(
{ payer, name, symbol, decimals: 6, initial_supply: "1000000000000", uri, revoke_mint_authority: false, revoke_freeze_authority: true, mode: "unsigned" },
{
idempotencyKey: crypto.randomUUID(), // → Idempotency-Key header
requestId: "req_my_correlation_id", // → X-Request-Id header
metadata: { campaign: "q2_2026", batch: "42" }, // → echoed on response + webhook events
externalUid: "tx_abc123", // legacy field; prefer `metadata`
signal: controller.signal, // AbortSignal
}
);Pagination
Two flavors. Use list() for one page of control; use iterate() to stream every page transparently:
// Manual:
const { data, meta } = await pc.pools.list({ limit: 25, type: "dbc" });
if (meta.has_more) {
const next = await pc.pools.list({ limit: 25, type: "dbc", cursor: meta.next_cursor! });
}
// Auto-paginating async iterator:
for await (const pool of pc.pools.iterate({ type: "dbc" })) {
console.log(pool.address, pool.status);
}Errors
Non-2xx responses throw a typed PirateApiError with the full v2 error envelope flattened onto the instance:
import { PirateCrew, PirateApiError, PirateNetworkError } from "@piratecrewfun/pirate-sdk";
try {
await pc.pools.get("11111111111111111111111111111111");
} catch (err) {
if (err instanceof PirateApiError) {
console.error(err.status); // 403
console.error(err.code); // "forbidden"
console.error(err.message); // "Invalid or revoked API key"
console.error(err.param); // optional offending field
console.error(err.doc_url); // optional link to docs
console.error(err.request_id); // server-generated id — quote in bug reports
} else if (err instanceof PirateNetworkError) {
// DNS, socket, timeout — server never saw the request
}
}Webhook signature verification
The PirateCrew API signs every webhook delivery with HMAC-SHA256 and ships the result in X-Pirate-Signature: t=<unix>,v1=<hex>. The signing string is ${timestamp}.${rawBody}.
import express from "express";
import { verifyWebhookSignature } from "@piratecrewfun/pirate-sdk";
const app = express();
app.post(
"/webhooks/piratecrew",
express.raw({ type: "application/json" }),
(req, res) => {
const ok = verifyWebhookSignature({
rawBody: req.body, // Buffer from express.raw
header: req.header("X-Pirate-Signature")!,
secret: process.env.PIRATE_WEBHOOK_SECRET!,
// tolerance: 300, // seconds; default 300
});
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
// handle event …
res.status(204).end();
}
);verifyWebhookSignature performs a constant-time compare via crypto.timingSafeEqual and rejects timestamps older than tolerance seconds (replay protection). Returns false on mismatch / stale; throws on a malformed header.
Migrating from 1.x
1.x talked to /v1/* with hand-written request shapes. 2.x is a clean rewrite against /v2/* and is not API-compatible — but the underlying flows are the same, with cleaner names.
| 1.x call | 2.x equivalent |
|---|---|
| sdk.createCoin(...) (end-to-end helper) | pc.pools.create(...) → sign locally → pc.transactions.submit(...) → pc.pools.verify(address, ...) |
| sdk.pools.getState(addr) | pc.pools.get(addr) |
| sdk.pools.getCurveProgress(addr) | pc.pools.curveProgress(addr) |
| sdk.fees.claim({ kind: "dbc", ... }) | pc.pools.createFeeClaim(addr, { kind: "dbc", ... }) |
| sdk.fees.split({ poolAddress, ... }) | pc.pools.createFeeClaim(addr, { kind: "dbc", splits: [...] }) (atomic claim+split) |
| sdk.airdrops.initialize({...}) | pc.airdrops.create({...}) |
| sdk.airdrops.claim({...}) | pc.airdrops.claims.create(airdropId, {...}) |
| sdk.airdrops.withdrawUnclaimed({...}) | pc.airdrops.vaultWithdrawals.create(airdropId, {...}) |
| sdk.staking.stake(...) / unstake(...) | pc.stakes.create(...) / pc.stakes.release(asset, ...) |
| sdk.nfts.mint(...) / burn(...) | pc.nfts.create(...) / pc.nfts.burn(asset, ...) |
| sdk.gold.lock(...) / unlock(...) | pc.goldLocks.create(...) / pc.goldLocks.release(user, ...) |
| sdk.tokens.create(...) | pc.tokens.create(...) |
| sdk.tokens.revokeAuthority(mint, type) | pc.tokens.authorities.revoke(mint, type, ...) |
| sdk.swap.quote(...) / swap(...) | pc.swaps.quote(...) / pc.swaps.submit(...) |
| sdk.tx.submit(...) | pc.transactions.submit({ transaction, bundle: false }) |
| sdk.tx.submitJito(...) | pc.transactions.submit({ signed_transactions: [...], bundle: true }) |
| sdk.tx.simulate(...) | pc.transactions.simulations.create(...) |
| sdk.tx.status(sig) | pc.transactions.get(sig) |
| sdk.accounts.get(addr) / batch(addresses) | pc.accounts.get(addr) / batchFetch({ addresses }) |
| sdk.accounts.derivePda({ name, args }) | pc.pdas.<name>(args) — one method per PDA kind |
Other breaking changes
- Field casing: snake_case throughout (was mixed). Read straight from the OpenAPI spec.
- Amounts: token / lamport quantities are decimal strings (was
number). Don't coerce. - Responses: every method returns the unwrapped
data(was the raw HTTP body). Usepc.client.get/post/...for{ data, meta }if you needrequest_id. - Errors: throws
PirateApiError(wasAPIError). New fields:code,param,doc_url,request_id. - Wallet handling removed: 2.x doesn't take a
secretKey— the SDK no longer signs. Use any web3.js wallet to sign the base64 transactions returned bymode: "unsigned"endpoints, then callpc.transactions.submit(...). This is a deliberate decoupling — the SDK is now pure HTTP. - Network/RPC config removed: no
network,rpcUrl,commitment. The SDK never talks to Solana RPC directly anymore.
Reference
- API docs: docs.piratecrew.fun
- OpenAPI spec: api.piratecrew.fun/v2/openapi.json
- Get an API key: developer.piratecrew.fun
License
MIT
