Overview

Fully onchain 40x40 pixel art PFPs on Ethereum.

Ethereans are fully onchain 40x40 pixel art PFPs on Ethereum. Each Etherean is linked to an X (Twitter) handle and comes with its own ERC-6551 Token Bound Account (TBA), a smart contract wallet owned by the NFT itself.

Key Properties

  • >
    Fully onchain SVG: no IPFS, no external dependencies. The pixel data is stored directly in the contract and rendered as SVG at tokenURI time.
  • >
    4-color palette system: 8 named palettes inspired by the Game Boy color space (classic, pink, ocean, amber, grayscale, lavender, crimson, mint).
  • >
    Gasless minting: users never connect a wallet. A protocol relayer pays gas, funded by the $ADHD treasury.
  • >
    ERC-6551 TBA: every token gets a deterministic Token Bound Account created atomically during mint.
  • >
    One per handle: each X handle can only mint once (enforced onchain via case-insensitive hash). X user IDs are stored onchain for post-mint analytics and potential airdrop blacklisting.

Current Status

Testnet

Live on Sepolia testnet with 558+ mints. Smart contract, backend, and frontend audit in progress. Mainnet deployment planned after audit completion.

Architecture

End-to-end system design: browser to blockchain.

System Diagram

Browser (console.gami.vc) | |-- 1. Enter X handle, generate pixel art |-- 2. Select palette, enter receive address (0x or ENS) |-- 3. Post verification tweet with nonce |-- 4. Click "I've Posted It" (triggers verify) |-- 5. Click "Mint to Ethereum" (triggers mint) | v Cloudflare Worker (ethereans-api.ethereans.workers.dev) | |-- POST /api/verify: searches X for nonce tweet, returns attestation |-- POST /api/mint: validates attestation, signs EIP-712, submits tx | v Ethereum (Sepolia / Mainnet) | |-- Etherean.sol: ERC-721, onchain SVG, handle uniqueness |-- SVGRenderer.sol: pure library, unpacks 2bpp pixel data, renders SVG |-- ERC-6551 Registry: creates TBA per token atomically on mint

Key Design Decisions

  • No wallet connection required. Users provide a receive address manually.
  • EIP-712 typed data signatures bind the verifier attestation to the exact mint parameters (handle, to, svgData, palette, nonce, expiry).
  • Separate relayer and verifier keys. The relayer is a funded EOA that submits transactions. The verifier is a separate EOA that signs attestations. These are different keys for security.
  • ENS resolution happens client-side via the ensdata.net public API.

Smart Contract

Etherean.sol and SVGRenderer.sol on Ethereum.

Etherean.sol

StandardERC-721 (OpenZeppelin)
ChainEthereum (Sepolia testnet, Mainnet planned)
CompilerSolidity ^0.8.24
FrameworkFoundry (Forge)
SymbolETRN

Constructor Parameters

ParameterDescription
owner_Admin address (can update relayer/verifier)
relayer_Address authorized to call mint()
verifier_Address whose EIP-712 signatures are trusted
tbaImplementationERC-6551 account implementation address
erc6551RegistryCanonical registry (defaults to 0x000000006551c19487814612e58FE06813775758 if zero)

Mint Function

function mint(
    address to,
    string calldata handle,
    bytes calldata svgData,
    string calldata palette,
    bytes32 nonce,
    uint256 expiry,
    bytes calldata signature
) external onlyRelayer returns (uint256 tokenId, address tba)

Mint Flow (7 Steps)

  1. Check expiry timestamp
  2. Check and consume nonce (prevents replay)
  3. Derive case-insensitive handle hash
  4. Enforce handle uniqueness
  5. Verify EIP-712 signature against verifier
  6. Validate svgData is exactly 400 bytes
  7. Mint ERC-721 token, store data, create ERC-6551 TBA

EIP-712 Domain

name: "Etherean"
version: "1"
chainId: (current chain)
verifyingContract: (contract address)

EIP-712 Mint Type

Mint(string handle, address to, bytes svgData, string palette, bytes32 nonce, uint256 expiry)

View Functions

