Protocol reference
Documentation
A SOBJECT is a fungible Token-2022 balance drawn as a 64 by 64 object. The token is the asset. The picture is a pure function of who holds it, how many whole tokens they hold, and how far the curve has climbed. This page is the integer specification: DNA, the constant-product curve, fees, evolution, and the parts this deployment does not pretend are finished.
Agents can follow the installable skill in the repository at skills/sobject/SKILL.md.
What is a SOBJECT
Holding one whole token materializes one object. The object is not a second mint, and it is not a picture that can be listed on its own. Transfer the tokens and the pictures are recomputed for the wallets that remain. Sell down to the same balance later and the same pictures return.
Every launch uses the same supply. The mint has 6 decimals, so one whole token is 1,000,000 base units and the full supply is 1,000,000,000 base units: 1,000 tokens. The curve account holds 800 of them. A migration account holds the other 200 until ascension. A wallet balance of 3.42 tokens is three objects. The leftover 420,000 base units do not mint a fourth.
1,000.000000 tokens = 1,000,000,000 base units
The first market can only be opened by the protocol authority. That mint becomes the main token, SOBJECT. Later launches are permissionless. None of them set a creator fee, and none of them can withdraw the curve's principal. An admin may pause new launches and curve trading. The pause does not rewrite balances, DNA, or past trades.
The site you are reading quotes this math and reads the cluster. It does not invent markets, volume, or a successful graduation.
DNA
DNA is 32 bytes. It is SHA-256 of a fixed preimage. Nothing in the account stores a picture per holder, and the transfer hook does not write DNA. Rendering reads the balance, the collection seed, the mint, and the wallet, then hashes.
01
SOBJECT_V1
10 ASCII bytes
02
seed
32 bytes
03
mint
32 bytes
04
wallet
32 bytes
05
index
u32 LE
DNA = SHA256(
"SOBJECT_V1"
|| collection_seed // 32 bytes
|| mint // 32 bytes
|| wallet // 32 bytes
|| object_index // u32, little-endian
)The prefix is the ten ASCII bytes of SOBJECT_V1. The object index is the zero-based count of whole tokens in that wallet: the first object is index 0, the second is index 1. Little-endian means the least significant byte comes first, so index 1 is the four bytes 01 00 00 00.
The same inputs always produce the same DNA. Sell down and buy back to the same whole-token balance, in the same wallet, and the same objects return. A different wallet holding the same number of the same mint gets different objects, because the wallet is inside the hash. Two objects in one wallet differ because the index is inside the hash.
The collection seed is shared by every holder of that mint. Its first four bytes are the pack style:
| Byte | Field | Meaning |
|---|---|---|
| 0 | Palette | Taken modulo 3. Each pack has three color ramps. |
| 1 | Eyes | 0 round, 1 tall, 2 sleepy. Some bodies lock the drawing anyway. |
| 2 | Marking | 0 spots, 1 stripe, 2 freckles, 3 cheek, 4 clean. |
| 3 | Charm | 0 none, 1 horns, 2 glasses, 3 bow, 4 spark. |
| 4 to 31 | Salt | The rest of the seed. Two collections with the same four choices still diverge. |
Style picks the species. DNA picks the individual: expression, hair on a portrait, where speckles land, and which side a charm sits on. Hair, expression, speckles, and charm side come from mixing the DNA. They are not extra fields in the seed.
Transform, not transfer
Objects do not move. When Alice sends tokens to Bob, Alice's remaining balance redraws her objects and Bob's new balance draws his. If Alice keeps two whole tokens, she still has objects 0 and 1 from her own DNA. Bob's first whole token is his object 0. It is a new hash, with Bob's wallet in the preimage.
Walk a transfer of 2.5 tokens from a wallet that holds 4:
| Alice before | Alice after | Bob before | Bob after | |
|---|---|---|---|---|
| Balance | 4.0 | 1.5 | 0 | 2.5 |
| Objects | 0, 1, 2, 3 | 0 | none | 0, 1 |
Alice's surviving object is still her index 0. Bob's two objects are his index 0 and index 1. The 0.5 token on each side is dust. Dust changes the balance and does not change the object count.
The Token-2022 transfer hook on the mint is permanent. Mint authority is revoked at launch, and the hook authority is none, with the hook program set to sobject-core. Official Token-2022 updates balances before it calls the hook, so the hook sees the post-transfer amounts. Its job on that path is lightweight: it does not write an object account. Rendering happens off the transfer, from the balance and the seed.
There is no inventory of unique items to trade one by one. The items were never separate assets. Restoring a balance restores the picture.
Packs
There are ten bodies, drawn at 64 by 64 with a one pixel outline and flat color bands. The background of the SVG is transparent. The site shows them on the same black as the mark.
| Id | Pack | What the body locks |
|---|---|---|
| 0 | Egg | Face only at ascended and final form. The crack grows with the stage. |
| 1 | Blob | Soft body, blush, feet. |
| 2 | Totem | Three stacked faces. |
| 3 | Mech | Visor. The eye byte is stored, and the drawing stays a visor. |
| 4 | Beast | Ears, muzzle, tail. |
| 5 | Glyph | Tall eyes, even if the seed asked for another eye style. |
| 6 | PFP | Portrait. Hair is five variants from the holder DNA. |
| 7 | Spirit | Scalloped body and a glow. |
| 8 | Knight | Visor, plume, shield. |
| 9 | Relic | Gem on a pedestal. |
On Create you either take a ready-made pack or generate one. Generate rolls a palette (3 choices), an eye style (round, tall, sleepy), a marking (spots, stripe, freckles, cheek, clean), and a charm (none, horns, glasses, bow, spark). Mech and Knight always draw a visor. Glyph always draws tall eyes. Those locks are part of the body. The eye byte is still written into the seed so the recipe stays explicit.
Stages change the same grid. Dormant closes the eyes and darkens the bands. Awake opens the eyes. Mutated adds the marking and the charm. Ascended adds a ring. Final form adds a crown. An egg stays a closed shell until ascended, then the shell opens onto a face. A portrait's hair comes from the holder DNA, so one collection can hold crops, bobs, long hair, a tuft, or a side sweep. The shirt and the glasses, if you chose them, stay with the collection.
Collection style
Style is not a second renderer id. The on-chain pack id is still a u16, one of the ten bodies, registered before the launch. The recipe lives in the collection seed. Preview on Create shows the shared look, the five evolution stages, and three wallets with the same style and different DNA.
That preview does not launch a mint. The Create button reads the protocol account and reports whether a transaction was built. With the protocol uninitialized, the answer is that nothing was launched.
The TypeScript renderer and the Rust renderer consume the same sprite grids. A fixture hash covers all ten packs, all five stages, and two sample seeds. If those hashes diverge, the pictures diverged.
Curve math
The curve is constant product on virtual reserves. Every division floors. The invariant k is the product of the two virtual reserves at launch, and buys keep the product by moving along integer steps of that same k.
| Constant | Value | Unit |
|---|---|---|
| Virtual token at launch | 1,066,666,666 | base units (1,066.666666 tokens) |
| Virtual SOL at launch | 6,666,666,667 | lamports (6.666666667 SOL) |
| k | 7,111,111,107,022,222,222 | virtual token × virtual SOL |
| Curve inventory | 800,000,000 | base units |
| Migration inventory | 200,000,000 | base units |
| Graduation | 20,000,000,000 | lamports of real SOL, after fees |
A buy takes the SOL the signer sends, peels the fee, then slides the reserves:
fee = floor(sol_in * bps / 10_000)
net = sol_in - fee
k = virtual_sol * virtual_token
new_vs = virtual_sol + net
new_vt = floor(k / new_vs)
tokens_out = virtual_token - new_vttokens_out is also capped by the real token inventory on the curve. The virtual token reserve starts above 800 tokens so the spot price can rise as inventory leaves. The extra virtual tokens are not spendable. Only the 800 real tokens can be delivered.
Spot price, in lamports per whole token:
spot = floor(virtual_sol * 1_000_000 / virtual_token)On the fresh curve that is 6,666,666,667 * 1,000,000 / 1,066,666,666 = 6,250,000 lamports, which is 0.00625 SOL per token. The chart below rebuilds virtual SOL as floor(k / virtual token) after each sold amount. That is the integer hyperbola. The dots are the checkpoints in the table.
| Tokens sold | Lamports / token | SOL / token |
|---|---|---|
| 0 | 6,250,000 | 0.00625 |
| 200 | 9,467,455 | 0.009467455 |
| 400 | 16,000,000 | 0.016 |
| 600 | 32,653,061 | 0.032653061 |
| 800 | 100,000,000 | 0.1 |
Worked buy: 1 SOL on a fresh main market
The signer sends 1,000,000,000 lamports. The main fee is 300 basis points.
fee = floor(1_000_000_000 * 300 / 10_000) = 30_000_000
net = 970_000_000
new_vs = 6_666_666_667 + 970_000_000 = 7_636_666_667
new_vt = floor(k / new_vs) = 931_179_979
tokens_out = 1_066_666_666 - 931_179_979 = 135_486_687The buyer receives 135.486687 tokens. Real SOL becomes 0.97. Real token inventory falls by the same 135,486,687 base units. A later token with the same 1 SOL input pays a 10,000,000 lamport fee and receives 137,919,025 base units, because more of the SOL reaches the curve.
The buy that finishes the curve
Net 20 SOL buys the entire 800 token inventory in one shot from a fresh curve. After that trade the virtual token reserve is 266,666,666 and the virtual SOL reserve is 26,666,666,667 lamports. Spot price is 100,000,000 lamports per whole token: 0.1 SOL.
Rebuilding virtual SOL as floor(k / 266,666,666) yields 26,666,666,718 instead. The 51 lamport gap is floor dust. The product of the post-trade reserves sits slightly under k. Both states quote a spot of 0.1 SOL, because the floor division lands on the same lamport price.
The gross amounts that leave exactly 20 SOL after the fee:
| Market | sol_in (lamports) | Fee | Net |
|---|---|---|---|
| Main, 300 bps | 20,618,556,701 | 618,556,701 | 20,000,000,000 |
| Later token, 100 bps | 20,202,020,202 | 202,020,202 | 20,000,000,000 |
Sells
A sell adds the tokens to the virtual token reserve and removes SOL:
new_vt = virtual_token + token_in
new_vs = floor(k / new_vt)
gross = virtual_sol - new_vs
gross = min(gross, real_sol)
fee = floor(gross * bps / 10_000)
net = gross - feeThe cap matters. Floor division on the way out and the way back can ask for more lamports than the vault holds. The program pays the vault. It does not mint SOL to fill the gap. A sell quote without a live reserve is rejected, because that cap cannot be known from a blank curve.
Migration sizing
The 200 token migration inventory is a separate token account. At the terminal spot, 200 whole tokens would ask for 200,000,000 * 26,666,666,667 / 266,666,666 = 20,000,000,050 lamports: 20 SOL plus 50 lamports. The real reserve only has 20 SOL, so the sizer trims.
tokens = floor(real_sol * virtual_token / virtual_sol) = 199,999,999
sol = floor(tokens * virtual_sol / virtual_token) = 19,999,999,950One base unit of the migration pile stays behind, and 50 lamports of SOL stay in the curve vault. The SOL leg never exceeds the real reserve. This build does not move either pile into a pool. The formula is what a future deposit has to satisfy.
Fees
Fees come out of the SOL leg inside the curve program, and after ascension they are intended to come out in a router in front of the pool. They are not a Token-2022 transfer-fee extension. A transfer of the token itself does not skim a tax. Sending tokens between wallets is free of protocol fee. The fee is on the trade.
Main token, 300 bps
0.03 dev
Later token, 100 bps
0.01 buys SOBJECT
The main token, SOBJECT, pays 300 basis points of the SOL leg to the dev wallet stored on the protocol account. That address is configuration, set from the environment and written into the protocol account. It is not a personal key baked into the program binary.
Every later token pays 100 basis points. That SOL is not left as a tip. It is the input of a second buy on the main curve, with fee_bps = 0, and the main tokens from that buy land in a protocol treasury. Because the inner buy uses a zero fee, the 3% is not charged again. The child trade requires the main market to be ACTIVE. If the main market is paused, missing, or already graduating, the child trade stops.
The fee uses floor division, so a small trade can round down to a fee of zero. One lamport on either market pays nothing.
| Market | Last sol_in with a 0 fee | First sol_in with a 1 lamport fee |
|---|---|---|
| Main, 300 bps | 33 | 34 |
| Later token, 100 bps | 99 | 100 |
fee + net equals sol_in exactly. Graduation compares real_sol after the net is added, so a buy that crosses the target is sized on the post-fee amount. The table in Curve math is that size: 20,618,556,701 lamports on the main token, 20,202,020,202 on a later token.
After ascension, a trade that talks to Meteora directly can skip a router-only fee. This site does not claim the fee is unavoidable on the pool until a test shows on-chain enforcement that still passes the DLMM and hook tests.
Ascension
Graduation uses post-fee real reserves, not the user's gross spend. The production target is 20 SOL. The buy that pushes real_sol to the target completes, the buyer receives the tokens, and then the status flips to GRADUATING. Curve trading stops on that market. The status never moves backwards.
1 / 1 ACTIVE
Launch writes this. Curve trading is open.
2 / 2 GRADUATING
The crossing buy completed. Curve trading stops.
3 / 3 MIGRATED
Would require a verified pool. This build does not write it.
Finalize cannot succeed without a verified pool. The instruction checks the pool and then returns MeteoraDepositUnimplemented. It does not set status to MIGRATED. migrate on the liquidity program checks that the market is GRADUATING and that the DLMM program account is executable, then returns MeteoraPoolNotCreated. It does not move curve SOL or the migration inventory.
A dev-only graduation threshold exists behind the Cargo feature dev-thresholds. That feature is not the default build. The production program rejects any target other than 20 SOL. The dev feature rejects the 20 SOL target, so a dev artifact cannot be initialized as if it were the production target, and a production artifact cannot be initialized with a toy target.
Meteora
The pool venue is Meteora DLMM, program LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo on both mainnet and devnet. This project does not use Meteora Dynamic Bonding Curve. The hook stays on the mint. Disabling the hook, or swapping the venue and calling it done, is not the plan.
A transfer hook that still runs custom logic needs a Meteora TokenBadge. That badge is permissionless only when the hook program and its authority are revoked. Revoking them would make the hook optional, which this protocol does not do. Until a badge and a real deposit are proven, the router refuses a post-ascension swap and does not move reserves while it refuses.
The repository document docs/METEORA_INTEGRATION.md records the same gap. A green test that fakes a pool is not a migration. Final form in the renderer is reserved for a market whose status is actually MIGRATED.
Wallets
Phantom and Solflare connect in the browser. The header control says Select wallet until one is connected, then it shows a shortened address and opens the same modal. The default cluster is devnet. A connected wallet is not a trade.
Buy and sell on a market page quote the live curve. They do not offer a button that pretends a swap was sent. The server does not submit the transaction. Minimums, when a signer builds a transaction locally, are what protect them if the reserves move.
The devnet programs are deployed only after a real airdrop funds the deployer. If the faucet refuses, the site keeps an empty launch list and says so. It does not invent a market to fill the grid.
Errors
Slippage, an empty quote, a paused protocol, a string that is not a mint, or a main market that cannot absorb the child-fee buy all stop the action. The interface prints the failure. It does not replace it with a success toast.
| Situation | What you should see |
|---|---|
| Indexer unreachable | An error, not a report of zero volume. |
| Indexer returns no trades | An empty activity list. |
| Sell quote, no live reserve | The quote is rejected. The vault cap is unknown. |
| Protocol account missing | Create reports that nothing was launched. |
| Bad mint string | The token route says the string is not a mint and does not query the cluster. |
| Well-formed mint, no market | The page says there is no curve account. |
Object index
Object count is the balance divided by 1,000,000, integer division. Index 0 is the first whole token. Index 1 is the second. The index is part of the DNA hash, so object 0 and object 1 in the same wallet are different pictures even though they share the collection style.
| Balance (base units) | Whole tokens | Objects |
|---|---|---|
| 0 | 0 | none |
| 999,999 | 0.999999 | none |
| 1,000,000 | 1 | index 0 |
| 1,820,000 | 1.82 | index 0 |
| 3,420,000 | 3.42 | indexes 0, 1, 2 |
A wallet page lists an object only when a real Token-2022 balance covers that index. It does not pad the grid with specimens. Pagination is 12 cards. Fractional dust does not add a card.
Slippage
Buys require a minimum token out. Sells require a minimum SOL out. The program rejects the trade when the quote misses that floor. The quote you see is floor division on the virtual reserves after the fee, so two readers of the same reserves see the same integers.
A quote is not a fill. The market can move between the preview and a signed transaction. If it does, the minimums are what protect the signer. This deployment does not submit that transaction from the server.
On the fresh main curve, the 1 SOL example above is exact: minimum tokens of 135,486,687 fills if nobody trades first, and a higher minimum fails if the reserves have already moved. There is no hidden spread on top of the floor division.
Evolution
Stage follows the share of the 20 SOL target already sitting in the curve as real reserves. The share is basis points, floored:
bps = floor(real_sol * 10_000 / target)Under 2,000 the stage is Dormant. Under 5,000 it is Awake. Under 8,000 it is Mutated. From 8,000 upward, while the market is not migrated, it is Ascended. Final form is reserved for a migrated market. On a 20 SOL target the cuts fall on 4 SOL, 10 SOL, and 16 SOL, and the boundary itself belongs to the later stage.
Final form sits outside this bar. It replaces the stage only when status is migrated.
| Stage | Basis points | Real SOL on a 20 SOL target | Drawing |
|---|---|---|---|
| Dormant | < 2,000 | < 4 | Closed eyes, darker bands. |
| Awake | 2,000 to 4,999 | 4 to just under 10 | Eyes open. |
| Mutated | 5,000 to 7,999 | 10 to just under 16 | Marking and charm. |
| Ascended | ≥ 8,000 | 16 through 20 | Ring. An egg's face appears here. |
| Final form | migrated | any, once migrated | Crown. This build does not write the status. |
A sell that drops the reserve can move the stage back down. The picture follows the curve. It is not a one-way story. The TypeScript renderer and the Rust renderer use the same integer steps.
Reference
Addresses the client is built against. A market exists on a cluster only after these programs are initialized there. An empty launch list means they are not initialized on the cluster this site is reading.
| Program | Address |
|---|---|
| sobject-core | 28VVRHM6NP7eqbRxjMipvfWcpGVqzJBCpMaZQHCDMinK |
| sobject-curve | AgH6eb6LJqZowpQqGvL3FFkG7F9n9FsW9632ePcHqBSq |
| sobject-liquidity | DtLKxr2XNbkohZ9HCAUpXozAKvBSuTi3zoFnm37p6g7G |
| Token-2022 | TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb |
| Meteora DLMM | LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo |
| Symbol | Value |
|---|---|
| Decimals | 6 |
| Object unit | 1,000,000 base units |
| Supply | 1,000 tokens |
| Main fee | 300 bps to the configured dev wallet |
| Later-token fee | 100 bps, buys main at 0 bps into the treasury |
| Graduation | 20 SOL real reserve after fees |
| Packs | 10 bodies, 64 by 64 |
| DNA | SHA-256, 32 bytes, not stored per holder |
