01 / Overview
What is PIXI?
PIXI is one asset with two coordinated layers: divisible value for trading and indivisible canvases for making art.
At the amount layer, PIXI behaves like a conventional ERC-20 with 18 decimals. A wallet may hold or transfer any base-unit amount. At the canvas layer, every complete 1e18 units gives that wallet control of one numbered 8×8 image. The protocol continuously keeps those two facts aligned:
active canvases(account) = floor(balanceOf(account) / 1e18)
A canvas is not a separate NFT. It is persistent state attached to a numbered PIXI block. Its identity, bitmap, revision counter, owner, and wallet-relative placement are read from the token contract. The website is a renderer and transaction client; it is never the source of truth.
02 / Concepts
The mental model
Fraction
Any balance below a whole PIXI. It remains fully transferable but does not independently control a canvas.
Block
One numbered, persistent canvas identity activated by one whole PIXI. “Block,” “tile,” and “canvas” refer to the same 8×8 unit in different contexts.
Wallet mural
The visual composition of all tiles controlled by one account, arranged at signed integer tile coordinates.
Reserve
The contract-held inventory where a canvas waits when fractional transfers reduce a wallet’s whole balance without giving another wallet a whole unit.
Terminology
03 / User guide
Quick start
Connect on Ethereum
Open the Studio and connect a wallet holding PIXI. The interface reads liquid balance, blank canvas slots, and materialized canvases directly from the verified mainnet contract.
Paint a canvas
Paint a blank draft or select an existing canvas. Pick one of 16 colors and edit the 8×8 bitmap locally before spending gas.
Arrange your mural
Move canvases at wallet-relative coordinates. Add several blank drafts to build a larger painting and save them together.
Review and save
New drafts use one atomic activateCanvases transaction. Existing canvases use one atomic compose transaction.
Confirm onchain
Approve the transaction in your wallet and wait for confirmation. The Studio reloads contract state and links to the transaction in the configured explorer.
Gas is spent only when a mainnet transaction is signed and accepted. Closing or refreshing before saving discards unsaved edits.
04 / Interface
Studio control reference
Collection
Lists every active canvas returned by blocksOf(wallet). Each card shows its ID, preview, and x,y placement. Refresh discards local drafts and reloads the current source.
Wallet canvas
Renders all placed tiles as one mural. The size label is the bounding rectangle in pixels, not the sum of painted pixels. The ruby outline marks the selected tile.
Palette
Selects a four-bit color index from 0 through 15. The name is a UI label; only the numeric index is stored onchain.
Tools
Undo restores the last stroke or operation. Reset restores the selected tile’s last loaded bitmap. Clear fills it with Paper. Remix generates random palette indexes.
Selected tile
Displays the ID and placement. Arrow controls change tile coordinates locally. Negative coordinates are valid; the mural view normalizes its bounds for display.
State and save
Shows token balance, active canvases, owner, selected revision, and total current bitmap bytes. The save button is enabled only when at least one tile differs from the loaded state.
Bitmap strip
Displays the selected tile’s packed bytes32. Copying it is useful for verification, calldata generation, and independent rendering.
World
Groups canvases by current owner. Zoom changes rendering scale; the viewport itself scrolls. Selecting your group returns to the Studio.
Interaction and accessibility details
- Pointer dragging paints a continuous stroke. One pre-stroke snapshot is added to Undo when the pointer is released.
- Focused pixels respond to Enter and Space. Controls expose labels, selection state, and status messages to assistive technology.
- The coordinate readout uses mural pixels:
globalX = tileX × 8 + localXand the same rule for Y. - Undo history exists only in memory and is cleared when records are reloaded. Reset affects pixels, not tile placement.
- A move into an occupied coordinate is blocked before submission. The contract independently enforces the same uniqueness rule.
05 / Transactions
What happens when you save
The frontend packs each changed tile and calls compose(uint256[],bytes32[],int16[],int16[]). The arrays are parallel: item i in every array describes the same canvas. The contract first verifies that lengths match, every ID is owned by the caller, and no ID is duplicated. It clears affected placements, then saves each bitmap and applies each new placement.
Because all changes are in one EVM transaction, a cross-tile stroke cannot be half-saved. If ownership, calldata, placement, wallet approval, gas, or execution fails, every state change reverts. The UI keeps the local draft available after most failures so it can be retried.
The studio activates up to 32 painted and positioned drafts in one transaction.
The studio submits only changed canvases, polls the receipt, and verifies every bitmap, revision, and position after confirmation.
06 / World view
How PIXI World is assembled
World is an ownership-grouped projection, not a second onchain map. Records are grouped by the lowercase owner address. Each group renders its tiles at that owner’s wallet-relative coordinates, while the group itself is positioned by the frontend for browsing.
- World contains only canvases materialized in the live PIXI contract. No sample wallets or mock paintings are included.
- The Studio reads canvas IDs from
1up to the smaller ofmaterializedCanvasSupplyand the browser scan limit. - Unplaced records are assigned display-only open coordinates. This does not write placement to the contract.
- The bounded browser scan is intentionally simple. A production world should use an event indexer to avoid many RPC calls and to cover the full supply.
07 / Protocol
Balance boundaries and canvas control
For a transfer amount, the contract records each account’s whole balance before and after updating ERC-20 balances. The difference determines how many canvases leave the sender and how many arrive for the recipient.
| Balance | Whole balance | Active canvases | Effect |
|---|---|---|---|
0.42 | 0 | 0 | Fraction trades only |
1.42 | 1 | 1 | One painting authority |
3.80 | 3 | 3 | Three-tile mural |
12.0001 | 12 | 12 | Fraction above 12 changes no canvas |
Reconciliation cases
Matching counts move directly from sender to recipient.
Departing canvases move to the contract reserve.
Needed canvases activate from the reserve.
No canvas moves; only ERC-20 balances change.
The selected-canvas methods transferBlock and transferBlockFrom always transfer exactly one PIXI_UNIT together with a specified canvas. Standard transfer and transferFrom choose deterministically from the sender’s inventory when reconciliation requires a move.
08 / Encoding
64 pixels in one storage word
Every pixel is a palette index from 0 to 15, which fits in four bits. Sixty-four pixels therefore occupy exactly 256 bits, or one bytes32.
Pixels use row-major coordinates. Index i = y × 8 + x. Pixel zero occupies the least-significant nibble; pixel i is shifted left by i × 4 bits.
packed |= BigInt(color) << BigInt((y * 8 + x) * 4)
color = Number(
(packed >> BigInt((y * 8 + x) * 4)) & 0xfn
)
The 16-color palette is fixed by convention in the frontend. The contract validates the four-bit range for paintPixel, but a full bytes32 bitmap is already structurally valid because every nibble is necessarily between 0 and 15. Color names and hex display values are renderer metadata.
09 / Composition
Wallet-relative placement
Each canvas may have an int16 x, int16 y, and bool placed. Coordinates are tile units, so adjacent (0,0) and (1,0) form a 16×8 image. They are meaningful only within the current owner’s wallet.
- Negative coordinates are valid and let a mural expand in any direction.
- One owner cannot place two tiles at the same coordinate.
placeManyclears all affected coordinates before applying new ones, allowing atomic tile swaps.- Transferring a canvas clears its placement. Neighboring tiles keep their positions, so the mural retains a visible gap.
- The recipient must place the arriving canvas in their own coordinate system.
10 / Lifecycle
Transfers, lazy inventory, and reserve
Genesis ownership is implicit. At deployment the full balance belongs to genesisOwner, but thousands of canvas owners are not written individually. ownerOf derives untouched ownership, and IDs are materialized only as transfers require it. This makes fixed-supply deployment constant-cost with respect to supply.
The initial whole-unit deposit from genesis owner to PoolManager has a dedicated O(1) path that reserves a contiguous implicit ID range. As pool transfers cross whole boundaries, IDs are consumed deterministically from that range.
The reserve changes control, not identity or art. canvasOf and revisionOf remain attached to the ID. A reserved canvas cannot be painted because the contract—not a user—is its current owner.
11 / Uniswap v4
The token is its canonical hook
The core PIXI contract also implements a deliberately narrow Uniswap v4 hook. Its deployment address must encode exactly two permission flags: beforeInitialize and afterSwap. A CREATE2 salt therefore has to be mined before deployment.
Accepts calls only from the immutable PoolManager, only when sender is the launch authority, only once, and only for the exact native-ETH/PIXI pool key.
Accepts only the canonical PoolManager and pool key, increments worldEpoch, and emits a compact WorldSwap pulse. It returns zero hook delta.
The validated pool key requires native ETH as currency0, PIXI as currency1, the token itself as hooks, and immutable fee and tick spacing values. Swap settlement never invokes painting logic and ordinary routers do not need to understand canvases.
12 / Solidity API
Core contract reference
ERC-20 amount interface6 items
balanceOf(address) | Base-unit balance; divide by 1e18 for PIXI. |
allowance(address,address) | Approved base-unit spend. |
approve(address,uint256) | Sets allowance and emits Approval. |
transfer(address,uint256) | Moves amount and reconciles whole-balance canvas changes. |
transferFrom(address,address,uint256) | Spends allowance, then follows standard transfer reconciliation. |
wholeBalanceOf(address) | Returns balanceOf / PIXI_UNIT. |
Canvas ownership & reads9 items
ownerOf(uint256) | Current owner; zero for an invalid ID. |
blocksOf(address) | All active IDs for an account. |
blockAt(address,uint256) | ID at an owner inventory index. |
canvasOf(uint256) | Current bytes32 bitmap. |
revisionOf(uint256) | Number of bitmap saves. |
placementOf(uint256) | Returns (int16 x,int16 y,bool placed). |
blockAtPosition(address,int16,int16) | Canvas at an owner-relative coordinate, or zero. |
colorAt(uint256,uint8,uint8) | Four-bit palette index at a pixel coordinate. |
canvasSupply() | Fixed number of persistent canvas IDs. |
Painting & placement8 items
paint(uint256,bytes32) | Replaces one owned bitmap. |
paintPixel(uint256,uint8,uint8,uint8) | Updates one owned pixel. |
paintMany(uint256[],bytes32[]) | Atomically replaces multiple owned bitmaps. |
place(uint256,int16,int16) | Moves one owned tile. |
placeMany(uint256[],int16[],int16[]) | Atomically rearranges multiple tiles, including swaps. |
unplace(uint256) | Clears an owned tile’s placement. |
compose(ids,pixels,xs,ys) | Saves bitmaps and placements together. |
transferBlock(uint256,address) | Moves exactly one PIXI with a selected canvas. |
Immutable deployment & hook state12 items
totalSupply / canvasSupply | Fixed token base units and canvas count. |
genesisOwner / launchAuthority | Initial inventory owner and authorized pool initializer. |
poolManager / poolFee / poolTickSpacing | Canonical pool parameters. |
poolInitialized / worldEpoch | Hook lifecycle state. |
beforeInitialize(...) | Authenticates the one allowed canonical pool initialization. |
afterSwap(...) | Advances the world epoch for canonical swaps. |
13 / Indexing
Events and failure modes
Current state belongs in contract reads. Historical state belongs in event logs. An indexer can reconstruct transfers, every bitmap revision, placements, reserve transitions, pool initialization, and swap epochs.
TransferERC-20 amount movementApprovalAllowance changesBlockTransferredCanvas ownership movementCanvasReservedCanvas entered reserveCanvasActivatedCanvas left reserveCanvasUpdatedBitmap, editor, revisionBlockPlacedNew wallet coordinateBlockUnplacedCleared coordinateGenesisBlocksImplicit genesis rangeBlocksReservedImplicit pool rangeCanonicalPoolInitializedInitial pool priceWorldSwapEpoch and compact swap dataCommon reverts
NotBlockOwnerPositionOccupied(id)ArrayLengthMismatchDuplicateBlockInvalidCoordinates / InvalidColorInvalidPool14 / Web architecture
A zero-build browser client
The web application uses static HTML, CSS, and native JavaScript modules. There is no framework, bundler, backend session, or application database.
apps/web/
index.htmlStudio structure and product explanationapp.jsProduction Studio entry moduletestg/testg.jsLive state, painting, batch saves, read-back, and Worldchain.jsMinimal ABI encoding/decoding and EIP-1193 clientcodec.jsPalette and bitmap packingconfig.jsPublished Ethereum and PIXI contract configurationdocs/This documentation experienceState flow
On load, the Studio reads the live materialized canvas count and world state without a wallet. After connection it requests Ethereum mainnet, then reads the wallet’s liquid balance, blank slots, and owned canvases through eth_call.
The client hand-encodes only the function selectors and ABI shapes it uses. This keeps the app dependency-free but means selectors and deployed ABI versions must stay synchronized. Read-only world scans are batched in groups of 12 concurrent calls.
15 / Integrators
Choosing the right transfer path
Use ERC-20 methods
When canvas identity does not matter, use transfer, transferFrom, allowances, and standard 18-decimal amounts. Boundary reconciliation is automatic.
Use block-aware methods
When the buyer or sender expects a specific image, use transferBlock or transferBlockFrom. Never assume which ID a generic amount transfer will select.
Use compose for murals
Submit only changed owned IDs. Pack pixels exactly, encode signed 16-bit placement values as sign-extended ABI words, and treat the entire call as one atomic draft.
Combine reads and logs
Use events for history and ownership changes, then reconcile against current getters. Handle implicit genesis/pool ownership and the contract reserve explicitly.
Integration checklist
16 / Development
Run, configure, and test
Requirements are Node.js 20+ and Foundry. From the PIXI directory:
npm run dev
# Open http://127.0.0.1:4173
npm test
npm run check
forge test
Runtime configuration
| Key | Purpose | Default |
|---|---|---|
chainId | Hex EIP-155 chain ID used for wallet switching | 0x1 |
chainName | Human-readable wallet/network label | Ethereum |
contractAddress | Verified PIXI mainnet contract | 0xEE6f…60cc |
explorerUrl | Base for transaction links and wallet chain metadata | Etherscan |
rpcUrl | RPC URL supplied if the wallet must add the chain | Publicnode Ethereum |
worldScanLimit | Maximum materialized IDs scanned directly by the browser World view | 256 |
The production entry is permanently connected to PIXI on Ethereum mainnet. The static server maps / and /docs/ to their route entry files and disables caching for development.
17 / Safety
Security assumptions and limits
The core contract is experimental and has not received an independent security audit. Do not treat it as production-ready custody software.
Painting and placement transactions have no separate editor role: the current canvas owner signs directly. A compromised wallet controls both balance and art.
Default RPC endpoints can rate-limit, fail, or expose request metadata. Production deployments should use reliable infrastructure and an indexer.
Verify chain, contract address, calldata intent, and transaction value in the wallet. A correct contract cannot make a modified frontend trustworthy.
World may show only the configured prefix of canvas IDs. Absence from the browser view does not prove absence onchain.
A repaint replaces current storage but emits the new revision publicly. Past artwork can remain reconstructable from logs.
18 / FAQ
Frequently asked questions
Do I need one full PIXI to trade?
No. Any fractional amount can be transferred or traded. A complete PIXI is required only for one active canvas.
Is each canvas an NFT?
No. Canvas identity is tracked inside the fractional ERC-20 contract and follows its whole-balance invariant. There is no ERC-721 transfer surface.
What happens to my art if my balance falls below a whole token?
The detached canvas moves to the contract reserve with its bitmap, revision, and ID intact. Its placement is cleared. It can later activate for an account crossing a whole-token boundary.
Can two tiles overlap?
No. Each owner may have only one tile at a given coordinate. The Studio prevents the move locally and the contract reverts conflicting placement.
Can a stroke cross tile boundaries?
Yes. The Studio edits each affected bitmap locally and submits all changed tiles in one compose call.
Does Reset undo a moved tile?
No. Reset restores the selected tile’s loaded bitmap. Use Undo for a recent placement change, or move the tile back before saving.
Why does the displayed mural size change?
It is the bounding box of placed tile coordinates. Moving an edge tile outward adds eight displayed pixels to that axis; gaps still count inside the bounds.
Can ordinary Uniswap trades choose a particular canvas?
No. Generic amount transfers use deterministic reconciliation. A dedicated block-aware transfer or router is required when canvas identity matters.
Is the Studio onchain?
Yes. The production Studio reads PIXI on Ethereum mainnet. Painting remains local until you approve a save; confirmed saves are then read back from the contract.
Where is complete canvas history stored?
The current bitmap and revision count are contract state. Each saved revision is emitted in CanvasUpdated, so an indexer can reconstruct history from logs.
Ready to use it?