A generative art engine for layered NFT collections, built with SvelteKit 2 and Svelte 5 runes. Upload your trait folders, set rarities as honest percentages, define multi-layer incompatibility rules, and ship a ZIP.
Inspired by 0xSylla/Generative-NFT-Art-Metadata-Generator; we just fuck with Svelte more.
images/ and metadata/ folders, ERC-721/1155-shaped JSON.@tailwindcss/vite@sveltejs/adapter-static) - the whole thing is a prerendered SPA, no servergit clone https://github.com/abashoverse/nft-generator.git
cd nft-generator
npm install
npm run dev # http://localhost:5173
npm run check # svelte-check + TS
npm run build # production build (static, lands in build/)
npm run preview # serve the production build locally
In npm run dev, the paywall is bypassed by default. Set PUBLIC_FORCE_PAYWALL=true in .env to exercise it during dev (see Paywall below).
Give it a name, description, and target size (1 to 10000). Then either drag your layers folder onto the dropzone or click to browse. Dragging avoids the browser's "Are you sure you want to upload all files from..." prompt; clicking triggers it.
The folder should look like:
my-collection/
├── Background/
│ ├── blue.png
│ └── sunset.png
├── Body/
│ ├── frog.png
│ └── cat.png
└── Eyes/
└── ...
Each subfolder = one layer, each file in it = one trait. The top folder name is ignored. Each trait starts with an evenly distributed weight (every layer sums to 100% out of the gate).
After upload, the app reads each image's dimensions via an Image element with a 5-second per-file timeout. If anything doesn't match, you'll see exactly which layers are off and you can't move on until they do.
Drag layers to set the stack order (top of the list = top of the canvas, like Photoshop). Tune each trait's percentage with the +/- controls or by typing. The sum line at the bottom of each layer turns amber when it isn't exactly 100%; one click normalizes everything back to 100%.
The right side has a full-width live preview canvas and a Generate Random button. Previews render at 512px for speed; the export resolution is independent.
Below the main card, click the Incompatible traits accordion to define rules. The builder is two-stepped:
Example: If Background = (blue or red) AND Hat = crown then block Eyes = laser-eyes. Saved rules appear in a list above the builder; you can delete individually or clear all.
The whole collection renders into a thumbnail grid. Hit Regenerate to roll new combinations. Thumbnails render at low resolution for speed; final exports use the resolution from step 4.
Set the export resolution (1 to 8192 px²), or click "Match source" to use your traits' native size for a lossless render. If the paywall is on and you haven't unlocked, the resolution is fixed at 500×500 (see Paywall).
Then click Generate & download ZIP to render every token at the chosen size and download a ZIP with images/0.png, metadata/0.json, etc. Metadata is ERC-721/1155-shaped, ready to upload to your preferred storage.
All config is via env vars. Copy .env.example to .env and fill in what you want; everything is optional.
PUBLIC_UMAMI_SCRIPT_URL=https://umami.example.com/script.js
PUBLIC_UMAMI_WEBSITE_ID=your-website-id-here
Both must be set for the Umami snippet to inject; otherwise no analytics.
# Mainnet ERC-721 addresses for the free-access NFT holder check.
# Set either or both. Enables the "hold an NFT" path in the unlock modal.
PUBLIC_ABASHO_NFT_ADDRESS=0x...
PUBLIC_ABASHOS_NFT_ADDRESS=0x...
# Receiver wallet for the one-time payment. Enables the "pay once" path.
PUBLIC_PAY_RECEIVER_ADDRESS=0x...
# Payment amount in USD. Converted to ETH at runtime via CoinGecko. Default 10.
PUBLIC_PAY_AMOUNT_USD=10
# GitHub repo link for the self-host option. Hidden if unset.
PUBLIC_SELF_HOST_URL=https://github.com/abashoverse/nft-generator
# Dev-only: force the paywall on while running `npm run dev` so you can
# exercise the locked UI and unlock modal without a production build.
# Ignored in production builds.
PUBLIC_FORCE_PAYWALL=true
If none of the paywall vars are set, the paywall is OFF and the app is fully free (same as dev mode). The moment any of them is set, the paywall turns on and each option in the unlock modal renders only if its specific config is set.
All vars are read via SvelteKit's $env/static/public, not import.meta.env, so the PUBLIC_ prefix is required and missing vars resolve to undefined.
The free tier is hard-capped at 500×500 exports. Custom resolution unlocks via one of three paths:
window.ethereum. A plain "Connect browser wallet" button falls back to whatever owns the global when no EIP-6963 announces fire.ensureMainnet() safety call re-requests the switch explicitly for wallets that ignore the connect-time hint.siwe library, see tradeoffs). Signature verification happens client-side via viem's verifyMessage.Paywall state is in-memory only. Reload the page = blank slate, user reconnects and re-signs. wagmi is configured with noopStorage so it doesn't auto-reconnect either. There's no localStorage to clear.
Mainnet and Base reads go through a fallback() transport over three public RPCs each (eth.llamarpc.com, cloudflare-eth.com, ethereum-rpc.publicnode.com for mainnet; mainnet.base.org, base-rpc.publicnode.com, base.llamarpc.com for Base). If one rate-limits or CORS-fails, viem rolls to the next. If you want your own Alchemy/Infura, swap the URLs in src/lib/wagmi.ts.
This is honor-system gating: everything runs client-side, so a determined user with dev tools can monkey-patch the runtime and bypass it. The self-host link is the deliberate pressure release valve. If you need real enforcement, you'd want to move the export pipeline to a backend that verifies payment on-chain before serving the ZIP, which is out of scope for this version.
The siwe library was dropped because its CJS source has a top-level require('ethers') that breaks esbuild bundling. The hand-rolled EIP-4361 message uses the same wire format and verifies through viem.
src/
├── app.css # Tailwind v4 theme + abashoverse design tokens
├── app.html # font links (Satoshi + JetBrains Mono), OCB favicon
├── lib/
│ ├── components/
│ │ ├── Step1Setup.svelte
│ │ ├── Step2Layers.svelte
│ │ ├── Step3Preview.svelte
│ │ ├── Step4Export.svelte
│ │ ├── PreviewItem.svelte # single tile in step 3 grid
│ │ ├── PaywallModal.svelte
│ │ ├── StepIndicator.svelte # floating nav step pills
│ │ ├── StepNav.svelte # floating bottom nav
│ │ └── ui/ # Button, Card, Input, Modal, Pill, Select, etc.
│ ├── stores/
│ │ ├── generator.svelte.ts # core state (layers, rules, config, collection)
│ │ └── theme.svelte.ts # light/dark theme store
│ ├── paywall.svelte.ts # paywall state, env-driven gating (in-memory only, no persistence)
│ ├── wagmi.ts # wagmi config, RPC fallback transports, noopStorage
│ ├── web3.ts # viem helpers: connect, SIWE, balanceOf, delegate.xyz, sendTx, CoinGecko
│ └── types.ts
└── routes/
├── +layout.svelte # umami injection, theme init
├── +layout.ts # prerender + ssr=false
└── +page.svelte # floating shell, mounts the 4 steps
| Upstream (React/Next) | This (Svelte) | |
|---|---|---|
| Framework | Next.js 15 | SvelteKit 2 + Svelte 5 runes |
| State | useNFTGenerator hook |
Svelte rune-based stores |
| Styling | Vanilla CSS + glassmorphism | Tailwind v4 + abashoverse tokens |
| Rarities | Integer weights (0 to 100) | Percentages (0.001% to 100%) with normalize |
| Image checks | None | Auto dimension validation on upload |
| Folder upload | Click only | Click or drag (drag skips the browser prompt) |
| Incompatibility | Pairwise (trait A ↔ trait B) | Multi-condition (N layers IF, 1 trait THEN) |
| Export resolution | Fixed | Configurable 1 to 8192 px² (paywalled at 500 by default) |
| Distribution | IPFS via Pinata | ZIP download only |
| Monetization | None | Optional honor-system paywall: NFT holder, pay once, or self-host |
| Theme | Dark only | Light + dark toggle |
If you came here for IPFS upload, that path was removed in favor of users handling their own pinning. The ZIP layout is compatible with most pinning services.
MIT.