# 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](/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.
