CACHET docs, for developers
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.
POST { "txHash": "0x..." } to api/mint-auth.js. It refuses at the first failure, in this order, each with its own status code:
| Step | Status | Message |
|---|---|---|
| Method and JSON shape | 405 / 400 | POST only / Send a JSON object |
| txHash shape | 400 | txHash must be 0x and 64 hex characters |
| Config present | 503 | minting not connected |
| Per-IP budget | 503 / 429 | busy, try again shortly / too many authorisations from this address, wait a few minutes |
| Receipt reachable | 502 | chain not reachable |
| Transaction exists | 404 | transaction not found |
| Transaction succeeded | 400 | that transaction failed |
| Two confirmations | 409 | buy is too new, try again in a moment |
| Collection readable, not paused, right chain | 502 / 503 | collection not readable / minting is paused / wrong chain |
| A qualifying pool-to-buyer transfer | 400 | buy under the minimum / no buy from the pool in that transaction |
| Not already used | 409 | that buy already has its piece |
| Domain and digest match the contract | 502 | collection domain mismatch |
| Success | 200 | buyer, 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.
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.
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 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.
revealSeed(bytes32 seed) externalReverts 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.
notBefore or on any seed that fails the commitment check. Nothing is written, and nothing is revealed by a bad guess.