Documentation

A walkthrough of the protocol.

Bonding curve math, the Token-2022 transfer hook, commit-reveal mints, on-chain pixel rendering, and the trade-offs behind every line. Also available raw at /docs.md.

SPEG

A long-form walkthrough of what SPEG is, how it works, and the engineering choices behind it. Written for people who are about to mint, or about to read the source. Either way, this page tries to be honest about the trade-offs instead of glossing over them.

What there is, is a small, exact mechanism. You buy a utility token called tPEG from a bonding curve. You burn 100,000 tPEG and an NFT appears in your wallet roughly one second later. Or you burn 10,000 tPEG for a 10% chance at the same outcome. The supply caps at 10,000 NFTs across six rarity tiers. After that, the bonding curve closes and the collection is sealed.

This document explains why each of those numbers exists, what happens on-chain when you press the mint button, and what we deliberately chose to leave out.


Where this comes from

The artwork in SPEG sits between two reference points. The first is the Japanese gachapon: a coin-operated capsule dispenser you'd find on a quiet street in Tokyo, painted on the side by whoever ran the shop, dropping a small plastic egg into your hand for a single coin. The aesthetic is forty years old and still in use. Pixel artists have been redrawing it for almost as long.

The second is the Solana Candy Machine era between 2021 and 2022, when an entire chain learned that the click between mint and reveal was the whole thing. Candy Machine is the technical heritage. Every sPEG NFT is a continuation of that same minting ritual, just rebuilt on top of a primitive that didn't exist back then.

The pixel art borrows the look of the first. The protocol borrows the spirit of the second. What sits between them is a hook, on the 2022 standard.


The protocol is the transfer hook

This is the part that determines almost everything else.

Token-2022 is the successor to the original SPL Token program on Solana. One of the new things it ships is the Transfer Hook extension: a mint can be configured so that every transfer of that mint invokes a custom program first. The custom program receives the source account, the destination account, the amount, and a list of extra accounts that the mint's deployer set up in advance, and can either approve the transfer (Ok(())) or revert it (Err(...)).

Most projects that use transfer hooks use them defensively, for royalty enforcement, blocklists, KYC gates. SPEG uses one offensively. The hook is not a guard. It IS the protocol. When a transfer of tPEG lands at SPEG's portal_vault account, the hook is the thing that mints the sPEG NFT. There is no separate claim_nft instruction. There is no "the user calls X, then the program calls Y". The entire mint flow happens inside the hook invocation, atomically with the transfer that triggered it.

The mechanism looks roughly like this. A user transfers 100,000 tPEG from their wallet to portal_vault. Solana sees that the tPEG mint has the TransferHook extension and routes the call through our hook program. Our hook examines the destination: if it is not the portal, it returns Ok(()) immediately and the transfer completes normally. This is how you keep tPEG transferable, wallet-to-wallet moves, deposits to AMM pools, escrow flows. All of those hit the hook, and the hook recognises them as non-portal and steps aside.

If the destination IS the portal, the hook performs a different ritual. It checks the amount (must be exactly 100,000 or 10,000 tPEG, nothing else), confirms there is room left in the cap, derives a deterministic PDA address for the next NFT mint for this wallet, and creates a SpinCommitment account that promises a roll two slots from now. That second instruction (the reveal) uses the slot hash of a future Solana slot as the source of randomness. Two slots, in human terms, is roughly one second. The frontend chains the commit and the reveal inside one modal so users see a single "minting…" experience, but two transactions are actually happening.

Both modes work the same way: commit at the hook, reveal a second later. The only difference between guaranteed (100k tPEG) and jackpot (10k tPEG) is the success probability used inside the reveal. We'll come back to why we split it that way in a moment.


Two tokens. The protocol uses both, you might not notice.

It is easier to follow the rest of this page if you have two names firmly separated in your head.

tPEG is the bonding-curve token, the fuel. It is a standard Token-2022 SPL mint with one of the extensions enabled (more on which extension in a moment). You buy tPEG with SOL from an on-chain curve; the price rises as the protocol progresses; you burn tPEG to mint an NFT. tPEG is fungible. You can hold it, trade it, send it to a friend, list it on a DEX once one supports Token-2022 transfer hooks. It does nothing on its own except sit in your wallet waiting to be burned.

sPEG is the NFT, the artefact. Every successful mint creates a brand new SPL token mint with a supply of exactly 1, a Metaplex metadata account, and a SpegArt account holding 576 bytes that describe the pixel grid. After the mint completes, the mint authority is revoked. The NFT is unique, immutable, and yours.

The collection is called SPEG without a prefix when we talk about the project as a whole, the brand, the website, the social handle. Inside the code and on the chain, tPEG and sPEG are the only two things you'll see.

We chose this naming because the burn-to-mint flow is the single most important thing about the protocol, and naming the inputs and outputs with different prefixes (rather than calling them both "SPEG tokens") makes that flow much easier to talk about. Without it, sentences end up like "transfer SPEG to mint a SPEG" and nobody is sure what's going on.


The bonding curve

Bonding curves are not novel. The variant SPEG uses is mildly so.

Every mint counts. tPEG is sold by an on-chain function that takes a SOL amount, multiplies by a base rate, divides by a multiplier, and gives you the resulting number of tPEG. The base rate is 1,000,000 tPEG per SOL, which means at the start of the game, 0.1 SOL buys exactly the 100,000 tPEG needed for one guaranteed mint. The multiplier starts at 1× and rises with progress, peaking at 50× when the cap is exhausted.

The shape we picked is multiplier(p) = 1 + 32·p + 17·p¹⁵, where p is the protocol's progress between 0 and 1. The linear term dominates through about 85% of the run, the multiplier climbs from 1× to roughly 30× as the supply fills. The high-power term sleeps until the very end, then suddenly contributes 17 more to push the final stretch from 30× toward 50×. The shape rewards early participation without making the last NFTs free.

What progresses p? Two things, whichever is larger at any given moment: the count of NFTs actually minted, and the count of tPEG already purchased divided by GUARANTEED_MINT_COST (100,000). The larger of the two drives the curve. This is the hybrid part. Earlier we shipped a curve that progressed only on mints, and discovered the obvious flaw the day after: a syndicate could quietly buy 1 billion tPEG at the 1× rate while the multiplier sat still, then mint at their leisure. We replaced the curve with max(total_minted, total_bought / 100_000) clamped to NFT_CAP. Now every buy ratchets the curve forward, and the syndicate has to face progressive prices like everyone else.

Mints also advance the curve directly, this matters when the actual mint count overtakes the implied count from purchases (some buyers hold rather than mint, some mints fail probabilistically, some commits expire). The two counters move independently; the curve takes the maximum.

There is one practical consequence of all of this: the price a user sees in the frontend is a quote, not a quote with a side of certainty. By the time the user signs and the validator picks up the transaction, the curve may have moved. The on-chain program recomputes the rate at execution time using whatever state actually exists, and enforces a slippage check using min_tokens_out provided by the caller. The frontend exposes a slippage selector (1% / 5% / 10% / 50%) so users can choose how tolerant they want to be of curve movement between their signature and inclusion. Default is 1%. If the curve moved further than that, the transaction reverts and the tPEG never leaves the buyer's wallet, they just pay the priority fee and try again.


On-chain art

The pixels are part of the protocol's storage. Not a hash of the pixels, not a URI to a server that holds the pixels, the pixels.

Each successful mint creates a SpegArt account whose pixels field is a 576-byte array (24 rows × 24 columns) of palette indices from 0 to 15. The account is ~642 bytes once you include the Anchor discriminator, the NFT mint pubkey, the rarity index, the entropy seed, and the bump. Rent for the account is paid by the program's mint_authority PDA, which the team pre-funds at bootstrap. At ~0.005 SOL per SpegArt, 10,000 NFTs locks roughly 50 SOL of rent into the collection's storage permanently. That cost is deliberate. The pixels are baked in.

The pixel grid is computed by a Rust function inside the program , art.rs::render_pixel_grid(seed, rarity). The function is deterministic: the same (seed, rarity) pair produces the same grid every time. The seed comes from SlotHashes[target_slot] (see above) and the rarity comes from a weighted draw over the remaining capacity in each tier.

The painter runs in two phases. First, it lays down the machine: the boxy body in the rarity's palette, a washi-paper window that shows the capsule waiting to dispense, a kanji glyph stamped on the front, the rounded coin knob, the engraving plate near the dispense slot, and small variations seeded by specific bytes of the entropy (wood grain density, capsule colour, kanji style, knob shape). These variations are deterministic from the seed but produce something like 50+ distinguishable machine bodies at the Common tier alone.

