Skip to content

Instantly share code, notes, and snippets.

@staccDOTsol
Last active April 26, 2026 23:08
Show Gist options
  • Select an option

  • Save staccDOTsol/f1661c3677c8455112f7544c5de445d0 to your computer and use it in GitHub Desktop.

Select an option

Save staccDOTsol/f1661c3677c8455112f7544c5de445d0 to your computer and use it in GitHub Desktop.

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.


what you're trying to build (as I read it)

  1. Programmatic token launch — agent/API kicks it off, no UI hand-holding.
  2. 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
  3. 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.


what staccpad already ships (the parts that matter to you)

1. POST /api/launch-bundle — atomic launch in one round-trip

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.

2. POST /api/launch-path/user/init — programmatic config minting

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.

3. GET /api/profile/[wallet] + POST /api/profile/claim — the auto-route layer

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.

4. spam control is already there

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; /init mints land in a separate user-supplied tier 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.


the synergy in one sentence

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.


what "still fail" probably means

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.


try it without committing

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


IMPLEMENTATION GUIDE — copy-paste ready

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.

Step 0 — install deps (Node/TS agent)

npm i @solana/web3.js @solana/spl-token bs58

That's it. You don't need our SDK; everything is over HTTP.


Step 1 — mint a custom config (your 80/20 split lives here)

POST /api/launch-path/user/init partially-signs a config-creation tx that routes partner-fees to the wallet you call from.

Request

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 if userWallet is 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.

Response (200)

{
  "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
  }
}

Sign + land

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.

Errors you might hit

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

Step 2 — launch a token against your config

POST /api/launch-bundle returns a 5-tx Jito bundle. Sign all 5, push to Jito, done.

Request

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 userConfig field uses the prefix user- to disambiguate from the official launch-paths (proofv3, staccpad, etc.). If you omit it, you launch against the default PROOFV3 path.

Response (200)

{
  "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
  }
}

Sign + send to Jito

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.


Step 3 — read accrued partner fees

GET /api/profile/[wallet] returns every pool launched against any config you own + accrued claimable atoms + USD.

Request

GET https://www.staccpad.fun/api/profile/<your_wallet>

Response (200)

{
  "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"
    }
    //
  ]
}

Cron loop pseudocode

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);

Step 4 — claim partner fees (single-tx sweep per pool)

POST /api/profile/claim returns a base64 tx. Sign + land.

Request

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
}

Response (200)

{ "tx": "<base64 v0 tx, unsigned>" }

Sign + land

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.


Step 5 — full agent loop (≤ 80 LoC)

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);

Custom 80/20 split (e.g. real two-wallet routing)

If you want on-chain enforced 80% to wallet-A / 20% to wallet-B (instead of "claim to one wallet, then split off-chain"):

  1. Deploy a tiny splitter program (50 LoC Anchor — or use SquadsV4 multisig with a 0-of-N policy + scheduled tx).
  2. Pass userWallet = <splitter PDA> to /api/launch-path/user/init.
  3. Now /api/profile/claim lands 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.


Spam control / "lock it down for the first few"

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.


Endpoints reference (full surface)

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.


Local dev / staging

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:3000

You 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.


Questions?

DM stacc. Reference impl: this repo. Mainnet live at https://www.staccpad.fun.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment