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

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:

  1. 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.
  2. Mints accrue as tickets — the indexer watches mint events (Transfer from the zero address) on the mint contract. Each mint becomes one ticket for the minter, reconciled into the active round.
  3. Threshold (“enough for a buy”) — each round has a targetPrice. Tickets keep accruing (fundedValue) until fundedValue >= targetPrice.
  4. Creator triggers the buy + draw — once the threshold is met, the creator calls attemptPurchase. The contract buys the prize NFT, marks the round Bought, and requests Chainlink VRF in the same transaction.
  5. 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’s owner().
  • access_control — the requester holds DEFAULT_ADMIN_ROLE via hasRole.
  • 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-721normalizeErc721MintEvent (Transfer from zero).
  • ERC-1155normalizeErc1155TransferSingleMintEvent and normalizeErc1155TransferBatchMintEvent.

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-712 FundingBatch (batched recipients + ticket counts), with replay protection via usedFundingBatch[batchId].
  • addTicketsFromSource — called by an authorizedTicketSource, one recipient at a time, deduped by usedSourceId[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 submits fundTickets / addTicketsFromSource on-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:

  • attemptPurchase reverts with TargetNotMet while fundedValue < targetPrice.
  • When fundedValue >= targetPrice, the contract computes a cutoff (_findCutoffByValue) — the exact ticket where accrued value reaches targetPrice. 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 by requireSweepCreator), which submits attemptPurchase(roundId) on-chain.
  • attemptPurchase records the cutoff/overflow metadata, calls the strategy’s _purchasePrize(tokenId, targetPrice) to actually buy the prize NFT, and on success sets the round to Bought.
  • In the same call it invokes _requestVrfForRound, which calls Chainlink VRF v2.5 requestRandomWords (1 word) and stores vrfRequestId → 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_tryAutoTransferPrize transfers the prize to the winner and emits NFTAutoAirdropped.
  • Claim — if auto-transfer doesn’t fire, the winner calls claimNFT(roundId).
  • Admin recovery — after NFT_CLAIM_TIMEOUT (7 days) unclaimed, adminRecoverNFT lets 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

RoleWhat they do
CreatorDeploys 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 / sourceReconciler-driven signer that commits pending tickets on-chain (fundTickets / addTicketsFromSource); runs operational actions. Cannot alter the VRF outcome.
Protocol / SafeOwns 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-chain fundTickets / addTicketsFromSource.
  • Multi-round contracts with targetPrice threshold, cutoff/overflow math, fee split, and auto-advance.
  • Creator-triggered attemptPurchase that 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.