Second, the painter applies tier overlays. Common and Uncommon NFTs get the body and not much else. Rare introduces a sakura branch with falling petals above the machine. Epic adds an ema tag, a small wooden wishing plaque, hanging from a string. Legendary frames the machine in a full torii gate, the kind you'd see at a shrine entrance, with a halo behind it. Mythic adds a spirit aura, two floating lanterns, and a kitsune peeking from behind the torii. The visual hierarchy is intentional: at a glance you can tell roughly how rare an NFT is from across a room.

A 16-colour palette per tier gives every rarity its own light. Common runs in pine greens; Uncommon shifts toward cool blues; Rare uses amber and ochre; Epic settles into rust reds; Legendary glows gold with the akabeni accent we use throughout the site; Mythic is a spirit-night purple with whites and stars. The same 24×24 silhouette plus the palette swap plus the tier overlays plus the seed-driven trait variations gives the collection its full visual range without needing thousands of hand-drawn assets.

The frontend mirrors the Rust algorithm bit-for-bit in TypeScript. artifact-art.ts reads a SpegArt account, decodes the pixel array, looks up the matching tier palette, and renders one <rect> per pixel inside a single SVG. The result is crisp at any zoom level because it's not a rasterised image, it's a small grid of coloured squares described in vector form. NFT viewers like Phantom and explorers like Solscan can grab the metadata URI, hit our small renderer endpoint, and receive a Metaplex-compliant JSON document with the SVG inlined as data:image/svg+xml;base64,.... No image hosting service is in the loop. The renderer can be served from any Vercel deployment and the NFT's appearance remains identical because the input is always the on-chain pixel grid.

There is a subtle property worth pointing out. Because the pixel generation is deterministic from (seed, rarity), and the seed lives inside SpegArt, anyone, at any time, with no network access except a Solana RPC, can re-run render_pixel_grid and verify that the stored pixels match what the seed should produce. The protocol is self-describing. If the renderer endpoint disappears tomorrow, you can run the same algorithm locally and reconstitute every NFT's image from chain state alone.


Rarity and supply

Ten thousand sPEG NFTs across six tiers, with caps chosen so that the expected aesthetic balance of the collection is preserved as long as the mint flow stays fair.

  • Common holds 7,000. The everyday machines.
  • Uncommon holds 2,000. Slightly elevated palette, no overlays.
  • Rare holds 700. Sakura branch appears.
  • Epic holds 240. Sakura plus ema plaque.
  • Legendary holds 50. Full torii framing, halo.
  • Mythic holds 10. Spirit-night palette, kitsune, lanterns.

These numbers are written into the RARITY_CAPS constant in Rust and mirrored in the TypeScript constants module. They are enforced on-chain by the reveal handler: when a roll lands in a tier whose cap is already filled, the reveal redraws to a tier that still has room. The end of the collection is therefore not "Mythic only", by the time the last 50 NFTs mint, only Legendary and below have capacity, and the protocol gracefully drains them. There is no "last man standing gets a Mythic" exploit at the cap.