FunctionReturns
totalSupply()Current token count
getTokenByHandle(string)tokenId for a handle (case-insensitive)
getHandle(uint256)Original handle string
getSVGData(uint256)Raw 400-byte pixel data
getTBA(uint256)Deterministic TBA address
tokenURI(uint256)Fully onchain data URI with JSON metadata and base64 SVG

Admin Functions (onlyOwner)

  • setRelayer(address): update the relayer address
  • setVerifier(address): update the verifier address
  • transferOwnership(address): transfer contract ownership (inherited from OpenZeppelin Ownable). Recommended: transfer to a hardware wallet before mainnet.

Custom Errors

HandleAlreadyMinted InvalidSignature ExpiredSignature NonceAlreadyUsed InvalidSVGData OnlyRelayer ZeroAddress

SVGRenderer.sol

A pure library that:

  • Unpacks 400 bytes of 2-bit packed pixel data (1,600 pixels total)
  • Resolves palette colors from a string key (8 palettes hardcoded)
  • Renders a complete SVG document with 1,600 rect elements
  • Uses assembly-backed buffer writes for gas efficiency

Bit Layout

Pixel index i is in byte i/4, at bit-offset (3 - (i % 4)) * 2 (MSB first).

Token Metadata Format

The tokenURI returns a base64-encoded JSON object:

{
  "name": "Etherean #1",
  "description": "Fully onchain 40x40 pixel PFP on Ethereum. @handle",
  "image": "data:image/svg+xml;base64,...",
  "attributes": [
    { "trait_type": "Handle", "value": "gami_vc" },
    { "trait_type": "Palette", "value": "classic" }
  ]
}

API

Cloudflare Worker endpoints for verification and minting.

POST /api/verify

Searches X for a verification tweet containing the nonce. The user's tweet must contain all three of: the nonce string, the word "Etherean", and "@gami_vc".

Request

{
  "handle": "gami_vc",
  "nonce": "A7K2F9"
}

Response (success)

{
  "verified": true,
  "handle": "gami_vc",
  "nonce": "A7K2F9",
  "tweetId": "1234567890",
  "expiry": 1709312400,
  "message": "Verified! Ready to mint."
}

Response (failure)

{
  "verified": false,
  "message": "Tweet not found. Please post the tweet and try again."
}

Rate limiting: 10 requests per IP per minute. Returns 429 with Retry-After header.

POST /api/mint

Validates the attestation, signs the EIP-712 message with the real mint parameters, and submits the transaction via the relayer.

Request

{
  "handle": "gami_vc",
  "to": "0x1234...abcd",
  "svgData": "0x...",
  "palette": "classic",
  "expiry": 1709312400,
  "nonce": "A7K2F9"
}

Response (success)

{
  "success": true,
  "tokenId": 42,
  "txHash": "0xdef...",
  "tba": "0x789...",
  "etherscanUrl": "https://etherscan.io/tx/0xdef..."
}

Attestation expiry: 30 minutes. The mint handler rejects attestations expiring in fewer than 30 seconds.

GET /health

{ "status": "ok", "service": "ethereans-api" }

Worker Project Structure

ethereans-api/ wrangler.toml config + non-secret vars package.json tsconfig.json src/ index.ts main router + CORS verify.ts POST /api/verify handler mint.ts POST /api/mint handler twitter.ts X API v2 + twitterapi.io fallback signer.ts EIP-712 signing + verification (viem) relayer.ts Ethereum tx submission (viem) types.ts TypeScript interfaces utils.ts rate limiting, validation, logging test/ verify.test.ts Vitest unit tests

Environment Variables

wrangler.toml (non-secret)

VariableValue
CHAIN_ID"11155111" (Sepolia) or "1" (Mainnet)
CONTRACT_ADDRESSDeployed Etherean.sol address
TBA_IMPLEMENTATIONERC-6551 account implementation
ERC6551_REGISTRYCanonical registry address

Secrets (via wrangler secret put)

SecretDescription
RELAYER_PRIVATE_KEY0x-prefixed private key of relayer EOA
VERIFIER_PRIVATE_KEY0x-prefixed private key of verifier EOA
X_API_BEARER_TOKENTwitter API v2 bearer token
TWITTERAPI_IO_KEYtwitterapi.io API key (fallback)
RPC_URLEthereum JSON-RPC endpoint

