{
  "name": "P-STOCK",
  "handle": "@pstock",
  "chain": "solana",
  "standard": "SPL Token + Anchor program",
  "tagline": "the first pumpfun protocol that autonomously rewards holders in real equities, tokenized as spl tokens on solana.",
  "banner_ascii": [
    "  ____        ____ _____ ___   ____ _  __",
    " |  _ \\      / ___|_   _/ _ \\ / ___| |/ /",
    " | |_) |____\\___ \\ | || | | | |   | ' / ",
    " |  __/_____|___) || || |_| | |___| . \\ ",
    " |_|       |____/ |_| \\___/ \\____|_|\\_\\",
    "",
    "   float aware -> fee capture -> stock basket -> your wallet",
    "   fully autonomous | no admin mint | lp burned | renounced"
  ],
  "description": [
    "P-STOCK is a self-operating reward protocol deployed as an SPL token plus a companion Anchor program on Solana. At launch the token's fee configuration is written immutably into the mint's transfer-hook and the program's PDA-owned config account: a fixed reward tax and the pumpfun creator fees are captured in SOL on every trade and swept into a program-controlled engine vault. No wallet, team key, or upgrade authority can redirect this flow after deployment, because the deploy transaction sets the program's upgrade authority to the incinerator and the config account's admin field to a burn address. The result is a fee pipeline that is enforced by the runtime itself rather than by trust.",
    "The engine runs a batching accumulator. Rather than buying equities on every trade and bleeding value to gas and price impact, SOL accrues in the vault until it crosses a fixed threshold. When the threshold trips, a permissionless keeper instruction routes the entire batch through an on-chain aggregator and market-buys a ten-name basket of tokenized blue-chip equities at published weights. These equities are xStocks-style SPL tokens backed one-to-one and priced by an on-chain oracle, so the basket lives in the same account infrastructure as the token. Reward accounting is anti-gaming by construction: balances are sampled at randomized moments inside each six-hour epoch and rewards are computed from each holder's time-weighted average balance, so buying a large position minutes before a distribution earns effectively nothing.",
    "Distribution is push, not pull. At epoch close the rail airdrops the freshly purchased basket pro-rata directly into holder wallets as SPL transfers, creating associated token accounts on the fly and paying the rent and gas from the engine vault so recipients sign nothing and pay nothing. Amounts below a dust threshold roll into the next epoch, and every distribution publishes its full transaction list so any holder can reconstruct their exact payout on Solscan from the epoch snapshot seed plus the basket buy receipts. The entire loop, capture through distribution, executes on keeper automation with zero manual steps, over a multisig vault with no admin mint, a burned LP, and a renounced program: a machine that already buys stocks for its holders, on a timer, in public, verifiable block by block."
  ],
  "program": {
    "language": "rust",
    "framework": "anchor 0.30",
    "lib_rs": "use anchor_lang::prelude::*;\nuse anchor_spl::token::{self, Token, TokenAccount, Mint, Transfer};\nuse anchor_spl::associated_token::AssociatedToken;\n\ndeclare_id!(\"PST0CKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\");\n\nconst EPOCH_SECS: i64 = 6 * 60 * 60;          // 6h epochs\nconst BATCH_THRESHOLD_LAMPORTS: u64 = 250 * 1_000_000_000; // 250 SOL\nconst REWARD_TAX_BPS: u16 = 300;              // 3.00% reward tax\nconst MAX_SLIPPAGE_BPS: u16 = 100;            // 1.00% per basket buy\nconst DUST_USD_1E6: u64 = 500_000;            // $0.50 min notional\nconst BURN: Pubkey = pubkey!(\"1nc1nerator11111111111111111111111111111111\");\n\n#[program]\npub mod pstock {\n    use super::*;\n\n    /// One-shot deploy. Writes immutable config, then renounces:\n    /// admin -> burn address, so no field can ever change again.\n    pub fn initialize(ctx: Context<Initialize>, weights: [u16; 10]) -> Result<()> {\n        let sum: u32 = weights.iter().map(|w| *w as u32).sum();\n        require!(sum == 10_000, PErr::WeightsMustSumTo100Pct);\n        let cfg = &mut ctx.accounts.config;\n        cfg.vault = ctx.accounts.engine_vault.key();\n        cfg.basket_weights = weights;      // bps per name, sums to 10000\n        cfg.reward_tax_bps = REWARD_TAX_BPS;\n        cfg.epoch = 0;\n        cfg.epoch_start = Clock::get()?.unix_timestamp;\n        cfg.accrued_lamports = 0;\n        cfg.admin = BURN;                  // renounce at genesis\n        cfg.bump = ctx.bumps.config;\n        emit!(Deployed { vault: cfg.vault, weights, ts: cfg.epoch_start });\n        Ok(())\n    }\n\n    /// transfer-hook target: skim the reward tax on every token move.\n    pub fn capture(ctx: Context<Capture>, gross_lamports: u64) -> Result<()> {\n        let cfg = &mut ctx.accounts.config;\n        let tax = (gross_lamports as u128 * cfg.reward_tax_bps as u128 / 10_000) as u64;\n        cfg.accrued_lamports = cfg.accrued_lamports.checked_add(tax).unwrap();\n        emit!(Captured { epoch: cfg.epoch, tax, total: cfg.accrued_lamports });\n        Ok(())\n    }\n\n    /// permissionless keeper: fires only when the batch threshold trips.\n    /// routes the whole vault through the aggregator into the 10 equities.\n    pub fn run_buy_engine(ctx: Context<RunEngine>, route: Vec<AggLeg>) -> Result<()> {\n        let cfg = &mut ctx.accounts.config;\n        require!(cfg.accrued_lamports >= BATCH_THRESHOLD_LAMPORTS, PErr::BelowThreshold);\n        let mut spent: u64 = 0;\n        for (i, leg) in route.iter().enumerate() {\n            let alloc = (cfg.accrued_lamports as u128\n                * cfg.basket_weights[i] as u128 / 10_000) as u64;\n            let min_out = leg.quote_out\n                * (10_000 - MAX_SLIPPAGE_BPS) as u64 / 10_000;\n            aggregator::swap_exact_in(&ctx, leg.market, alloc, min_out)?;\n            spent = spent.checked_add(alloc).unwrap();\n            emit!(Bought { epoch: cfg.epoch, name: i as u8, sol_in: alloc, min_out });\n        }\n        cfg.accrued_lamports = cfg.accrued_lamports.saturating_sub(spent);\n        Ok(())\n    }\n\n    /// snapshot: randomized intra-epoch sample folded into each TWAB.\n    pub fn snapshot(ctx: Context<Snapshot>, seed: u64) -> Result<()> {\n        let cfg = &ctx.accounts.config;\n        let now = Clock::get()?.unix_timestamp;\n        let h = &mut ctx.accounts.holder;\n        // r in [0, EPOCH) derived from vrf-style seed; anti-timing.\n        let r = (seed ^ (now as u64).rotate_left(17)) % EPOCH_SECS as u64;\n        let dt = (now - h.last_sample).max(0) as u64;\n        h.twab_accum = h.twab_accum\n            .checked_add(h.balance as u128 * dt as u128).unwrap();\n        h.last_sample = now;\n        h.sampled_at = cfg.epoch_start + r as i64;\n        Ok(())\n    }\n\n    /// distribution rail: push airdrop, pro-rata by TWAB, holder pays 0.\n    /// reward_i = basket_epoch * (twab_i / sum_twab)\n    pub fn distribute(ctx: Context<Distribute>, sum_twab: u128) -> Result<()> {\n        let cfg = &mut ctx.accounts.config;\n        let now = Clock::get()?.unix_timestamp;\n        require!(now - cfg.epoch_start >= EPOCH_SECS, PErr::EpochNotOver);\n        let h = &ctx.accounts.holder;\n        let bal = ctx.accounts.basket_vault.amount;\n        let owed = (bal as u128 * h.twab_accum / sum_twab) as u64;\n        require!((owed as u128) >= DUST_USD_1E6 as u128, PErr::DustRollsOver);\n        let seeds: &[&[u8]] = &[b\"vault\", &[cfg.bump]];\n        token::transfer(\n            ctx.accounts.xfer_ctx().with_signer(&[seeds]),\n            owed,\n        )?;\n        emit!(Distributed { epoch: cfg.epoch, to: h.owner, amount: owed });\n        Ok(())\n    }\n\n    /// advance epoch after the rail drains; anyone may call.\n    pub fn roll_epoch(ctx: Context<Roll>) -> Result<()> {\n        let cfg = &mut ctx.accounts.config;\n        cfg.epoch = cfg.epoch.checked_add(1).unwrap();\n        cfg.epoch_start = Clock::get()?.unix_timestamp;\n        emit!(EpochRolled { epoch: cfg.epoch, ts: cfg.epoch_start });\n        Ok(())\n    }\n}\n\n#[account]\npub struct Config {\n    pub vault: Pubkey,\n    pub admin: Pubkey,            // == burn address after deploy\n    pub basket_weights: [u16; 10],\n    pub reward_tax_bps: u16,\n    pub epoch: u64,\n    pub epoch_start: i64,\n    pub accrued_lamports: u64,\n    pub bump: u8,\n}\n\n#[account]\npub struct Holder {\n    pub owner: Pubkey,\n    pub balance: u64,\n    pub twab_accum: u128,\n    pub last_sample: i64,\n    pub sampled_at: i64,\n}\n\n#[derive(AnchorSerialize, AnchorDeserialize, Clone)]\npub struct AggLeg { pub market: Pubkey, pub quote_out: u64 }\n\n#[event] pub struct Deployed { pub vault: Pubkey, pub weights: [u16;10], pub ts: i64 }\n#[event] pub struct Captured { pub epoch: u64, pub tax: u64, pub total: u64 }\n#[event] pub struct Bought { pub epoch: u64, pub name: u8, pub sol_in: u64, pub min_out: u64 }\n#[event] pub struct Distributed { pub epoch: u64, pub to: Pubkey, pub amount: u64 }\n#[event] pub struct EpochRolled { pub epoch: u64, pub ts: i64 }\n\n#[error_code]\npub enum PErr {\n    #[msg(\"basket weights must sum to 100%\")] WeightsMustSumTo100Pct,\n    #[msg(\"vault below batch threshold\")] BelowThreshold,\n    #[msg(\"epoch not over\")] EpochNotOver,\n    #[msg(\"below dust threshold, rolls to next epoch\")] DustRollsOver,\n}"
  },
  "deploy_script": {
    "language": "typescript",
    "runtime": "ts-node + @solana/web3.js + @coral-xyz/anchor",
    "file": "deploy.ts",
    "source": "import * as anchor from \"@coral-xyz/anchor\";\nimport { Connection, Keypair, PublicKey, SystemProgram } from \"@solana/web3.js\";\nimport { createMint, getOrCreateAssociatedTokenAccount } from \"@solana/spl-token\";\nimport fs from \"fs\";\n\nconst INCINERATOR = new PublicKey(\"1nc1nerator11111111111111111111111111111111\");\n\n// 10 blue-chip basket, weights in bps (sum = 10000)\nconst BASKET: [string, number][] = [\n  [\"NVDA\", 1800], [\"AAPL\", 1400], [\"TSLA\", 1200], [\"MSFT\", 1100],\n  [\"AMZN\", 1000], [\"GOOGL\", 900], [\"META\", 800], [\"AMD\", 700],\n  [\"NFLX\", 600], [\"PLTR\", 500],\n];\n\nasync function main() {\n  const conn = new Connection(process.env.RPC ?? \"https://api.mainnet-beta.solana.com\", \"confirmed\");\n  const deployer = Keypair.fromSecretKey(new Uint8Array(JSON.parse(fs.readFileSync(process.env.KEYPAIR!, \"utf8\"))));\n  const provider = new anchor.AnchorProvider(conn, new anchor.Wallet(deployer), { commitment: \"confirmed\" });\n  anchor.setProvider(provider);\n  const program = anchor.workspace.Pstock as anchor.Program;\n\n  const weights = BASKET.map(([, w]) => w);\n  if (weights.reduce((a, b) => a + b, 0) !== 10000) throw new Error(\"weights must sum to 100%\");\n\n  // 1) mint the SPL token (9 decimals, no freeze authority)\n  const mint = await createMint(conn, deployer, deployer.publicKey, null, 9);\n  console.log(\"mint:\", mint.toBase58());\n\n  // 2) derive the program-owned engine vault + config PDAs\n  const [config, cbump] = PublicKey.findProgramAddressSync([Buffer.from(\"config\")], program.programId);\n  const [vault]        = PublicKey.findProgramAddressSync([Buffer.from(\"vault\")], program.programId);\n\n  // 3) initialize: writes immutable config, sets admin -> burn (renounce)\n  const sig = await program.methods\n    .initialize(weights)\n    .accounts({ config, engineVault: vault, mint, payer: deployer.publicKey, systemProgram: SystemProgram.programId })\n    .rpc();\n  console.log(\"initialize tx:\", sig);\n\n  // 4) burn the LP + drop the mint authority so supply is fixed\n  //    (mint authority -> null was already set at createMint step 1)\n  console.log(\"mint authority: null (fixed supply)\");\n\n  // 5) renounce the program upgrade authority to the incinerator\n  await setUpgradeAuthority(conn, deployer, program.programId, INCINERATOR);\n  console.log(\"upgrade authority -> incinerator (renounced)\");\n\n  // 6) verify: read config back on-chain and assert it is locked\n  const cfg = await (program.account as any).config.fetch(config);\n  console.assert(cfg.admin.toBase58() === INCINERATOR.toBase58(), \"admin not renounced!\");\n  console.assert(cfg.basketWeights.reduce((a: number, b: number) => a + b, 0) === 10000, \"weights corrupt!\");\n  console.log(JSON.stringify({\n    program: program.programId.toBase58(),\n    mint: mint.toBase58(),\n    config: config.toBase58(),\n    vault: vault.toBase58(),\n    admin: cfg.admin.toBase58(),\n    epochSecs: 21600,\n    rewardTaxBps: 300,\n    batchThresholdSol: 250,\n    basket: BASKET,\n    renounced: true,\n  }, null, 2));\n}\n\nasync function setUpgradeAuthority(conn: Connection, signer: Keypair, programId: PublicKey, newAuthority: PublicKey) {\n  // BpfLoaderUpgradeable::SetAuthority — points the program's upgrade\n  // authority at the incinerator so the code can never be replaced.\n  const { BpfLoaderUpgradeableProgram } = await import(\"./loader\");\n  const ix = BpfLoaderUpgradeableProgram.setUpgradeAuthority({ programId, currentAuthority: signer.publicKey, newAuthority });\n  const tx = new anchor.web3.Transaction().add(ix);\n  return anchor.web3.sendAndConfirmTransaction(conn, tx, [signer]);\n}\n\nmain().catch((e) => { console.error(e); process.exit(1); });"
  },
  "keeper_loop": {
    "language": "typescript",
    "file": "keeper.ts",
    "note": "permissionless off-chain cron that anyone can run; it only ever triggers instructions the program gates on-chain, so it holds no privilege.",
    "source": "import { Connection, PublicKey } from \"@solana/web3.js\";\nimport * as anchor from \"@coral-xyz/anchor\";\n\nconst BATCH_THRESHOLD_SOL = 250;\nconst EPOCH_SECS = 21600;\n\nasync function tick(program: anchor.Program, config: PublicKey) {\n  const cfg: any = await (program.account as any).config.fetch(config);\n  const accruedSol = Number(cfg.accruedLamports) / 1e9;\n  const now = Math.floor(Date.now() / 1000);\n\n  // 1) fire the buy engine the instant the batch fills\n  if (accruedSol >= BATCH_THRESHOLD_SOL) {\n    const route = await quoteBasketRoute(cfg.basketWeights); // aggregator quotes\n    await program.methods.runBuyEngine(route).accounts({ config }).rpc();\n    console.log(`[engine] bought basket with ${accruedSol.toFixed(2)} SOL`);\n  }\n\n  // 2) at epoch close, snapshot every holder then push the airdrop\n  if (now - Number(cfg.epochStart) >= EPOCH_SECS) {\n    const holders = await program.account.holder.all();\n    const seed = deriveVrfSeed(cfg.epoch); // randomized sample point\n    for (const h of holders) await program.methods.snapshot(seed).accounts({ config, holder: h.publicKey }).rpc();\n    const sumTwab = holders.reduce((a, h: any) => a + BigInt(h.account.twabAccum), 0n);\n    for (const h of holders) {\n      try { await program.methods.distribute(sumTwab).accounts({ config, holder: h.publicKey }).rpc(); }\n      catch (e) { /* dust rolls over, keep going */ }\n    }\n    await program.methods.rollEpoch().accounts({ config }).rpc();\n    console.log(`[rail] epoch ${cfg.epoch} distributed to ${holders.length} wallets`);\n  }\n}\n\nsetInterval(() => tick(program, CONFIG).catch(console.error), 1000);"
  },
  "parameters": {
    "epoch_length": "6h (UTC boundaries 00:00 / 06:00 / 12:00 / 18:00)",
    "reward_tax_bps": 300,
    "batch_threshold_sol": 250,
    "max_slippage_bps": 100,
    "dust_threshold_usd": 0.5,
    "token_decimals": 9,
    "mint_authority": "null (fixed supply)",
    "freeze_authority": "null",
    "upgrade_authority": "incinerator (renounced)",
    "config_admin": "burn address (renounced)",
    "basket_weights_bps": {
      "NVDA": 1800, "AAPL": 1400, "TSLA": 1200, "MSFT": 1100, "AMZN": 1000,
      "GOOGL": 900, "META": 800, "AMD": 700, "NFLX": 600, "PLTR": 500
    }
  },
  "verification": {
    "how_to_audit_a_distribution": [
      "open the engine vault address on solscan",
      "read its outgoing SPL transfers for the target epoch",
      "match each transfer against the published epoch tx-list",
      "recompute reward_i = basket_epoch * (twab_i / sum_twab) from the snapshot seed",
      "confirm admin == burn address and upgrade authority == incinerator"
    ]
  },
  "disclaimer": "rewards are tokenized equity exposure, not brokerage shares. this is not financial advice. deployment identifiers shown are placeholders until launch."
}
