dash.foogy
docs updated 2026-06-30 building
Related task Create a Docs page (FWA-style) — our functionality & rules done

Agents4.fun — Whitelist

Product docs · 2026-06-30 · companion to task a4f-docs · written from the shipped code in the agent-wl repo

Agents4.fun ships two products. This page documents the Whitelist (allowlist) product. For the mint-as-ticket draw, see the companion Agents4.fun — Sweepstakes doc.

What this is. Documentation for the Agents4.fun Whitelist — an on-chain, fair random-selection / allowlist tool (live at agents4.fun). An operator creates an entry list, eligible wallets enter (token-gated), the list closes, a verifiable draw picks winners, and the winning wallets are exported as an allowlist (CSV). There is no prize pool, no payout, no buyback, and no reward token — the deliverable is the exported winner list, used off-chain (for example, an NFT mint allowlist). Everything below describes the real, shipped behavior: WhitelistRouter + StandardHook, Chainlink VRF v2.5, Solady LibPRNG.

Companion docs: Docs page brief · Agents4.fun notes · NFTX overview · FWA review.


Overview — how it works

Agents4.fun is a paid, on-chain, fair random-selection tool. An operator (artist/project) creates an event; humans and agents enter through the same permissionless on-chain path; after the list closes, winners are selected from the entrant pool using verifiable randomness. The system issues no rewards itself — the winning wallet list is exported for off-chain use (typically an NFT allowlist / mint allowlist).

The core loop, end to end:

  1. Create — an operator submits a create transaction specifying chain, close time, winner count, entry fee, tier configuration, eligibility hook, recipient mode, and a metadata/artwork URI. Creating an event pays a one-time setup fee.
  2. Enter — any eligible wallet enters on-chain, paying entry fee × tickets. Each entry calls the eligibility hook, which decides how many tickets (entries) the wallet receives.
  3. Close — after the configured close time the operator’s keeper closes the list and requests randomness. The contract also hard-rejects any entry after closeTime as a safety net.
  4. Draw — Chainlink VRF returns one random seed; winners are computed deterministically from that seed, in strict tier order, across batched transactions.
  5. Export — winners are emitted on-chain and exported as a CSV of wallet addresses (for example, an OpenSea allowlist import). The result is exportable once it reaches chain finality, so a reorg can’t change a downloaded list.

The lifecycle is a fully event-driven state machine: Created → Closed → RandomnessRequested → RandomnessFulfilled → Drawing (per-batch) → Completed, with a Failed/retryable branch. Status is reconstructable from on-chain events alone.

Supported chains: Ethereum mainnet, Base mainnet, Ethereum Sepolia, Base Sepolia.


Token-gated entries

An entry = one ticket in the draw. More tickets = more chances. A wallet can hold many tickets, and tickets are scoped to a single event.

How a wallet earns tickets is decided per-entry by the eligibility hook, called by the contract via staticcall (gas-limited, fail-closed on revert/overrun/malformed output). The built-in StandardHook is table-driven — one row per gated collection plus an optional public fallback tier:

  • ERC-721 gateticketsPerToken tickets for each token the applicant owns in the gated collection.
  • ERC-1155 gate — one ticket per held unit of a gated id (per-unit grant, default 1), capped by a configured per-id capacity (normally the edition’s supply).
  • Public tier — anyone can enter, no token required; the entrant chooses a ticket count up to the configured public maximum and the tier’s per-wallet cap.

No double-spending of tokens. Each token carries a use key (keccak256(collection, tokenId) for ERC-721). A use key is consumed once per event and stays consumed even after the token is sold or transferred — a buyer can’t reuse it to farm extra tickets. For ERC-1155, total tickets for an id can never exceed its configured per-id capacity, no matter how the tokens move between wallets. Capacity is pinned on first use and counted on-chain.

Custom hooks are supported through the same IEligibilityHook interface; the StandardHook is the default.


Tiers, weighting & caps

Weighting is by tier priority, then uniform per-ticket within a tier — not stake-weighted and not self-assessed.

Tiers are drawn in strict order. A higher-priority tier is fully drawn before any lower tier wins a single slot:

  • If a higher tier holds more eligible tickets than the remaining winner count, all remaining winners come from that tier and no lower tier is drawn at all.
  • If a tier holds fewer-or-equal tickets than the remaining count, all its tickets win and the draw continues into the next tier.

Within the slots available to a tier, winners are chosen uniformly at random — every ticket in a tier has an equal chance, and holding more tickets in that tier linearly increases your chances. A typical setup is one or more token-gated priority tiers above a public tier, so priority holders are served first and the public tier wins only from whatever slots remain.

Per-wallet caps bound concentration: each tier carries a per-wallet ticket cap (0 = unlimited), enforced by the contract across all of a wallet’s entries, not just one.


Pricing & fees

  • Entry fee — a per-ticket fee; an entry pays entry fee × number of tickets. Default 0.01 ETH per ticket (configurable per chain / per event).
  • Setup fee — a one-time native-ETH fee to create an event. Default 0.01 ETH (configurable per chain).
  • Non-refundable. Both fees are non-refundable whether or not you win, and are swept to the project Safe wallet in the same transaction that collects them — the contract never holds accumulated fee balances.
  • Allocation = winner count. The operator sets a fixed winner count; the draw stops when that count is reached or all eligible tickets are exhausted. Allocation across tiers follows the strict tier-priority rules above.
  • Cost shown upfront. The agent flow returns explanatory fee/value fields with the unsigned transaction, and value = entry fee × grants, so the cost is known before signing.

The draw