Frontend

Client-side application hosted on Cloudflare Pages.

Live at

console.gami.vc (Cloudflare Pages)

File Structure

ethereans/ (root) index.html main page with all sections style.css design tokens + component styles base.css CSS reset and base styles engine.js pixel art conversion engine (8 palettes, quantization, SVG generation) packer.js 2bpp pixel data packer (matches contract format) api.js API client for verify + mint endpoints app.js main application logic (UI, verification, minting) ogs.js "The First 100" onchain gallery og-image.png Open Graph preview image

Pixel Art Pipeline (engine.js)

  1. Load image (via unavatar.io for X PFPs, DiceBear fallback, or file upload)
  2. Downsample to 40x40 with bilinear interpolation (center crop to square)
  3. Convert to grayscale using ITU-R BT.601 luminance
  4. Quantize to 4 levels using threshold quantization (dithering disabled by default)
  5. Render to canvas at display scale (8x) and generate SVG string

2bpp Packing (packer.js)

Packs 1,600 palette indices (each 0-3) into 400 bytes. Bit layout per byte (MSB first):

[ px0 | px1 | px2 | px3 ]
 7-6   5-4   3-2   1-0

Output: "0x" + 800 hex digits = 802 chars total.

ENS Resolution (app.js)

Uses the ensdata.net public API. If the wallet input matches an ENS pattern (word.tld), resolves to a 0x address before submitting the mint request. Shows "Resolving ENS..." spinner during resolution.

The First 100 Gallery (ogs.js)

Displays the first 100 minted Ethereans in a 10-column grid. Fetches data directly from the contract via JSON-RPC batch requests to Alchemy. Uses caching: token data is immutable onchain, so cached entries never need invalidation. Only totalSupply() is fetched on every load to check for new mints.

Mint Flow (app.js)

  1. User enters X handle, generates pixel art preview
  2. User selects palette and enters receive address (0x or ENS)
  3. Verification section appears: user posts a tweet containing a 6-char nonce
  4. User clicks "I've Posted It", frontend calls POST /api/verify
  5. On success, mint button enables. User clicks "Mint to Ethereum"
  6. Frontend packs pixel data, calls POST /api/mint
  7. Progress steps animate: Encoding SVG, Packing data, Minting, Creating TBA, Done
  8. Result shows tx hash with Etherscan link

Palettes

8 named palettes, each with 4 colors from darkest to lightest. Hardcoded in both the frontend (engine.js) and the contract (SVGRenderer.sol).

Classic Green classic
#0f380f
#306230
#8bac0f
#9bbc0f
Pink Dream pink
#2b0f38
#6b2070
#c060a0
#f0a0d0
Ocean Blue ocean
#0a1628
#1a3a5c
#4a8ab0
#8ac4e0
Amber Gold amber
#1a1000
#4a3000
#b08020
#e8c060
Grayscale grayscale
#1a1a1a
#555555
#aaaaaa
#e0e0e0
Lavender lavender
#1a0a2e
#3d1f6d
#8b5ec7
#c9a0f0
Crimson crimson
#2a0a0a
#6b1a1a
#c04040
#f08080
Mint mint
#0a2a1a
#1a5a3a
#40b070
#80e0a0

Full Reference Table

KeyNameColor 0Color 1Color 2Color 3
classicClassic Green#0f380f#306230#8bac0f#9bbc0f
pinkPink Dream#2b0f38#6b2070#c060a0#f0a0d0
oceanOcean Blue#0a1628#1a3a5c#4a8ab0#8ac4e0
amberAmber Gold#1a1000#4a3000#b08020#e8c060
grayscaleGrayscale#1a1a1a#555555#aaaaaa#e0e0e0
lavenderLavender#1a0a2e#3d1f6d#8b5ec7#c9a0f0
crimsonCrimson#2a0a0a#6b1a1a#c04040#f08080
mintMint#0a2a1a#1a5a3a#40b070#80e0a0

Deployed Addresses

Contract addresses and service URLs across networks.

Sepolia Testnet Current

