Agents4.fun — Sweepstakes
Product docs · 2026-06-30 · companion to task a4f-docs · written from the shipped code in the agent-wl repo (packages/contracts/sweep, apps/indexer, apps/backend)
Agents4.fun ships two products. This page documents the Sweepstakes product, where tickets are earned by minting — not by direct entry. For the token-gated allowlist tool, see the companion Agents4.fun — Whitelist doc.
What this is. Sweepstakes is an on-chain, multi-round, mint-as-ticket draw. A creator configures a mint source; each mint registers as one ticket; tickets accrue until a round holds enough value for a configured “buy” threshold; the creator then triggers the on-chain purchase + the Chainlink VRF v2.5 draw that picks the winner. The prize is an actual NFT bought by the contract and delivered to the winner — unlike Whitelist, which only exports an allowlist.
Overview — how it works
The Sweepstakes flow, end to end:
- Configure the mint source — a creator deploys a sweep and points it at a mint contract (the source) plus the block it was deployed at (
mintContract,mintDeployBlock). The mint source must be approved before it can fund tickets. - Mints accrue as tickets — the indexer watches mint events (
Transferfrom the zero address) on the mint contract. Each mint becomes one ticket for the minter, reconciled into the active round. - Threshold (“enough for a buy”) — each round has a
targetPrice. Tickets keep accruing (fundedValue) untilfundedValue >= targetPrice. - Creator triggers the buy + draw — once the threshold is met, the creator calls
attemptPurchase. The contract buys the prize NFT, marks the roundBought, and requests Chainlink VRF in the same transaction. - Winner selection & settlement — VRF returns a random word; the contract maps it to a winning ticket across the round’s ticket spans. The prize NFT is auto-airdropped to the winner (or claimable by them); leftover tickets/value carry over to the next round.
The round lifecycle is a state machine: Pending → Active → Bought → (winner selected) → Completed, with Failed / Abandoned branches. Rounds auto-advance — overflow tickets and value from a completed round are carried into the next round.
Supported strategies: the prize “buy” is pluggable via an INFTStrategy — shipped strategies are SweepMultiRound_NFTStrategy (generic ERC-721 buy) and SweepMultiRound_PunkStrategy (CryptoPunks). VRF is Chainlink VRF v2.5 (VRFConsumerBaseV2Plus / VRFV2PlusClient).
Configuring the mint source
A sweep is deployed through SweepFactory with a mintContract address, its mintDeployBlock, a name/slug, and a strategy type. The mint contract is the source of tickets — every mint on it counts toward this sweep.
Mint-source approval. Before a mint source can fund tickets, it is verified through the backend’s mint-source approval model (mintSourceVerifier / mintSourceEvidence, ported from the earlier sweepy mint-source-approval.ts). Approval gates which contracts may custody prizes and feed tickets. Auto-approval paths, each gated by an off-chain backfill-window check that the deploy block is verifiable within a cap:
onchain_owner— the requester wallet matches the mint contract’sowner().access_control— the requester holdsDEFAULT_ADMIN_ROLEviahasRole.opensea_owner— the requester matches the OpenSea collection owner (forced to manual review on NSFW / editor-wallet / non-empty fees / safelist signals).env_allowlist/admin— allowlisted or admin-approved wallets.
Approval state (pending / approved / rejected) plus the evidence JSON is persisted; unverified sources do not auto-approve.
Mint = ticket (accrual)
One mint, one ticket. The indexer (apps/indexer/src/sweep.ts) subscribes to the mint contract’s transfer events and normalizes them into mintEvent rows:
- ERC-721 —
normalizeErc721MintEvent(Transfer from zero). - ERC-1155 —
normalizeErc1155TransferSingleMintEventandnormalizeErc1155TransferBatchMintEvent.
The backend reconciler syncPendingTicketsFromPonder reads those indexed mint events and inserts one pending_ticket per mint, with a deterministic id (${sweepId}:${mintEventId}) and a (source_tx_hash, source_log_index) dedupe key — so re-runs are idempotent and a mint can never be counted twice.
Pending tickets are then committed on-chain. Two contract paths add paid tickets to the active round:
fundTickets— a keeper-signed, EIP-712FundingBatch(batched recipients + ticket counts), with replay protection viausedFundingBatch[batchId].addTicketsFromSource— called by anauthorizedTicketSource, one recipient at a time, deduped byusedSourceId[sourceId].
Both call _addPaidTickets, which records each recipient’s tickets as a TicketSpan (cumulative ticket + value counters) and splits payment into ticket value plus team fee (2%) and a special fee (8%) — TEAM_FEE_BPS = 200, SPECIAL_FEE_BPS = 800, ticket value 90%.
Honest note on the mint→ticket bridge. The contract does not read mint events itself. Minting drives tickets through an off-chain pipeline: indexer detects the mint → backend reconciler turns it into a
pending_ticket→ keeper/source submitsfundTickets/addTicketsFromSourceon-chain. The on-chain contract is the source of truth for funded tickets; the indexer + reconciler are the source of truth for which mints map to tickets. This split is shipped, not planned.
Threshold (“enough for a buy”)
Each round carries a targetPrice (set at openRound / setRoundParams). As tickets accrue, the round’s fundedValue grows. The buy can only happen once the threshold is met:
attemptPurchasereverts withTargetNotMetwhilefundedValue < targetPrice.- When
fundedValue >= targetPrice, the contract computes a cutoff (_findCutoffByValue) — the exact ticket where accrued value reachestargetPrice. Tickets up to the cutoff are “in” for this round’s draw; the remainder is overflow that carries into the next round.
Creator-triggered VRF draw
The buy + draw is explicitly triggered by the creator, not automatic on threshold:
- The creator calls the backend RPC
triggerAttemptPurchase(gated byrequireSweepCreator), which submitsattemptPurchase(roundId)on-chain. attemptPurchaserecords the cutoff/overflow metadata, calls the strategy’s_purchasePrize(tokenId, targetPrice)to actually buy the prize NFT, and on success sets the round toBought.- In the same call it invokes
_requestVrfForRound, which calls Chainlink VRF v2.5requestRandomWords(1 word) and storesvrfRequestId → roundId. - If the buy fails, the round is marked Failed (
PURCHASE_FAILED) and tickets become refundable.
VRF safety: recoverFromVRFTimeout lets the keeper/owner re-request randomness if VRF doesn’t fulfill within VRF_TIMEOUT (24h), without changing who can win once the seed lands.
Winner selection & settlement
On the VRF callback, the contract derives the winning ticket from the random word over the round’s cutoff ticket range, and resolves it to the winning wallet via the TicketSpan cumulative index.
Prize delivery. The bought NFT is delivered to the winner:
- Auto-airdrop —
_tryAutoTransferPrizetransfers the prize to the winner and emitsNFTAutoAirdropped. - Claim — if auto-transfer doesn’t fire, the winner calls
claimNFT(roundId). - Admin recovery — after
NFT_CLAIM_TIMEOUT(7 days) unclaimed,adminRecoverNFTlets the owner recover the prize.
Refunds & carry-over. Tickets above the cutoff don’t lose value: overflow tickets and value are carried into the next round via _autoAdvanceToNextRound / RoundAdvanced. Failed/abandoned rounds expose claimable refunds (claimableRefunds) and fees (claimableFees).
Roles & safety
| Role | What they do |
|---|---|
| Creator | Deploys the sweep, configures the mint source, sets each round’s targetPrice, and triggers the buy + draw (triggerAttemptPurchase). Gated by requireSweepCreator. |
| Minter (human or agent) | Mints on the configured mint source; each mint becomes a ticket. No direct entry path. |
| Keeper / source | Reconciler-driven signer that commits pending tickets on-chain (fundTickets / addTicketsFromSource); runs operational actions. Cannot alter the VRF outcome. |
| Protocol / Safe | Owns the UUPS-upgradeable sweep contracts; receives the team + special fees. |
Contract surface. SweepFactory deploys per-sweep SweepMultiRound_* contracts (UUPS-upgradeable, Ownable, Pausable, ReentrancyGuard, EIP-712). Replay/dedupe guards on funding batches and source ids. In-repo Foundry tests cover the factory, funding, round params, and the Punk strategy.
Shipped vs planned
Shipped (verified in agent-wl):
- Mint-as-ticket model end to end: indexer mint normalization (ERC-721 + ERC-1155 single/batch) → reconciler
pending_ticket→ on-chainfundTickets/addTicketsFromSource. - Multi-round contracts with
targetPricethreshold, cutoff/overflow math, fee split, and auto-advance. - Creator-triggered
attemptPurchasethat buys the prize and requests Chainlink VRF v2.5; VRF winner resolution; prize auto-airdrop / claim / admin-recovery; VRF-timeout recovery. - NFT-buy strategies: generic ERC-721 and CryptoPunks.
- Mint-source approval model (
owner()/hasRole/ OpenSea / allowlist / admin), with backfill-window deploy-block verification.
Planned / not (yet) confirmed in code:
- The mint → ticket bridge is off-chain (indexer + reconciler + keeper-signed funding). It is not a single atomic on-chain “mint mints a ticket” call. This is the current shipped design, but it means liveness of ticket accrual depends on the reconciler/keeper.
- End-user-facing copy on the public site framing this as one of two distinct products is the subject of this restructure; the contract/indexer/backend mechanics above are already in the repo.