DEV Community

blink88
blink88

Posted on

Designing a mint that a human can't complete

"Agents can act autonomously" is usually a claim in a pitch deck. I wanted to see what happens if you take it literally and build something that structurally requires it: an NFT mint with no wallet-connect button, no form, no button at all — a mint that can only be completed by code. The result is BLINK, 5,555 pixel sigils on Robinhood Chain, and this post is about the three design problems that fell out of committing to that premise.

The gate: why a time limit and not a puzzle

The first idea that occurs to everyone is a puzzle. Make the minter solve something hard. But any puzzle a machine can solve, a human with a calculator and patience can also solve — arithmetic doesn't discriminate between hands and scripts. What does discriminate is time.

The mint works like this: you request a challenge and get back a seed and an iteration count between 64 and 128. You apply keccak256 to the seed that many times and post the result back. The server accepts the answer for 3 seconds after issuing the challenge.

Solving the chain of hashes takes single-digit milliseconds in any language. But a person clicking through the steps — read the seed, paste it somewhere, run something, copy the answer back — has no chance. Three seconds is comfortably above a slow network round trip and comfortably below human reaction plus typing. The window is the whole gate.

agent                      server                     chain
  |--- POST /api/challenge --->|                        |
  |<-- {seed, iterations} -----|   (3s clock starts)    |
  |    keccak256 x N           |                        |
  |--- POST /api/solve ------->|                        |
  |<-- signed voucher ---------|                        |
  |    sign tx locally         |                        |
  |--- POST /api/submit ----------------------------->  |
Enter fullscreen mode Exit fullscreen mode

The honest limitation, stated early: this proves the mint was executed by code, not that no human is behind the code. Anyone who writes a script is, for these purposes, an agent. I think that's the interesting version anyway — the gate constrains execution, not identity. Also worth saying plainly: the 3-second window is server policy, not cryptography. What the contract actually enforces is a single-use signed voucher; the server just refuses to issue one unless the challenge came back in time.

Stateless challenges

I didn't want a database of outstanding challenges, so the challenge token is self-contained: an HMAC-signed blob carrying {wallet, quantity, seed, iterations, issuedAt}. When the answer comes back, the server verifies the HMAC, checks issuedAt against the clock, recomputes the hash chain, and — if everything holds — signs a mint voucher. No session, no state, scales to zero. The signing key for vouchers lives only in the server environment; if it ever leaks, the contract owner rotates it with setSigner().

The art lives in the contract

tokenURI() renders the SVG on-chain from keccak256(tokenId). No IPFS, no pinning service, no image server to go down in five years. Each sigil is a 14×14 mirrored bitmap; palette and symmetry come from the hash bytes.

The fiddly part was gas. The naive implementation builds the grid cell by cell with abi.encodePacked in a loop, which blows past comfortable limits fast. The fix was packing the whole grid into a single 196-bit integer and writing two bits per iteration to get mirror symmetry for free:

function _buildGrid(bytes32 h) internal pure returns (uint256 grid) {
    uint256 density = 80 + (uint256(uint8(h[2])) % 45);
    bool quad = uint256(uint8(h[0])) % 8 == 7;
    uint256 k = 24;
    for (uint256 y = 0; y < 14; y++) {
        for (uint256 x = 0; x < 7; x++) {
            uint256 v = (uint256(uint8(h[k % 32])) +
                uint256(uint8(h[((k + 1) * 7) % 32]))) % 256;
            if (v < density) {
                grid |= 1 << (y * 14 + x);          // left half
                grid |= 1 << (y * 14 + (13 - x));   // mirrored right half
            }
            unchecked { k++; }
        }
    }
    // 1-in-8 pieces also mirror vertically (quad symmetry)
    ...
}
Enter fullscreen mode Exit fullscreen mode

Only the SVG string assembly touches abi.encodePacked, and only for cells that are actually set. The contract is verified, so you can read the renderer and reproduce every piece offline.

Auth with no server in it

Holding a sigil gets you into The Loop, an on-chain chat contract with one access rule: post() reverts unless balanceOf(msg.sender) >= 1. Reading is public and free. There's no API key to leak, no session to hijack, no endpoint to attack, because there's no endpoint — the chain itself is the bouncer. Writing costs about half a cent in gas, which turns out to be a respectable spam filter on its own.

What surprised me

The real-time constraint changed how the agent instructions had to be written. The obvious skill design — fetch the challenge, show the user what's happening, then answer — fails every time, because the round trip through a model's output eats the 3-second budget. The skill file has to say, explicitly: run the whole protocol as one script, no pauses, no confirmation prompts mid-flight. It's a concrete case where "let the model reason step by step in the loop" is exactly the wrong instruction, and "have the model write one script and get out of the way" is the right one.

Code, contracts, and tests: https://github.com/blink-agent/blink — live skill file: https://blink5555.vercel.app/skill.md

That's it. No call to action; here's the code.

Top comments (0)