I generated 22 anime portraits on a GTX 1650 with 4GB of VRAM, wrote my own ERC-721 and Seaport listing layer in Python, and put the whole thing on Base.
This is not a "look at my NFT" post. It's the three bugs that ate a day, because none of them throw an error that points at the real cause.
1. fp16 silently produces NaN on GTX 16-series cards
First run: 20 images came out completely black. Not dark — pixel mean 0.0, non-black pixel ratio 0%.
pipe = StableDiffusionPipeline.from_single_file(ckpt, torch_dtype=torch.float16)
img = pipe(prompt, num_inference_steps=10).images[0]
# np.asarray(img).mean() -> 0.0
Nothing in the pipeline warns. The diffusers call returns successfully, and only when you inspect the tensor do you see invalid value encountered in cast.
Fix: run fp32 on small/consumer cards and offload modules instead of quantizing:
pipe = StableDiffusionPipeline.from_single_file(ckpt, torch_dtype=torch.float32)
pipe.enable_model_cpu_offload() # 4GB card, ~2.5 min per 512x768 image
If you keep fp16, at minimum pipe.upcast_vae() — but on 4GB that still left me with NaN in the UNet. fp32 + offload was the only thing that worked.
2. Seaport's order typehash depends on EIP-712 type ordering
I built Seaport OrderComponents by hand. The signature was rejected by OpenSea with Signature invalid, and my locally computed order hash didn't match Seaport.getOrderHash() on-chain.
The cause: EIP-712 encodeType(primaryType) appends referenced types in alphabetical order. My type string had OfferItem before ConsiderationItem. Alphabetically, ConsiderationItem comes first.
OrderComponents(address offerer,address zone,OfferItem[] offer,ConsiderationItem[] consideration,...)
+ ConsiderationItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount,address recipient)
+ OfferItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount)
Get the order wrong and you get a different typehash — a valid-looking signature that the contract rejects.
How I found it: I read the immutable ORDER_TYPEHASH out of the deployed Seaport runtime bytecode and compared it against keccak256 of candidate strings until one matched. If you're debugging something similar, Seaport.getOrderHash() is a free oracle — it takes the same components and costs nothing to call.
3. If conduitKey != 0, the Conduit transfers the NFT — not Seaport
I approved Seaport and my listings still couldn't be filled. OpenSea's error was explicit once I read it:
Seaport order validation failed: errors=[ERC721 not owner of token, ERC721 conduit not approved]
With OpenSea's default conduitKey, the transfer is executed by the Conduit contract. isApprovedForAll(owner, seaport) is irrelevant.
Fix: resolve the conduit for the key and approve that too.
conduit, exists = conduit_controller.functions.getConduit(conduit_key).call()
# -> 0x1E0049783F008A0085193E00003D00cd54003c71 on Base
nft.functions.setApprovalForAll(conduit, True)
Also worth knowing: Seaport.validate() does not verify signatures. I fed it 65 random bytes and it passed. Don't use it as a signature oracle.
What it actually costs
Measured on Base at ~0.006 gwei:
| Operation | Gas | Cost |
|---|---|---|
| Deploy ERC-721 + ERC-2981 | 1.5M | 0.0000093 ETH |
| Write metadata URI (per piece) | ~120k | 0.0000008 ETH |
| Buyer mint | ~190k | paid by buyer |
| List on Seaport | 0 | off-chain signature |
22 pieces cost under 0.0001 ETH in total. The expensive part was my time, not the chain.
If you're doing this too
- Test
np.asarray(img).mean()right after your first generation — it catches the fp16 NaN immediately. - Call
Seaport.getOrderHash()and compare with your local implementation before you sign anything. - Before listing, check
conduitKeyand approve the conduit, not just Seaport.
The collection is live at charming-llama-47b816.netlify.app if you want to see what 4GB gets you.
Happy to answer questions about any of the three.
Top comments (0)