Contract / ServiceAddress / URL
Etherean.sol 0x758C6Fa35af696B0472D30A05F830c70Ca231aeD
Owner 0xDf90F6BC5d2A8d66F69276F29a386DE5Ae36827b
Relayer 0x002eEe1eb7a2D4Ad376772F43f30f94ba2f9544d
Verifier 0x99BD6D19019b1d1Df04D2C53cf5ba107C99cCd0a
ERC-6551 Registry 0x000000006551c19487814612e58FE06813775758 (canonical)
TBA Implementation 0x41C8f39463A868d3A88af00cd0fe7102F30E44eC (Tokenbound V3)
API Worker ethereans-api.ethereans.workers.dev
Frontend console.gami.vc
RPC (Alchemy) eth-sepolia.g.alchemy.com

Mainnet Planned

Contract / ServiceAddress / URL
Etherean.solTBD
All other addressesTBD after deployment

Mainnet Checklist

Items required before mainnet deployment.

  1. 01

    Add TBA address to tokenURI metadata Planned

    Compute dynamically via registry account() view function. Include as an attribute: {"trait_type": "TBA", "value": "0x..."}

  2. 02

    X user ID storage Planned

    Store X user ID onchain (as bytes32 hash) alongside each mint. This does not block duplicate mints from the same account after a handle change. Instead, the data is used post-mint to identify accounts with multiple Ethereans (via different handles) and blacklist their TBAs from future airdrops or mints if needed.

  3. 03

    Smart contract audit In Progress

    External audit company currently reviewing smart contract, backend, and frontend.

  4. 04

    Contract verification on Etherscan Pending

    Sepolia contract not yet verified. Must verify before mainnet.

  5. 05

    CORS hardening Pending

    Tighten API CORS from * to https://console.gami.vc.

  6. 06

    Transfer ownership to hardware wallet Pending

    Call transferOwnership() to transfer contract ownership from the deployer EOA to a hardware wallet (e.g. gami.eth). Protects admin functions (setRelayer, setVerifier) from deployer key compromise.

  7. 07

    Mainnet deployment Pending

    Deploy contracts to Ethereum mainnet. Update wrangler.toml with mainnet chain ID and contract address. Fund relayer EOA with ETH. Update frontend RPC URL and contract address.

Security Considerations

Threat model and mitigation strategies.

  • Immutable contract: no proxy, no upgradeability. If a critical bug is found, a new contract must be deployed.
  • Permissioned minting: only the relayer can call mint(), and every mint requires an EIP-712 signature from the verifier.
  • Separate keys: relayer and verifier use different EOAs. Compromise of the relayer key alone cannot mint (needs verifier signature). Compromise of the verifier key alone cannot mint (needs relayer to submit tx).
  • Nonce replay protection: each nonce can only be used once (mapping in contract).
  • Expiry window: attestations expire in 30 minutes.
  • Handle uniqueness: enforced onchain via keccak256 of lowercased handle. A user can change their X handle and mint again. X user IDs are stored onchain so these cases can be identified and blacklisted from airdrops or future mints.
  • Rate limiting: API rate-limits verify requests to 10/min per IP. In-memory limiter resets per isolate cold start. KV-backed limiter available for stricter enforcement.
  • CORS: currently set to * (to be tightened to console.gami.vc for mainnet).
  • Relayer EOA: holds real ETH. Private key stored in Cloudflare Worker secrets only.
  • No wallet connection: users never expose their private keys to the frontend. They provide a receive address only.

Repository

Source code structure and organization.

GitHub

0xigami/ethereans (private)

Directory Structure

ethereans/ index.html, style.css, base.css frontend engine.js, packer.js, api.js frontend modules app.js, ogs.js frontend app logic og-image.png OG preview image api/ Cloudflare Worker (ethereans-api) wrangler.toml package.json src/ index.ts, verify.ts, mint.ts twitter.ts, signer.ts, relayer.ts types.ts, utils.ts test/ verify.test.ts contracts/ Foundry project src/ Etherean.sol SVGRenderer.sol interfaces/IERC6551Registry.sol script/ Deploy.s.sol test/ Etherean.t.sol foundry.toml

Latest Commit

5096830e: client-side caching for gallery