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
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
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
| Standard | ERC-721 (OpenZeppelin) |
| Chain | Ethereum (Sepolia testnet, Mainnet planned) |
| Compiler | Solidity ^0.8.24 |
| Framework | Foundry (Forge) |
| Symbol | ETRN |
Constructor Parameters
| Parameter | Description |
|---|---|
owner_ | Admin address (can update relayer/verifier) |
relayer_ | Address authorized to call mint() |
verifier_ | Address whose EIP-712 signatures are trusted |
tbaImplementation | ERC-6551 account implementation address |
erc6551Registry | Canonical 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)
- Check expiry timestamp
- Check and consume nonce (prevents replay)
- Derive case-insensitive handle hash
- Enforce handle uniqueness
- Verify EIP-712 signature against verifier
- Validate svgData is exactly 400 bytes
- 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
| Function | Returns |
|---|---|
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 addresssetVerifier(address): update the verifier addresstransferOwnership(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
Environment Variables
wrangler.toml (non-secret)
| Variable | Value |
|---|---|
CHAIN_ID | "11155111" (Sepolia) or "1" (Mainnet) |
CONTRACT_ADDRESS | Deployed Etherean.sol address |
TBA_IMPLEMENTATION | ERC-6551 account implementation |
ERC6551_REGISTRY | Canonical registry address |
Secrets (via wrangler secret put)
| Secret | Description |
|---|---|
RELAYER_PRIVATE_KEY | 0x-prefixed private key of relayer EOA |
VERIFIER_PRIVATE_KEY | 0x-prefixed private key of verifier EOA |
X_API_BEARER_TOKEN | Twitter API v2 bearer token |
TWITTERAPI_IO_KEY | twitterapi.io API key (fallback) |
RPC_URL | Ethereum JSON-RPC endpoint |
Frontend
Client-side application hosted on Cloudflare Pages.
console.gami.vc (Cloudflare Pages)
File Structure
Pixel Art Pipeline (engine.js)
- Load image (via unavatar.io for X PFPs, DiceBear fallback, or file upload)
- Downsample to 40x40 with bilinear interpolation (center crop to square)
- Convert to grayscale using ITU-R BT.601 luminance
- Quantize to 4 levels using threshold quantization (dithering disabled by default)
- 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)
- User enters X handle, generates pixel art preview
- User selects palette and enters receive address (0x or ENS)
- Verification section appears: user posts a tweet containing a 6-char nonce
- User clicks "I've Posted It", frontend calls POST /api/verify
- On success, mint button enables. User clicks "Mint to Ethereum"
- Frontend packs pixel data, calls POST /api/mint
- Progress steps animate: Encoding SVG, Packing data, Minting, Creating TBA, Done
- 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).
Full Reference Table
| Key | Name | Color 0 | Color 1 | Color 2 | Color 3 |
|---|---|---|---|---|---|
classic | Classic Green | #0f380f | #306230 | #8bac0f | #9bbc0f |
pink | Pink Dream | #2b0f38 | #6b2070 | #c060a0 | #f0a0d0 |
ocean | Ocean Blue | #0a1628 | #1a3a5c | #4a8ab0 | #8ac4e0 |
amber | Amber Gold | #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 |
Deployed Addresses
Contract addresses and service URLs across networks.
Sepolia Testnet Current
| Contract / Service | Address / 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 / Service | Address / URL |
|---|---|
| Etherean.sol | TBD |
| All other addresses | TBD after deployment |
Mainnet Checklist
Items required before mainnet deployment.
-
01
Add TBA address to tokenURI metadata Planned
Compute dynamically via registry
account()view function. Include as an attribute:{"trait_type": "TBA", "value": "0x..."} -
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.
-
03
Smart contract audit In Progress
External audit company currently reviewing smart contract, backend, and frontend.
-
04
Contract verification on Etherscan Pending
Sepolia contract not yet verified. Must verify before mainnet.
-
05
CORS hardening Pending
Tighten API CORS from
*tohttps://console.gami.vc. -
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. -
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 toconsole.gami.vcfor 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.
0xigami/ethereans (private)
Directory Structure
Latest Commit
5096830e: client-side caching for gallery