Docs menu

CACHET docs, for developers

Keeper

CACHET has no minting keeper. The signing route (api/mint-auth.js) checks a buy and signs an authorisation, exactly the way a keeper-model collection's route would, but the contract itself requires the transaction's own sender to be the named buyer. Nobody's wallet but the buyer's own can ever spend that authorisation, so there is nothing for a keeper to submit on a buyer's behalf. The repository's shared scripts/snag-keeper.mjs still exists in this codebase, inherited from the base collection template, but it calls mintForBuy from its own address, which this contract's caller check would refuse; it does not apply to CACHET and should not be run against this collection.

What CACHET does need, once the drand round lands, is somebody to send the one-time, permissionless revealSeed(seed) transaction that opens the whole collection. Anyone can send it: the team, a buyer, or a small script watching for the round, since the contract only checks that the submitted seed hashes to the commitment it already stored. Nothing here has been built as a live service yet.

Status

What the route does, in order

POST { "txHash": "0x..." } to api/mint-auth.js. It refuses at the first failure, in this order, each with its own status code:

StepStatusMessage
Method and JSON shape405 / 400POST only / Send a JSON object
txHash shape400txHash must be 0x and 64 hex characters
Config present503minting not connected
Per-IP budget503 / 429busy, try again shortly / too many authorisations from this address, wait a few minutes
Receipt reachable502chain not reachable
Transaction exists404transaction not found
Transaction succeeded400that transaction failed
Two confirmations409buy is too new, try again in a moment
Collection readable, not paused, right chain502 / 503collection not readable / minting is paused / wrong chain
A qualifying pool-to-buyer transfer400buy under the minimum / no buy from the pool in that transaction
Not already used409that buy already has its piece
Domain and digest match the contract502collection domain mismatch
Success200buyer, buyRef, amountIn, deadline, signature, collection

Nothing is cleaned up on the caller's behalf. A malformed field is refused, never guessed at. The buyer's own wallet takes that response and sends the claim itself; the route never submits anything to the chain.

Rate limits

Six authorisations per client per ten minutes, counted by the first of x-vercel-forwarded-for, x-forwarded-for or x-real-ip. The store is bounded: expired windows are dropped first, and when every slot is held by a live caller a new caller is told the service is busy rather than evicting someone else's window.

The signer key is the trust root, for the claim

Limit

Anyone holding SIGNER_KEY can authorise a claim for any address, though only that address's own wallet can spend the authorisation. That is the honest shape of this design: the chain proves the buy, a server attests to it, and the contract itself pins the authorisation to the one wallet allowed to use it. The contract can rotate the signer (setSigner) and can be paused, and the owner is a two-step transfer that cannot be renounced, so a leaked key is recoverable. It is still a key, and it should live nowhere but the Vercel environment.

The round has its own trust root: the commitment

The constructor fixes seedCommitment, ciphertextHash, attestationHash and sealManifestHash at first-claimer deployment. The owner cannot change them afterward. The time gate and seed hash make reveal permissionless, but the chain cannot verify the ciphertext or evidence. No live seal is configured, and producer knowledge remains a trust limit.

Getting the seed depends on drand publishing the target round and on the producer encrypting the committed seed to that exact round. Site-owned preparation checks the ciphertext round, hash and pinned chain identity, but the contract does not verify those details. Independent Chutes evidence and output binding are still unverified.

What sending the reveal looks like

revealSeed(bytes32 seed) external

Reverts unless block.timestamp >= notBefore, the collection has not already been revealed, and keccak256(abi.encodePacked(seed)) equals the stored seedCommitment. Whoever calls it first, successfully, pays that transaction's own gas and sets revealed to true for every piece at once; every call after that reverts, since there is nothing left to reveal.

Nothing here has been built as a running service yet. A small unattended script that watches drand's public endpoint for round R, decrypts the ciphertext with tlock, and sends this one transaction would do the job, but it is optional automation on top of a permissionless call, not a trust requirement the way a minting keeper would be.

What can go wrong, and what cannot