image.png# staccpad × your launchpad — the synergy pitch
You said: "80% stacc 20% LP, you can launch via an API or agent, but for the first few I'ma lock it down so it doesn't get spammy."
Stacc said: "or, launch via staccpad APIs."
Here's why that's not a deflection — it's the missing primitive your flow already needs.
- Programmatic token launch — agent/API kicks it off, no UI hand-holding.
- Configurable revenue split — e.g. 80% creator (you/stacc), 20% LP. Could just as easily be:
- X% to a marketing wallet that auto-buys the coin
- X% to a MM bot
- X% to LP top-ups
- X% to a treasury / charity / burn / whatever
- Spam control — locked down at first, then opened up without a rewrite.
Three real problems. None of them are "deploy SPL + seed pool" — that's the easy part. The hard part is routing the ongoing fee stream and gating who can call this.
5-tx Jito bundle, builds the DBC pool + initial creator buy + metadata + first-block snipe-protection in one shot. You sign once, we land or fail-atomic. No "did the pool create but the buy fail" half-states.
You don't have to write the bundle plumbing. You don't have to think about Jito tip placement, ALT lookups, or compute-unit pricing. You POST a JSON payload, you get back a base64 tx, you sign, you land.
This is the one that actually solves your revenue-split problem.
POST /api/launch-path/user/init
{
"wallet": "<creator>",
"quoteMint": "<USDC | SOL | $STACCPAD | $YOURTOKEN>",
"partnerWallet": "<where the 80% goes>",
"creatorPercent": 20, // the 20% LP-side / creator-side
"mcapAtMint": 10000 // USD target at config mint time
}Returns a configKey that's now mint-able against. Every token launched against that config routes the partner fee (a Meteora-native, on-chain fee leg) to your partnerWallet — forever, without any custodial trust. It's enforced at the curve level.
That's your 80/20 split. It's not us holding funds. It's not a multisig you have to babysit. It's a DBC config field that Meteora's program itself honours every swap.
Every launch under your config accrues partner fees. We expose:
- a list endpoint (which pools have how much claimable, in atoms + USD)
- a claim endpoint (sweeps everything in one tx)
So your "go to marketing buying the coin / MM / LP" loop is just:
cron(5min) → GET /profile/<bot> → if usd_claimable > $50:
POST /profile/claim
→ forward proceeds to:
• marketing-buy bot (40%)
• MM inventory wallet (40%)
• LP top-up (20%)We don't care how you fan it out downstream. We just hand you a clean, USD-denominated stream of claimable fees with a single-tx sweep.
User-supplied configs (/init flow) are:
- rate-limited per IP and per wallet (caps initiations per hour)
- filtered out of "official" surfaces (PROOFV3, friend-of-stacc, community-list are stacc-curated;
/initmints land in a separateuser-suppliedtier marked "NOT STACC-ENDORSED") - server-side rejected if anyone tries to fork an official config key via
?path=query
So your "lock it down to the first few" → just whitelist their wallets in your gate; everyone else hits 429 until you flip the switch. No code change to open it up later.
You bring the agent / the marketing logic / the MM strategy. We bring the launch primitive, the on-chain fee router, and the claim plumbing — battle-tested, atomic, and already on mainnet.
You don't have to write:
- DBC config minting
- Jito bundle assembly
- partner-fee claim logic
- a quote-token allow-list
- mcap targeting at mint time
- USD denomination of partner accruals
- a graduation-aware claim path (we handle pre- and post-migration)
You write:
- the agent that decides when to launch
- the wallet-fan-out that decides where the 80% goes after sweep
That's it. Two things instead of fifteen.
If you just slap a token on Meteora DBC yourself, you hit:
- partner-fee field is required → without us, it routes to you-or-nobody, no split
- LP-side fees can't be programmatically split at the protocol level → you'd need a custodial wrapper (yikes)
- post-graduation (DAMM v2 migration), the claim ABI changes → if you didn't account for both pre- and post-, your sweeper breaks the day your token bonds
We've already eaten those bugs. Twice. With on-chain proof.
Site is live on mainnet at https://www.staccpad.fun — endpoints are public, no signup gate. Spin up a throwaway wallet, mint a config via /init, launch one token against it, watch the partner-fee accrue on /profile. End-to-end takes ~3 minutes and ~$2 in tx fees.
If the shape fits your agent's needs, we go from "synergy vibe" to "you ship next week."
If it doesn't, you've still got a working DBC reference impl to crib from. Either way, win.
— stacc
Base URL: https://www.staccpad.fun (mainnet, no auth, public).
RPC: bring your own (Helius, Triton, QuickNode, whatever).
Signing: any Solana signer that can sign a base64-encoded VersionedTransaction.
npm i @solana/web3.js @solana/spl-token bs58That's it. You don't need our SDK; everything is over HTTP.
POST /api/launch-path/user/init partially-signs a config-creation tx that routes partner-fees to the wallet you call from.
POST https://www.staccpad.fun/api/launch-path/user/init
Content-Type: application/json
{
"userWallet": "<base58 pubkey, also pays for tx>",
"quoteMint": "<base58 pubkey of the quote token: USDC | SOL | $YOURTOKEN>"
}Why no
partnerWallet/creatorPercent? The partner_authority on the created config =userWallet. So ifuserWalletis your treasury / bot, 100% of the partner fee leg lands there. If you want a fan-out, sweep from there post-claim. Splitting at the curve level (e.g. 80/20 between two wallets) requires a separate splitter PDA — see "Custom split" below.
import { Connection, VersionedTransaction, Keypair } from '@solana/web3.js';
import bs58 from 'bs58';
const connection = new Connection(process.env.RPC_URL!, 'confirmed');
const signer = Keypair.fromSecretKey(bs58.decode(process.env.PRIVATE_KEY!));
const r = await fetch('https://www.staccpad.fun/api/launch-path/user/init', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
userWallet: signer.publicKey.toBase58(),
quoteMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
}),
});
const { txB64, configKey } = await r.json();
const tx = VersionedTransaction.deserialize(Buffer.from(txB64, 'base64'));
tx.sign([signer]); // already partially-signed by stacc; we add user sig
const sig = await connection.sendRawTransaction(tx.serialize());
await connection.confirmTransaction(sig, 'confirmed');
console.log('config minted:', configKey, 'tx:', sig);
// PERSIST configKey — every future launch refers to it.Cost: ~0.01 SOL (rent + fees). Done once per quote-mint, not per launch.
| status | meaning | fix |
|---|---|---|
409 quoteMint already covered… |
you tried to mint against an official quote (PROOF/$STACCPAD/etc.) | use that path's existing configKey from GET /api/launch-paths |
409 WSOL not supported via /init |
WSOL has a special pre-wrap path | use SOL path or the official WSOL launch-path |
400 Solscan has no USD price for… |
quote token has no liquidity → can't anchor mcap | pick a more liquid quote |
400 … only 6/8/9 decimals supported |
DBC SDK constraint | pick another quote |
| 429 | rate-limited (per IP + per wallet) | back off, retry in ~5min |
POST /api/launch-bundle returns a 5-tx Jito bundle. Sign all 5, push to Jito, done.
POST https://www.staccpad.fun/api/launch-bundle
Content-Type: application/json
{
"userWallet": "<base58 pubkey>",
"tokenName": "My Coin",
"tokenSymbol": "MYCOIN",
"userConfig": "user-<configKey from step 1>", // prefix REQUIRED
// pick ONE of:
"metadataUri": "https://arweave.net/...", // pre-uploaded JSON
// OR
"tokenLogo": "data:image/png;base64,iVBOR..." // we upload to R2 + mint metadata
}The
userConfigfield uses the prefixuser-to disambiguate from the official launch-paths (proofv3,staccpad, etc.). If you omit it, you launch against the default PROOFV3 path.
{
"txs": [
{ "label": "create-pool", "b64": "..." },
{ "label": "create-position", "b64": "..." },
{ "label": "creator-first-buy", "b64": "..." },
{ "label": "anti-snipe", "b64": "..." },
{ "label": "tip", "b64": "..." }
],
"ephemeralSigners": {
"baseMint": "<pubkey>", // throw-away keypair, must sign
"position": "<pubkey>"
},
"meta": {
"baseMint": "<the new token mint>",
"lbPair": "<pre-derived DBC pool address>",
"configKey": "<your config>",
"billieAtoms": "1000000000",
"metadataUri": "https://...",
"tipLamports": 100000
}
}import { VersionedTransaction, Keypair } from '@solana/web3.js';
const launchRes = await fetch('https://www.staccpad.fun/api/launch-bundle', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
userWallet: signer.publicKey.toBase58(),
tokenName: 'My Coin',
tokenSymbol: 'MYCOIN',
userConfig: `user-${configKey}`,
metadataUri: 'https://arweave.net/yourMetadataJson',
}),
});
const { txs, ephemeralSigners, meta } = await launchRes.json();
// The bundle ships with TWO ephemeral signers (baseMint + position).
// We don't have their secret keys server-side; you generate them on the
// frontend OR — for an agent — you need the server to return them.
// For programmatic use, request `ephemeralSecrets: true` (TODO: gated by
// API key — DM stacc to enable for your wallet).
// Sign each tx with [userSigner, baseMintSigner, positionSigner] as needed:
const signed = txs.map(({ b64 }) => {
const tx = VersionedTransaction.deserialize(Buffer.from(b64, 'base64'));
tx.sign([signer, baseMintKp, positionKp]); // each tx ignores irrelevant signers
return Buffer.from(tx.serialize()).toString('base64');
});
// Submit to Jito block-engine:
const jitoRes = await fetch('https://mainnet.block-engine.jito.wtf/api/v1/bundles', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0', id: 1, method: 'sendBundle', params: [signed],
}),
});
const { result: bundleId } = await jitoRes.json();
console.log('mint:', meta.baseMint, 'bundle:', bundleId);Note on ephemeral signers: for a fully-server-side agent flow we can return the keypairs server-side (currently a one-line flag). Ping stacc to enable. The frontend flow generates them client-side and the wallet adapter signs.
Atomic guarantee: Jito lands all 5 or none. No partial states.
GET /api/profile/[wallet] returns every pool launched against any config you own + accrued claimable atoms + USD.
GET https://www.staccpad.fun/api/profile/<your_wallet>{
"wallet": "<your wallet>",
"pools": [
{
"pool": "<lbPair>",
"baseMint": "<token>",
"quoteMint": "<USDC | SOL | …>",
"graduated": false,
"claimablePartnerBaseAtoms": "1234567",
"claimablePartnerQuoteAtoms": "9876543",
"claimablePartnerUsd": 12.34, // total USD across both legs
"claimableLpBaseAtoms": "0",
"claimableLpQuoteAtoms": "0",
"claimableLpUsd": 0,
"configKey": "<your config>",
"tier": "user-supplied",
"name": "My Coin",
"symbol": "MYCOIN"
}
// …
]
}setInterval(async () => {
const { pools } = await fetch(
`https://www.staccpad.fun/api/profile/${signer.publicKey}`
).then((r) => r.json());
for (const p of pools) {
if (p.claimablePartnerUsd < 50) continue; // dust threshold
await claimAndFanOut(p);
}
}, 5 * 60 * 1000);POST /api/profile/claim returns a base64 tx. Sign + land.
POST https://www.staccpad.fun/api/profile/claim
Content-Type: application/json
{
"wallet": "<your wallet>",
"pool": "<lbPair from step 3>",
"kind": "partner" // or "lp" — but you set creatorPercent=0, so 'partner' is what you want
}{ "tx": "<base64 v0 tx, unsigned>" }const claimRes = await fetch('https://www.staccpad.fun/api/profile/claim', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
wallet: signer.publicKey.toBase58(),
pool: p.pool,
kind: 'partner',
}),
});
const { tx: txB64 } = await claimRes.json();
const tx = VersionedTransaction.deserialize(Buffer.from(txB64, 'base64'));
tx.sign([signer]);
await connection.sendRawTransaction(tx.serialize());
// Now your wallet holds the claimed atoms in baseMint + quoteMint ATAs.
// Fan out to marketing-buy / MM / LP wallets via SystemProgram.transfer
// or SPL transfer-checked.The endpoint handles both pre-graduation (DBC) and post-graduation (DAMM v2) automatically. You don't branch on graduated — server picks the right ABI.
import { Connection, Keypair, VersionedTransaction, PublicKey } from '@solana/web3.js';
import bs58 from 'bs58';
const RPC = process.env.RPC_URL!;
const SK = bs58.decode(process.env.PRIVATE_KEY!);
const conn = new Connection(RPC, 'confirmed');
const me = Keypair.fromSecretKey(SK);
const BASE = 'https://www.staccpad.fun';
async function post(path: string, body: any) {
const r = await fetch(BASE + path, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
});
if (!r.ok) throw new Error(`${path}: ${r.status} ${await r.text()}`);
return r.json();
}
async function get(path: string) {
const r = await fetch(BASE + path);
if (!r.ok) throw new Error(`${path}: ${r.status}`);
return r.json();
}
async function signAndLand(b64: string, extra: Keypair[] = []) {
const tx = VersionedTransaction.deserialize(Buffer.from(b64, 'base64'));
tx.sign([me, ...extra]);
const sig = await conn.sendRawTransaction(tx.serialize());
await conn.confirmTransaction(sig, 'confirmed');
return sig;
}
// 1. one-time: mint config against USDC, partner=me
async function setupConfig() {
const { txB64, configKey } = await post('/api/launch-path/user/init', {
userWallet: me.publicKey.toBase58(),
quoteMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
});
await signAndLand(txB64);
return configKey;
}
// 2. on every "launch decision" your agent makes:
async function launch(configKey: string, name: string, symbol: string, metaUri: string) {
const baseMint = Keypair.generate();
const position = Keypair.generate();
const { txs } = await post('/api/launch-bundle', {
userWallet: me.publicKey.toBase58(),
tokenName: name,
tokenSymbol: symbol,
userConfig: `user-${configKey}`,
metadataUri: metaUri,
/* NOTE: server returns ephemeralSigners pubkeys; you must use the
* keypairs you control. Coordinate w/ stacc to enable a "pass me
* server-side ephemeral keys" flag, or use frontend signing. */
});
// ... build Jito bundle as in Step 2 ...
}
// 3. cron sweep
async function sweep() {
const { pools } = await get(`/api/profile/${me.publicKey.toBase58()}`);
for (const p of pools) {
if (p.claimablePartnerUsd < 50) continue;
const { tx } = await post('/api/profile/claim', {
wallet: me.publicKey.toBase58(), pool: p.pool, kind: 'partner',
});
await signAndLand(tx);
console.log('claimed', p.symbol, '$' + p.claimablePartnerUsd);
}
}
setInterval(sweep, 5 * 60 * 1000);If you want on-chain enforced 80% to wallet-A / 20% to wallet-B (instead of "claim to one wallet, then split off-chain"):
- Deploy a tiny splitter program (50 LoC Anchor — or use SquadsV4 multisig with a 0-of-N policy + scheduled tx).
- Pass
userWallet = <splitter PDA>to/api/launch-path/user/init. - Now
/api/profile/claimlands all partner fees inside the splitter, which atomically forwards 80/20 in the same tx.
Caveat: the splitter PDA must be able to sign the claim tx. Easiest path = sign via CPI from a permissionless crank instruction anyone can call. We have a reference impl — DM stacc if you want it.
Server-side already enforces:
- Per-wallet rate limit: N initiations per hour (configurable env var).
- Per-IP rate limit: prevents wallet-rotation DoS.
- Quote-token blocklist: WSOL, official-quotes, garbage tokens.
- Tier separation: user-supplied configs render in a separate "NOT STACC-ENDORSED" tier on the UI; they're not in
/api/launch-paths. - Server-rejected
?path=forks: users can't pretend their config is the official PROOFV3.
To whitelist a few devs while keeping everyone else gated:
// in src/pages/api/launch-path/user/init.ts, top of handler:
const ALLOWED = new Set(['<dev1pubkey>', '<dev2pubkey>']);
if (!ALLOWED.has(body.userWallet)) {
return res.status(403).json({ error: 'Closed beta — DM stacc for access' });
}One-line flip. Open it up later by deleting that block.
| method | path | purpose |
|---|---|---|
| GET | /api/launch-paths |
list official + your user-supplied configs |
| POST | /api/launch-path/user/init |
mint a new config (your 80/20 lives here) |
| GET | /api/launch-path/user/list?wallet=… |
configs minted by a wallet |
| POST | /api/launch-bundle |
5-tx Jito bundle for a launch |
| GET | /api/profile/[wallet] |
accrued fees per pool, USD-denominated |
| POST | /api/profile/claim |
sweep partner OR LP fees (per pool) |
| POST | /api/profile/claim-lp |
LP-side claim (post-graduation aware) |
| GET | /api/pool-stats?pool=… |
mcap, liquidity, holders for one pool |
| GET | /api/dbc-pools?configs=… |
list all pools under one or more configs |
| GET | /api/launch-stats |
tier-by-tier launch counts (homepage stats) |
| GET | /api/token-meta?mint=… |
proxy to Solscan + price cache |
All public. No API key required for the read surfaces. POSTs are signed-tx flows — server can't act on your behalf, you always sign last.
git clone https://github.com/<stacc>/fun-launch
cd fun-launch
pnpm i
cp .env.example .env.local
# fill in RPC_URL, POOL_CONFIG_KEY, R2_*, etc. (see DEPLOY.md)
pnpm dev
# http://localhost:3000You can run the entire stack against your own RPC + a fresh DBC config and validate the agent flow end-to-end before pointing at prod.
DM stacc. Reference impl: this repo. Mainnet live at https://www.staccpad.fun.
{ "txB64": "AQA...", // base64 partially-signed v0 tx "configKey": "9xKp...Z", // <-- this is what you launch against "meta": { "quoteMint": "EPjFW...", "symbol": "USDC", "decimals": 6, "usdPrice": 1.0, "migrationMarketCap": 67000, "initialMarketCap": 10000, "usdMigrationTarget": 67000 } }