Randomness source: Chainlink VRF v2.5 (VRFConsumerBaseV2Plus / VRFV2PlusClient; one VRF subscription per chain, with a requestId → whitelistId map linking each callback to its list). After close, the contract requests randomness; on callback it stores the VRF seed and emits an event linking the seed to the event.

Expansion: Solady LibPRNG. The single VRF word seeds a LibPRNG instance that performs a partial Fisher-Yates shuffle per tier across batched keeper transactions, with a persisted per-tier cursor and a prngSeeded flag. VRF supplies the unpredictable seed; LibPRNG deterministically expands it into the winner ordering. There is no homegrown seed RNG — the seed comes from VRF and only the expansion is on-chain.

Fairness guarantee. Given the deployed contract code and the stored VRF seed, the set of winning tickets is fully determined. The keeper that runs the draw batches performs only mechanics and cannot change who wins once the seed is set. The trust assumption is “the project Safe multisig does not upgrade the contract maliciously” — not “trust the day-to-day operator.”

Recipient mode. Default: the spot goes to the applicant wallet. Optional owner-at-pick (ERC-721 only): a token-gated win is assigned to the token’s current owner at draw time (keeper supplies (collection, tokenId), verified keccak256 == useKey, so the keeper can’t redirect it). ERC-1155, public-tier, and failed ownerOf lookups fall back to the applicant — a win is never voided.

Win model. Default: a wallet can win once per ticket. Optional uniqueWinnersPerWallet: each wallet wins at most once (skip-and-preserve-slot).


Winner export / allocation

A winning wallet is emitted on-chain and included in the exported winner CSV. There is no per-draw monetary payout, no buyback, and no keep-or-sell-back choice — the deliverable is the allowlist export, used off-chain (for example, imported as an OpenSea mint allowlist).

Export unlocks once the draw result reaches chain finality, so the list you download cannot later change because of a reorg. The number of winners equals the operator’s configured winner count (or fewer if eligible tickets run out).


Roles & safety

Custody / authority. The backend never signs or broadcasts applicant or artist transactions and custodies no keys. It only builds unsigned transaction data and serves reads; wallets sign and broadcast themselves. Agents get no special powers — an agent’s authority is exactly its wallet’s on-chain role, the same permissionless path humans use.

Contract surface. A fixed-address root WhitelistRouter per chain, UUPS-upgradeable and Safe-owned; per-event state lives in mappings keyed by whitelistId (no per-event proxy clones). Eligibility hooks are called via staticcall with a gas limit (default 300k), fail-closed on revert/overrun/malformed output. Bounds: MAX_CLAIM_LENGTH = 32, MAX_GRANTS_PER_APPLY = 64. Test coverage in-repo: 38 Solidity tests including fuzz for tier ordering and draw determinism.

Roles.

RoleWhat they do
Operator / artistCreates and manages an event; sets tiers, fees, hook, recipient mode, winner count, close time; exports the winner CSV.
Applicant (human or agent)Enters on-chain through the permissionless path; pays entry fee × tickets; signs and broadcasts their own tx.
AgentSame path as a human, no extra authority; uses the public /api to discover events, preflight eligibility, and build unsigned txs.
KeeperOperator-owned signer; submits only operational txs (close, VRF request, draw batches, retries). Rotatable by the Safe; cannot alter outcomes.
Protocol / SafeOwns the upgradeable router; receives fees; the governance-trusted multisig.

Parameters

Tunable knobs set per event (or per chain where noted):

  • Chain, close time, winner count.
  • Entry fee, setup fee (per chain defaults: 0.01 ETH each).
  • Tier table — priority order + per-wallet caps.
  • Eligibility hook + its config (StandardHook table or a custom hook).
  • ERC-1155 per-id capacity.
  • Recipient mode (applicant / owner-at-pick).
  • uniqueWinnersPerWallet (win-once-per-wallet vs. once-per-ticket).
  • Hook gas limit (default 300k).

Agent API

Agents (software entering on behalf of a wallet) use the same permissionless path as humans; being an agent grants no extra authority.

Discovery. The public /api surface and agent manifest describe everything: /llms.txt (index), /skill.md (procedure), /agent.md, and /api/agents/manifest (machine-readable actions, chains, fees, required signed-intent fields, endpoints).

Typical flow:

  1. list_current_whitelists — find open events across supported chains.
  2. get_whitelist — read full config (tiers, hook rules, fees, close time, status).
  3. preflight_apply — submit wallet + claim and get back grants/tickets, required payment, and any rejection reasons. Non-authoritative — the contract is the final judge.
  4. build_apply_tx — get unsigned transaction fields (to, data, value, chainId, deadline). The backend never signs or broadcasts — the wallet signs and broadcasts itself. value = entry fee × grants; underpayment is rejected on-chain.

Reads need no signature; personalized preflight and tx-builders require a wallet-intent signature.


Not in scope yet / possible future

These are not in the shipped code — listed only so the distinction is clear:

  • Tradeable ticket tokens (“buy/sell your odds”). Entries are recorded as on-chain entries gated by ERC-721/ERC-1155 ownership, not minted as separate transferable ticket tokens.
  • Prize pool / payout / buyback. A win delivers an allowlist spot, nothing more; there is no monetary prize, no FWA-style buyback floor for non-winners.
  • Reward token / fee-share. No protocol token and no rake-sharing to participants. If ever introduced, the research (FWA $FWA, NFTX donate()) points to revenue-tied rather than emission-based designs.
  • Stake-weighted / self-assessed entries. Odds today are discrete-tier + per-ticket-uniform; an ETH-backing or self-assessed-price weighting model would be a different mechanic, not current behavior.