Per wallet, the program caps total mints at 200 sPEG and total tPEG purchases at 20,000,000 (the equivalent of 200 guaranteed mints' worth). 200 NFTs is 2% of the total supply. The economic effect is that no single wallet can sweep more than a small slice of the collection, even at the early-curve prices. A coalition of multiple wallets can do more, that's a known limitation of permissionless protocols, but the per-wallet ceiling forces a coalition to pay wallet rent and operationalise across many addresses, which raises the cost of squatting the cap from "a bot" to "an organised attempt".

There is one more economic check, recently added. Both modes share a cap-reservation system: every guaranteed commit reserves a slot in GameState.committed_count and a per-wallet slot in UserAttempts.pending_guaranteed_count while it waits for reveal. The hook refuses new guaranteed commits if those slots are already saturated. The reason for this is unglamorous and important. Without reservation, two users committing 100,000 tPEG each at total_minted = 9,999 both pass the cap check (9,999 < 10,000). The first reveal mints; the second reveal hits the cap-full state, fails the success roll, and the user's 100,000 tPEG goes to total_failed_tokens. They paid for a guaranteed mint and got nothing. The reservation system means the second user simply gets GameOver at commit time, their tPEG never leaves their wallet, and the failure mode is clean rather than confusing.

Jackpot commits do not reserve cap slots. A jackpot user has already accepted a 90% loss probability; the extra few percent introduced by "the cap might fill before your reveal lands" is rolled into the same expected outcome. The on-chain math force-misses a jackpot reveal if the cap has filled by reveal time, which is the same outcome as a normal random miss from the user's perspective.


The roll, in two halves

Every reveal runs two rolls in sequence. The first decides whether the commit mints an NFT at all. The second, only if the first succeeded, decides which rarity tier the NFT belongs to. Both pull from the same 32-byte seed; one half of the seed feeds the success roll, the other half feeds the rarity roll.

Where the seed comes from

The seed is built in two layers. The first is a deposit_seed, deterministic from the commit's identity:

deposit_seed = sha256(
  project_mint  ||
  wallet        ||
  portal_vault  ||
  commit_amount ||
  commit_index  ||
  commit_slot
)

Everything in deposit_seed is already known at commit time. By itself it would be predictable, which is exactly why the second layer is needed.

The second layer mixes in entropy that did not exist when the commit was signed. The reveal runs at least two slots after the commit, and reads SlotHashes[target_slot], the hash of the slot the user pointed at when committing. That slot's hash is produced by validators at slot production time; nobody could have known it when signing the commit.

final_seed = sha256(
  deposit_seed ||
  SlotHashes[target_slot] ||
  nft_mint_pubkey
)

The nft_mint_pubkey is itself a PDA derived from the wallet and the commit index, so it adds no extra entropy, but it pins the seed to the exact NFT being created.

A user simulating the reveal transaction before submitting cannot change SlotHashes[target_slot]. They can read it, but the only thing that depends on it is the same reveal they are about to send, and the reveal can only execute once. There is no second draw.

Half one: did it mint?

The first four bytes of final_seed are read as a little-endian u32, taken modulo 10,000:

success_roll = u32_le(final_seed[0..4]) % 10_000

This puts success_roll somewhere in the range [0, 9999] with a near-uniform distribution. The threshold it gets compared against depends on which mode the commit was for:

  • Guaranteed mint (100,000 tPEG) uses a threshold of 10,000. The comparison success_roll < 10000 is always true. Guaranteed mints pay 10x more for a deterministic outcome.
  • Jackpot roll (10,000 tPEG) uses a threshold of 1,000. The comparison success_roll < 1000 succeeds when the roll is in [0, 999], which is exactly 1,000 outcomes out of 10,000.

That last line is what "10% chance" means concretely. 1,000 / 10,000 = 0.1 = 10%. The roll is implemented as integer modulo so there is no floating-point drift, and the threshold is a compile-time constant (JACKPOT_PROBABILITY_BPS) baked into the program binary.

There is a defense-in-depth re-check at this point. If by the time the reveal lands the global cap is full, or the wallet has already hit its 200-NFT ceiling, the success flag is forced to false even if the roll itself would have won. This is the path that drains jackpot commits gracefully at end-game without freezing them mid-flow.

The expected value of a jackpot is identical to a guaranteed mint per unit of tPEG burned. 10,000 tPEG times 10% mint probability equals 1,000 tPEG of expected NFT cost, which is exactly the same as 100,000 tPEG at 100%. Jackpots offer higher variance, not better economics.

Half two: which rarity?

If the first half passed, the next eight bytes of final_seed become the rarity roll:

rarity_roll = u64_le(final_seed[4..12])

This 64-bit value is mapped onto the rarity tiers by a weighted draw over the REMAINING supply, not the original caps. Every tier tracks how many NFTs it has minted already, and the roll picks proportional to what is still left.

Concretely, the on-chain code does this:

remaining[i] = RARITY_CAPS[i] - minted_per_rarity[i]   for i in 0..6
total_remaining = sum(remaining)
pick = rarity_roll % total_remaining

pick lands somewhere in [0, total_remaining). The code walks the tiers in order (Common, Uncommon, Rare, Epic, Legendary, Mythic), keeping a cumulative count, and the first tier whose cumulative count exceeds pick is the one selected.

At the start of the collection, remaining is identical to RARITY_CAPS = [7000, 2000, 700, 240, 50, 10] and the odds match exactly: 70% Common, 20% Uncommon, 7% Rare, 2.4% Epic, 0.5% Legendary, 0.1% Mythic.

As the collection progresses, tiers fill up at different rates. A Mythic tier filling out early would drop to remaining[5] = 0, removing it from the weighted draw entirely. From that point on, no new mint can land on Mythic regardless of the roll. The same mechanism gracefully drains the collection: when only Common and Uncommon have capacity left, the protocol mints from those two exclusively, and the final NFT goes to whichever tier has the last slot.

This is also why there is no "last 50 NFTs are all Mythic" attack at the cap. Mythic only has 10 slots ever; once those are claimed, remaining[5] stays at 0 forever and no late-game roll can land there.

The per_rarity_index (1 through the tier's cap) is incremented at mint time and stored in the NFT's metadata. That is how you see labels like "Mythic #7 of 10" on a sPEG: tier 5, the 7th out of 10 that will ever exist.

Both rolls, on the same hash

A subtle point worth pulling out. The success roll and the rarity roll come from disjoint byte slices of the same final_seed, not from two separate hashes. A user cannot retry one half without retrying the other, and both are equally bound to SlotHashes[target_slot]. If the reveal fails its first roll, the rarity bytes were rolled but discarded. If it succeeds, the rarity bytes pin the tier deterministically. There is no second random source anywhere in the path.

The same final_seed is also fed to the on-chain pixel renderer (art.rs::render_pixel_grid). Same input bytes, same output image, forever. Anyone with a Solana RPC can verify after the fact that the pixels stored in SpegArt match what the seed should have produced.


Hybrid bonding curve, detailed

Tap on this section if you want the math.

We track two cumulative counters in GameState:

  • total_minted, number of sPEG NFTs that have successfully been minted. Strictly monotonic, never decrements.
  • total_bought, total tPEG, in base units, ever sold by the buy_tokens instruction. Strictly monotonic.

The progress fed into the multiplier is:

p_units = min(
  NFT_CAP,
  max(total_minted, total_bought / GUARANTEED_MINT_COST)
)
p_fraction = p_units / NFT_CAP

Dividing total_bought by GUARANTEED_MINT_COST converts buys into "mint-equivalents" so the two counters live on the same axis. The larger wins. Clamping by NFT_CAP keeps p_fraction in [0, 1] even if the curve is briefly ahead of supply due to a flurry of buys.

The multiplier is computed in u128 fixed-point inside the on-chain math (Solana programs don't get floats). The formula is

multiplier(p) = 1 + 32·p + 17·p¹⁵

with everything scaled by BONDING_SCALE = NFT_CAP = 10,000 so the intermediates stay inside u128 without overflow. The p¹⁵ term is computed by repeated squaring with rescaling, never letting any single multiplication exceed BONDING_SCALE² (10⁸), which sits comfortably inside u128.

The frontend recomputes the same math in TypeScript so the live quote matches what the chain will charge. pda.ts exports effectiveCurveProgress, effectiveTokensPerSol, quoteTokensForSol, and quoteSolForTokens helpers that the mint panel uses for the "≈ X tPEG · Y× multiplier" preview underneath the SOL input. The on-chain math is the source of truth, the frontend is a UX hint.


The architecture, in one diagram

User wallet
   │
   ▼  buy_tokens(N lamports)
┌─────────────────────────────────────────────┐
│  Bonding curve                              │
│  rate = base / multiplier(p)                │
│  mint_to(buyer_token, rate × N)             │
│  game_state.total_bought += rate × N        │  ← curve advances
└─────────────────────────────────────────────┘
   │
   ▼  transfer_checked(100k tPEG → portal_vault)
┌─────────────────────────────────────────────┐
│  Token-2022 transfer_checked                │
│  invokes the TransferHook extension         │
└─────────────────────────────────────────────┘
   │
   ▼
┌─────────────────────────────────────────────┐
│  Hook program                               │
│  if destination != portal: return Ok(())    │  ← non-portal no-op
│  validate amount, mint, source, dest        │
│  reserve cap slot (guaranteed only)         │
│  create SpinCommitment(target_slot = N+2)   │  ← commit
└─────────────────────────────────────────────┘
   │
   ▼  (frontend waits 2 slots, then submits reveal)
┌─────────────────────────────────────────────┐
│  reveal_spin                                │
│  read SlotHashes[target_slot] as entropy    │
│  roll success based on commit.amount        │
│  if hit: mint NFT + ATA + metadata + art    │  ← actual mint
│  if miss: account as total_failed_tokens    │
│  release the reserved cap slot              │
│  close SpinCommitment                       │
└─────────────────────────────────────────────┘
   │
   ▼
User wallet now holds 1 sPEG NFT

The two transactions in the mint flow are bundled by the frontend into one wallet popup via signAllTransactions. The user signs once, the frontend handles both, the UI updates as each lands.


A guided tour of the program

For readers who plan to look at the source.

The Rust program lives in programs/speg/src/. The entry point is lib.rs, which contains the #[program] module declaring the instructions and the fallback handler that routes Token-2022's transfer-hook CPI into our transfer_hook instruction.

The instructions on the mint path are:

  • buy_tokens(lamports, min_tokens_out). Computes the current curve rate, mints tPEG to the buyer at that rate, and advances total_bought.
  • register_holder(owner). Cheaply creates a UserAttempts PDA for a wallet that received tPEG via transfer rather than via buy_tokens. Required before that wallet can ever forward tPEG onward, because the hook resolver needs the PDA to exist.
  • transfer_hook(amount), invoked by Token-2022 via the fallback handler. The protocol's hot loop. Validates the call's authenticity (Token-2022 owned source and destination, transferring flag true on both, mint matches project mint), branches on the destination, and either passes through as a no-op or creates a SpinCommitment.
  • reveal_spin, normally called by the same frontend that issued the commit. Reads SlotHashes, rolls success and rarity, and either mints the NFT or records the failure.
  • cleanup_expired_commit. Anyone can call to reclaim the rent from an abandoned commit after the reveal window closes.

The state types are:

  • Config, global, holds the project mint and curve parameters.
  • GameState, global counters: total_minted, total_bought, committed_count, minted_per_rarity, total_attempts, total_failed_tokens.
  • UserAttempts, per-wallet: next_index, total_bought, nfts_minted, pending_guaranteed_count.
  • SpinCommitment, per-commit, ephemeral. Created at hook-time, closed at reveal or cleanup.
  • SpegArt, per-NFT, permanent. Stores the 24×24 pixel grid and the entropy seed.

The shared utilities live in util.rs, which currently contains the create_pda_safely helper used everywhere we allocate a PDA. The art renderer lives in art.rs, ~600 lines that bake the palettes, the painter phases, and the tier overlays into a single deterministic function. The constants, every cap, every probability, every curve coefficient, live in constants.rs so they can be diffed in one place during reviews.

The frontend lives in app/. It is Next.js 16 with React 19 and Tailwind v4, deployed to Vercel. The on-chain art mirror is in app/src/lib/artifact-art.ts, the program wrapper is in app/src/lib/use-speg.ts, and the live feed of minted sPEG NFTs runs on a subscription to program logs.


What's deliberately not here

Worth listing, since you'll notice the absences.

There is no marketplace (for now). sPEG NFTs will appear on Magic Eden, Tensor, and any other Solana NFT marketplace once they index this collection. We don't run a market and we don't take royalties beyond the standard Metaplex 5% creator fee encoded in the metadata.

There is no DAO, no governance token, no treasury votes. The token called tPEG is a utility token for one purpose, burn-to-mint. It doesn't vote. We don't need it to.

There is no staking, no airdrop campaign, no points system. The collection is a fixed-supply set of pixel-art NFTs. You either own one or you don't.


A note about the name

SPEG is the project; tPEG is the bonding-curve token; sPEG is the NFT. The lowercase prefixes, t for "token", s for "Solana", make it easier to keep the two on-chain assets separate when you write about them. sPEG literally reads as "Solana peg", a small nod to the Token-2022 standard the whole protocol pegs onto. Pronounce them all the same: just say "speg".


Minting through an agent

The protocol was built so a human with a wallet can mint, and so an AI agent with the same wallet can mint exactly the same way. Both paths end at the same Token-2022 transfer hook, atomically, with the same guarantees.

There is a sister document at /agents.md written specifically for AI agents. It is a single drop-in integration guide. An agent fetches the markdown, reads it once, and from that point on knows the MCP endpoint, the five available tools, the burn-to-mint flow, and which signing backends are supported. No bespoke SDK per agent vendor. No documentation drift across clients. One file, one URL.

What an agent actually does

The MCP server lives at /api/mcp/mcp and exposes five tools that mirror the on-chain instructions:

  • get_status reads public state: current bonding-curve multiplier, remaining supply per rarity tier, the wallet's tPEG balance, the wallet's UserAttempts PDA, any pending commits.
  • register_holder returns the unsigned transaction needed to create a UserAttempts PDA for a wallet that doesn't have one yet. One-off, cheap, required before the wallet can ever forward tPEG onward.
  • buy_tokens returns the unsigned bonding-curve buy. Inputs: SOL amount, slippage. Output: unsigned tx the agent forwards to its signer.
  • mint_speg returns the unsigned commit tx (the 100,000 or 10,000 tPEG burn that lands in portal_vault and triggers the hook).
  • reveal_spin returns the unsigned reveal tx, scheduled at least two slots after the matching commit. The frontend bundles commit and reveal into one wallet popup; agents bundle them the same way, often via signAllTransactions.

The MCP server never signs. It only constructs unsigned transactions the agent's wallet would have to sign with the user's private key. Nothing on the server is drainable. A malicious caller can only build txs they have to fund and sign themselves.

Signing backends

The agent guide documents three signing paths the agent author can pick from, depending on how much custody friction they want:

  • Open Wallet Standard (OWS) is the lowest-friction path. The agent talks to a browser-extension wallet (Phantom, Solflare, Backpack) through the Wallet Standard interop spec. Every mint triggers a wallet popup the user clicks through. The user keeps full custody. Good for "Claude, mint me one" inside a browser session where the user is already signed into a wallet.
  • Turnkey is policy-gated programmatic signing inside a SOC2 HSM. Per-signature fee, zero operational footprint. The agent author whitelists the SPEG program ID and the Token-2022 program in the Turnkey policy; anything else gets rejected at the HSM, even if the agent has been compromised. Good for autonomous bots that need to mint without a human in the loop.
  • Privy server wallets is similar to Turnkey but the wallet lives inside Privy's infrastructure. Useful if the rest of the agent's stack already uses Privy for user auth.

The same agent code path works for all three. The difference is in how the unsigned tx gets signed and submitted; the protocol is identical.

What this means in practice

A user opens Claude, or Cursor, or whatever agent shell they prefer, and asks something like "mint me a sPEG, use the 100k tPEG flow". The agent fetches /agents.md, finds the MCP endpoint, calls get_status to confirm cap room, calls mint_speg to get the unsigned commit, hands it to the wallet, waits two slots, calls reveal_spin, hands that to the wallet too. About one second of wall-clock time between the wallet popups. The NFT lands.

The agent doesn't need to understand the bonding curve math, the transfer hook, or commit-reveal. The MCP server does the tx-construction; the agent is just plumbing between the user's intent, the user's wallet, and the chain.

This is also why the site renders an "If you are an AI agent" hint in the DOM of every page, with a link to /agents.md. A scraper that lands on the homepage finds the integration guide on the next hop without us shipping any per-client glue.


How to read the on-chain history

If you want to follow a specific mint:

  1. Find the buyer's wallet, look up their UserAttempts PDA (seeds ["user-attempts", wallet]). nfts_minted tells you how many they have minted; next_index is the index a new commit would use.
  2. For an existing NFT at index i, derive the NFT mint PDA (["speg-nft", wallet, i_le_bytes]) and the SpegArt PDA (["speg-art", nft_mint]).
  3. The SpegArt account contains the seed, the rarity, and the raw pixels. Run render_pixel_grid(seed, rarity) locally and you'll get back the same pixel array stored on-chain.
  4. The Metaplex metadata is at the standard Metaplex PDA (["metadata", token_metadata_program, nft_mint]) and contains the canonical name (SPEG <Rarity> #<rank>) and the URI to the renderer.

The transactions involved in any individual mint are listed in /spegs on the live feed page, with explorer links for the commit and the reveal. Every successful mint is also surfaced as a SpinEvent in the program logs, which is what the live feed subscribes to.


Final notes

We tried to build something small and honest. A pixel machine, a single coin slot, and a chain underneath that remembers. Every NFT that mints is a small print of something that didn't exist a second before, drawn by the program itself, kept by Solana for as long as Solana keeps running.

If that's the kind of thing you like, we hope you mint one.