Turns any wallet address into a living generative portrait. The identity is fixed by the address and never changes — but the piece grows with the wallet, reading tx count and balance live from chain on every call. Built as a standalone Agentic Service Provider (ASP) for the OKX.AI Genesis Hackathon — targets Artistic Excellence and Social Buzz.
POST /portrait { "address": "0x…", "chainId"?: 1, "format"?: "svg" | "json" }
GET /health
address(required) — hex wallet address. Nothing else is required: tx count and balance are read from chain for you.chainId(optional, default1) — which chain to read the wallet's activity from. Supported: 1, 10, 56, 137, 196, 8453, 42161. Override any RPC withRPC_URL_<chainId>.stats(optional) —{ txCount, balanceEth }to render a specific moment instead of live state.format—"svg"returns the raw image; default"json"returns{ meta, svg }.
- Endpoint:
https://erato.up.railway.app/portrait - ASP: registered on OKX.AI as agent #7102
- Payment: x402-gated via OKX's Broker,
0.10 USDTper call, X Layer mainnet (eip155:196).
pnpm install
cp .env.example .env
pnpm dev
curl -X POST http://localhost:8788/portrait -H "Content-Type: application/json" \
-d '{"address":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","format":"svg"}' -o portrait.svg
pnpm samples # render a spread of examples to ./samples
pnpm exec tsx scripts/testIdentity.ts # determinism + identity-stability checks
pnpm exec tsx scripts/testChain.ts # live-stats fallback + identity-under-live-stats checksLeave X402_ENABLED unset/false in .env for unpaid local dev.
| Variable | Purpose | Example |
|---|---|---|
PORT |
Port the server listens on | 8788 (hosts often override) |
X402_ENABLED |
Turns payment gating on. Must be true for the listing. |
true |
X402_NETWORK |
CAIP-2 chain id. X Layer mainnet, or eip155:1952 testnet |
eip155:196 |
X402_PAY_TO |
Wallet that receives payment | 0xYourAddress… |
X402_PRICE |
Price per call (settles as X Layer USDT/USDG) | $0.10 |
OKX_API_KEY |
OKX Developer Portal — facilitator auth | from the portal |
OKX_SECRET_KEY |
" | |
OKX_PASSPHRASE |
" |
Leaving X402_ENABLED unset runs the endpoint unpaid — fine for local dev, not for listing:
OKX's review requires the registered endpoint to actually return 402 when unpaid.
Two layers, deliberately separated:
Identity — fixed forever. Archetype and palette derive only from the address hash. Your portrait is always recognisably yours; it can never turn into someone else's.
State — live. Density, core mass and field energy come from the wallet's current tx count and balance, read from chain on each call. An active wallet renders denser and brighter than a fresh one, so the piece is worth revisiting as the wallet grows.
The same wallet on two different chains proves it: 0xd8dA…6045 renders Orbits / Sakura Night
on both Ethereum and X Layer — same identity — but at 5,951 txs vs 0 the two pieces look visibly
different. Verified by scripts/testChain.ts.
If a chain read fails, it silently falls back to address-derived values and still renders — a paid call never fails because a public RPC hiccuped.
/healthworks but/portraitreturns "Provide a valid hex address" — theaddressmust match0xfollowed by at least 6 hex chars. Check for stray quotes/whitespace in your JSON.- 402 Payment Required when you didn't expect it —
X402_ENABLED=true, so the route is gated. That's correct for a listed ASP; for unpaid local testing, unset it. - Server slow to start on first request — first boot compiles TypeScript deps; give it a few
seconds, then it's fast. In production,
pnpm run build+pnpm startavoids the cold compile. - Fonts look different across machines — the caption uses system serif/monospace stacks (Georgia / Courier New) so it renders without bundling font files; exact glyphs vary slightly by platform, which is fine for SVG delivery.