Skip to main content

Limited Edition Drop Use Case

Learn how to use Fanfare draws and auctions to distribute limited edition products fairly and create excitement.

Overview

Limited edition products require careful distribution to ensure fairness while maximizing engagement. Fanfare provides draws (raffles) and auctions to give everyone a fair chance while creating memorable experiences. What you’ll learn:
  • Choosing between draws and auctions
  • Setting up a draw-based release
  • Running an auction for premium items
  • Building the drop experience
  • Managing winners and fulfillment
Complexity: Intermediate Time to complete: 45 minutes

Prerequisites

  • Fanfare account with draws and/or auctions enabled
  • Limited edition product ready for release
  • Understanding of your customer base
  • Fulfillment process for winners

When to Use Each Distribution Method

MethodBest ForFairness ModelRevenue
Draw (Raffle)Mass market, brand buildingEqual chanceFixed price
AuctionCollectors, price discoveryHighest bidderVariable
QueueFirst-come-first-servedSpeed-basedFixed price

Draw-Based Release

Step 1: Configure the Draw

import { FanfareAdminClient } from "@waitify-io/fanfare-admin-sdk";

const adminClient = new FanfareAdminClient({
  apiKey: process.env.FANFARE_ADMIN_API_KEY!,
  organizationId: process.env.FANFARE_ORGANIZATION_ID!,
});

async function createLimitedEditionDraw() {
  const draw = await adminClient.draws.create({
    name: "Limited Edition Sneaker Release",
    slug: "ltd-sneaker-fall-2024",

    // Entry period
    entryStart: new Date("2024-09-01T10:00:00Z"),
    entryEnd: new Date("2024-09-07T23:59:59Z"),

    // Draw timing
    drawTime: new Date("2024-09-08T12:00:00Z"),

    // Prize configuration
    config: {
      totalWinners: 500,

      // Entry limits
      maxEntriesPerConsumer: 1,
      requireAuthentication: true,

      // Selection method
      selectionMethod: "random", // or "weighted" for VIP bonus entries

      // Winner notification
      notifyWinnersVia: ["email", "sms"],
      winnerClaimWindow: 48 * 60 * 60, // 48 hours to claim

      // Waitlist for unclaimed prizes
      enableWaitlist: true,
      waitlistSize: 200,
    },

    // Product details
    prize: {
      productId: "sneaker-ltd-fall-2024",
      name: "Fall 2024 Limited Edition Sneaker",
      price: 250,
      description: "Only 500 pairs available worldwide",
      imageUrl: "https://your-store.com/images/ltd-sneaker.jpg",
    },

    // Branding
    branding: {
      primaryColor: "#1A1A1A",
      accentColor: "#FFD700",
      logoUrl: "https://your-brand.com/logo.png",
    },
  });

  return draw;
}

Step 2: Build the Entry Experience

// pages/drops/[slug].tsx
import { FanfareProvider } from "@waitify-io/fanfare-sdk-react";
import { DrawExperience } from "@/components/DrawExperience";
import { ProductShowcase } from "@/components/ProductShowcase";

interface DropPageProps {
  drawId: string;
  product: Product;
  entryStart: string;
  entryEnd: string;
  drawTime: string;
}

export default function DropPage({ drawId, product, entryStart, entryEnd, drawTime }: DropPageProps) {
  return (
    <FanfareProvider organizationId={process.env.NEXT_PUBLIC_FANFARE_ORG_ID!} options={{ environment: "production" }}>
      <div className="drop-page">
        <ProductShowcase product={product} />

        <DrawExperience drawId={drawId} entryStart={entryStart} entryEnd={entryEnd} drawTime={drawTime} />

        <DropDetails product={product} totalWinners={500} drawTime={drawTime} />
      </div>
    </FanfareProvider>
  );
}

function DropDetails({
  product,
  totalWinners,
  drawTime,
}: {
  product: Product;
  totalWinners: number;
  drawTime: string;
}) {
  return (
    <section className="drop-details">
      <h2>How It Works</h2>
      <ol className="steps">
        <li>
          <strong>Enter the Draw</strong>
          <p>Sign up with your email during the entry period. One entry per person.</p>
        </li>
        <li>
          <strong>Wait for the Draw</strong>
          <p>Winners will be selected randomly on {new Date(drawTime).toLocaleDateString()}.</p>
        </li>
        <li>
          <strong>Get Notified</strong>
          <p>Winners receive an email and SMS with purchase instructions.</p>
        </li>
        <li>
          <strong>Complete Purchase</strong>
          <p>You have 48 hours to complete your purchase at ${product.price}.</p>
        </li>
      </ol>

      <div className="drop-stats">
        <div className="stat">
          <span className="value">{totalWinners}</span>
          <span className="label">Available</span>
        </div>
        <div className="stat">
          <span className="value">${product.price}</span>
          <span className="label">Price</span>
        </div>
      </div>
    </section>
  );
}

Step 3: Draw Experience Component

// components/DrawExperience.tsx
import { useExperienceJourney } from "@waitify-io/fanfare-sdk-react";
import { useState, useEffect } from "react";

interface DrawExperienceProps {
  drawId: string;
  entryStart: string;
  entryEnd: string;
  drawTime: string;
}

export function DrawExperience({ drawId, entryStart, entryEnd, drawTime }: DrawExperienceProps) {
  const { journey, state, start } = useExperienceJourney(drawId, { autoStart: true });

  const snapshot = state?.snapshot;
  const stage = snapshot?.sequenceStage;

  const now = Date.now();
  const entryStartTime = new Date(entryStart).getTime();
  const entryEndTime = new Date(entryEnd).getTime();
  const drawTimeMs = new Date(drawTime).getTime();

  // Determine phase
  const phase =
    now < entryStartTime
      ? "pre_entry"
      : now < entryEndTime
        ? "entry_open"
        : now < drawTimeMs
          ? "entry_closed"
          : "drawn";

  return (
    <div className="draw-experience">
      {phase === "pre_entry" && <PreEntryState entryStart={entryStart} />}

      {phase === "entry_open" && (
        <EntryOpenState snapshot={snapshot} stage={stage} onEnter={start} entryEnd={entryEnd} />
      )}

      {phase === "entry_closed" && <EntryClosedState snapshot={snapshot} drawTime={drawTime} />}

      {phase === "drawn" && <DrawnState snapshot={snapshot} />}
    </div>
  );
}

function PreEntryState({ entryStart }: { entryStart: string }) {
  return (
    <div className="pre-entry-state">
      <h2>Entry Opens Soon</h2>
      <CountdownTimer targetTime={entryStart} />

      <div className="notify-me">
        <p>Get notified when entry opens</p>
        <NotifyMeForm />
      </div>
    </div>
  );
}

function EntryOpenState({
  snapshot,
  stage,
  onEnter,
  entryEnd,
}: {
  snapshot: JourneySnapshot;
  stage: string;
  onEnter: () => void;
  entryEnd: string;
}) {
  const isEntered = stage === "entered" || stage === "waiting";
  const entryCount = snapshot?.context?.totalEntries || 0;

  if (isEntered) {
    return (
      <div className="entered-state">
        <div className="success-badge">
          <span className="icon"></span>
          <span>You're In!</span>
        </div>

        <p>Your entry has been recorded. Good luck!</p>

        <div className="entry-confirmation">
          <p>Entry Number: #{snapshot?.context?.entryNumber}</p>
          <p>Total Entries: {entryCount.toLocaleString()}</p>
        </div>

        <div className="draw-countdown">
          <p>Draw happens in:</p>
          <CountdownTimer targetTime={snapshot?.context?.drawTime} />
        </div>

        <p className="reminder">We'll notify you by email if you win.</p>
      </div>
    );
  }

  return (
    <div className="entry-open-state">
      <h2>Enter the Draw</h2>

      <div className="entry-stats">
        <span className="entries">{entryCount.toLocaleString()} entries so far</span>
      </div>

      <div className="entry-closing">
        <p>Entry closes in:</p>
        <CountdownTimer targetTime={entryEnd} />
      </div>

      <EntryForm onSubmit={onEnter} />

      <p className="terms">By entering, you agree to the draw rules and terms.</p>
    </div>
  );
}

function EntryForm({ onSubmit }: { onSubmit: () => void }) {
  const [email, setEmail] = useState("");
  const [phone, setPhone] = useState("");
  const [agreed, setAgreed] = useState(false);
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsSubmitting(true);

    try {
      // SDK handles entry
      onSubmit();
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="entry-form">
      <div className="form-group">
        <label htmlFor="email">Email Address</label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
          placeholder="[email protected]"
        />
      </div>

      <div className="form-group">
        <label htmlFor="phone">Phone Number (for SMS notification)</label>
        <input
          id="phone"
          type="tel"
          value={phone}
          onChange={(e) => setPhone(e.target.value)}
          placeholder="+1 (555) 123-4567"
        />
      </div>

      <div className="form-group checkbox">
        <label>
          <input type="checkbox" checked={agreed} onChange={(e) => setAgreed(e.target.checked)} required />I agree to
          the draw rules and terms of service
        </label>
      </div>

      <button type="submit" disabled={isSubmitting || !agreed}>
        {isSubmitting ? "Entering..." : "Enter Draw"}
      </button>
    </form>
  );
}

function EntryClosedState({ snapshot, drawTime }: { snapshot: JourneySnapshot; drawTime: string }) {
  const isEntered = snapshot?.context?.isEntered;
  const totalEntries = snapshot?.context?.totalEntries || 0;

  return (
    <div className="entry-closed-state">
      <h2>Entry Period Closed</h2>

      {isEntered ? (
        <div className="entered-confirmation">
          <p className="success">You're entered! Good luck!</p>
          <p>Entry Number: #{snapshot?.context?.entryNumber}</p>
        </div>
      ) : (
        <p className="missed">Entry is now closed. Follow us for future drops.</p>
      )}

      <div className="draw-info">
        <p>Total entries: {totalEntries.toLocaleString()}</p>
        <p>Draw happens:</p>
        <CountdownTimer targetTime={drawTime} />
      </div>
    </div>
  );
}

function DrawnState({ snapshot }: { snapshot: JourneySnapshot }) {
  const isWinner = snapshot?.context?.isWinner;
  const claimDeadline = snapshot?.context?.claimDeadline;

  if (isWinner) {
    return (
      <div className="winner-state">
        <div className="winner-celebration">
          <h2>Congratulations!</h2>
          <p>You've been selected!</p>
        </div>

        <div className="claim-section">
          <p>Complete your purchase before:</p>
          <p className="deadline">{new Date(claimDeadline).toLocaleString()}</p>

          <button onClick={() => (window.location.href = `/checkout/claim/${snapshot?.context?.claimToken}`)}>
            Claim Your Prize
          </button>
        </div>

        <p className="warning">Don't miss out - unclaimed prizes go to the waitlist.</p>
      </div>
    );
  }

  return (
    <div className="not-selected-state">
      <h2>Draw Complete</h2>
      <p>Unfortunately, you weren't selected this time.</p>

      {snapshot?.context?.waitlistPosition && (
        <div className="waitlist-info">
          <p>You're on the waitlist at position #{snapshot.context.waitlistPosition}</p>
          <p>You'll be notified if a spot opens up.</p>
        </div>
      )}

      <div className="follow-up">
        <p>Stay tuned for future drops!</p>
        <button onClick={() => (window.location.href = "/subscribe")}>Get Notified of Future Drops</button>
      </div>
    </div>
  );
}

Auction-Based Release

Step 1: Configure the Auction

async function createLimitedEditionAuction() {
  const auction = await adminClient.auctions.create({
    name: "Rare Collector's Edition Artwork",
    slug: "rare-artwork-auction-001",

    // Timing
    startTime: new Date("2024-10-01T18:00:00Z"),
    endTime: new Date("2024-10-03T18:00:00Z"),

    // Auction configuration
    config: {
      auctionType: "english", // ascending price auction

      // Pricing
      startingBid: 500,
      reservePrice: 1000, // Minimum price to sell
      bidIncrement: 50,
      buyNowPrice: 5000, // Optional instant purchase

      // Anti-sniping
      antiSnipingEnabled: true,
      antiSnipingExtension: 120, // Extend 2 minutes if bid in last 2 minutes

      // Bidder requirements
      requireAuthentication: true,
      requirePaymentMethod: true, // Must have card on file
      bidderDeposit: 100, // Refundable deposit to bid

      // Limits
      maxBidsPerConsumer: 50,
    },

    // Item details
    item: {
      productId: "artwork-rare-001",
      name: "Original Digital Artwork #001",
      description: "One-of-a-kind digital artwork with physical print",
      imageUrl: "https://your-store.com/images/artwork-001.jpg",
      attributes: {
        artist: "Famous Artist",
        medium: "Digital + Physical Print",
        size: "24x36 inches",
        certificate: "Includes Certificate of Authenticity",
      },
    },

    // Branding
    branding: {
      primaryColor: "#2C2C2C",
      accentColor: "#C9A227",
    },
  });

  return auction;
}

Step 2: Build the Auction Experience

// components/AuctionExperience.tsx
import { useExperienceJourney } from "@waitify-io/fanfare-sdk-react";
import { useState, useEffect, useCallback } from "react";

interface AuctionExperienceProps {
  auctionId: string;
  item: AuctionItem;
}

export function AuctionExperience({ auctionId, item }: AuctionExperienceProps) {
  const { journey, state, start } = useExperienceJourney(auctionId, { autoStart: true });

  const snapshot = state?.snapshot;
  const context = snapshot?.context;

  const currentBid = context?.currentBid || context?.startingBid;
  const highBidderId = context?.highBidderId;
  const myId = context?.consumerId;
  const isHighBidder = highBidderId === myId;
  const endTime = context?.endTime;
  const bidIncrement = context?.bidIncrement || 50;

  return (
    <div className="auction-experience">
      <AuctionHeader item={item} endTime={endTime} />

      <div className="auction-main">
        <div className="current-bid-section">
          <span className="label">Current Bid</span>
          <span className="amount">${currentBid?.toLocaleString()}</span>
          {context?.totalBids && <span className="bid-count">{context.totalBids} bids</span>}
        </div>

        {isHighBidder && (
          <div className="high-bidder-notice">
            <span className="icon"></span>
            You're the highest bidder!
          </div>
        )}

        <BidForm
          currentBid={currentBid}
          minBid={currentBid + bidIncrement}
          bidIncrement={bidIncrement}
          buyNowPrice={context?.buyNowPrice}
          isHighBidder={isHighBidder}
          onBid={(amount) => placeBid(journey, amount)}
          onBuyNow={() => buyNow(journey)}
        />

        <BidHistory bids={context?.recentBids || []} myId={myId} />
      </div>
    </div>
  );
}

function AuctionHeader({ item, endTime }: { item: AuctionItem; endTime?: number }) {
  const [timeLeft, setTimeLeft] = useState(0);
  const [isEnding, setIsEnding] = useState(false);

  useEffect(() => {
    if (!endTime) return;

    const interval = setInterval(() => {
      const remaining = endTime - Date.now();
      setTimeLeft(Math.max(0, remaining));
      setIsEnding(remaining < 5 * 60 * 1000); // Less than 5 minutes
    }, 1000);

    return () => clearInterval(interval);
  }, [endTime]);

  return (
    <header className="auction-header">
      <h1>{item.name}</h1>

      <div className={`auction-timer ${isEnding ? "ending-soon" : ""}`}>
        <span className="label">{isEnding ? "Ending Soon!" : "Time Remaining"}</span>
        <span className="time">{formatAuctionTime(timeLeft)}</span>
      </div>
    </header>
  );
}

function BidForm({
  currentBid,
  minBid,
  bidIncrement,
  buyNowPrice,
  isHighBidder,
  onBid,
  onBuyNow,
}: {
  currentBid: number;
  minBid: number;
  bidIncrement: number;
  buyNowPrice?: number;
  isHighBidder: boolean;
  onBid: (amount: number) => void;
  onBuyNow: () => void;
}) {
  const [bidAmount, setBidAmount] = useState(minBid);
  const [isSubmitting, setIsSubmitting] = useState(false);

  // Update minimum when current bid changes
  useEffect(() => {
    if (bidAmount < minBid) {
      setBidAmount(minBid);
    }
  }, [minBid, bidAmount]);

  const handleBid = async () => {
    if (bidAmount < minBid) return;

    setIsSubmitting(true);
    try {
      await onBid(bidAmount);
    } finally {
      setIsSubmitting(false);
    }
  };

  const quickBidAmounts = [minBid, minBid + bidIncrement, minBid + bidIncrement * 2, minBid + bidIncrement * 5];

  return (
    <div className="bid-form">
      <div className="quick-bids">
        {quickBidAmounts.map((amount) => (
          <button key={amount} onClick={() => setBidAmount(amount)} className={bidAmount === amount ? "selected" : ""}>
            ${amount.toLocaleString()}
          </button>
        ))}
      </div>

      <div className="custom-bid">
        <label htmlFor="bid-amount">Your Bid</label>
        <div className="bid-input-group">
          <span className="currency">$</span>
          <input
            id="bid-amount"
            type="number"
            value={bidAmount}
            onChange={(e) => setBidAmount(Math.max(minBid, Number(e.target.value)))}
            min={minBid}
            step={bidIncrement}
          />
        </div>
        <span className="min-bid">Minimum: ${minBid.toLocaleString()}</span>
      </div>

      <button onClick={handleBid} disabled={isSubmitting || bidAmount < minBid} className="place-bid-btn">
        {isSubmitting ? "Placing Bid..." : isHighBidder ? "Increase Bid" : "Place Bid"}
      </button>

      {buyNowPrice && (
        <div className="buy-now-section">
          <span className="divider">or</span>
          <button onClick={onBuyNow} className="buy-now-btn">
            Buy Now for ${buyNowPrice.toLocaleString()}
          </button>
        </div>
      )}
    </div>
  );
}

function BidHistory({ bids, myId }: { bids: Bid[]; myId: string }) {
  if (bids.length === 0) {
    return (
      <div className="bid-history empty">
        <p>No bids yet. Be the first!</p>
      </div>
    );
  }

  return (
    <div className="bid-history">
      <h3>Recent Bids</h3>
      <ul>
        {bids.map((bid, index) => (
          <li key={bid.id} className={bid.bidderId === myId ? "my-bid" : ""}>
            <span className="bidder">{bid.bidderId === myId ? "You" : `Bidder ${bid.bidderNumber}`}</span>
            <span className="amount">${bid.amount.toLocaleString()}</span>
            <span className="time">{formatTimeAgo(bid.timestamp)}</span>
            {index === 0 && <span className="leader-badge">Leading</span>}
          </li>
        ))}
      </ul>
    </div>
  );
}

async function placeBid(journey: ExperienceJourney, amount: number) {
  await journey.perform("bid", { amount });
}

async function buyNow(journey: ExperienceJourney) {
  await journey.perform("buyNow");
}

function formatAuctionTime(ms: number): string {
  const hours = Math.floor(ms / (1000 * 60 * 60));
  const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
  const seconds = Math.floor((ms % (1000 * 60)) / 1000);

  if (hours > 0) {
    return `${hours}h ${minutes}m ${seconds}s`;
  }
  if (minutes > 0) {
    return `${minutes}m ${seconds}s`;
  }
  return `${seconds}s`;
}

function formatTimeAgo(timestamp: number): string {
  const seconds = Math.floor((Date.now() - timestamp) / 1000);

  if (seconds < 60) return "Just now";
  if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
  if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
  return new Date(timestamp).toLocaleDateString();
}

Step 3: Winner Management

Process Draw Winners

// services/draw-winner-management.ts
async function processDrawWinners(drawId: string) {
  // Get winners list
  const winners = await adminClient.draws.getWinners(drawId);

  for (const winner of winners) {
    // Generate unique claim token
    const claimToken = await generateClaimToken(winner.consumerId, drawId);

    // Store claim data
    await db.insert(drawClaims).values({
      drawId,
      consumerId: winner.consumerId,
      claimToken,
      expiresAt: new Date(Date.now() + 48 * 60 * 60 * 1000), // 48 hours
      status: "pending",
    });

    // Send notifications
    await sendWinnerNotification(winner, claimToken);
  }

  // Process waitlist
  const waitlist = await adminClient.draws.getWaitlist(drawId);
  for (const entry of waitlist) {
    await sendWaitlistNotification(entry);
  }
}

async function sendWinnerNotification(winner: DrawWinner, claimToken: string) {
  await Promise.all([
    sendEmail({
      to: winner.email,
      subject: "You Won! Complete Your Purchase",
      template: "draw-winner",
      data: {
        name: winner.name,
        productName: winner.prize.name,
        claimUrl: `https://your-store.com/claim/${claimToken}`,
        deadline: new Date(Date.now() + 48 * 60 * 60 * 1000).toLocaleString(),
      },
    }),

    sendSMS({
      to: winner.phone,
      message: `Congratulations! You won the ${winner.prize.name}. Claim within 48h: https://your-store.com/claim/${claimToken}`,
    }),
  ]);
}

Process Auction Winner

async function processAuctionWinner(auctionId: string) {
  const auction = await adminClient.auctions.get(auctionId);

  if (auction.status !== "ended") {
    throw new Error("Auction not yet ended");
  }

  const winner = auction.highestBidder;
  const finalPrice = auction.highestBid;

  // Check reserve price met
  if (finalPrice < auction.config.reservePrice) {
    await handleReserveNotMet(auction);
    return;
  }

  // Generate payment intent for final amount
  const paymentIntent = await stripe.paymentIntents.create({
    amount: finalPrice * 100,
    currency: "usd",
    customer: winner.stripeCustomerId,
    metadata: {
      auctionId,
      type: "auction_win",
    },
  });

  // Send winner notification
  await sendEmail({
    to: winner.email,
    subject: `Congratulations! You won the auction for ${auction.item.name}`,
    template: "auction-winner",
    data: {
      itemName: auction.item.name,
      finalPrice,
      paymentUrl: `https://your-store.com/auction/pay/${auctionId}?pi=${paymentIntent.id}`,
      deadline: new Date(Date.now() + 72 * 60 * 60 * 1000).toLocaleString(),
    },
  });
}

async function handleReserveNotMet(auction: Auction) {
  // Notify seller
  await sendEmail({
    to: auction.sellerEmail,
    subject: `Reserve Not Met - ${auction.item.name}`,
    template: "auction-reserve-not-met",
    data: {
      itemName: auction.item.name,
      highestBid: auction.highestBid,
      reservePrice: auction.config.reservePrice,
    },
  });

  // Notify high bidder
  if (auction.highestBidder) {
    await sendEmail({
      to: auction.highestBidder.email,
      subject: `Auction Ended - Reserve Not Met`,
      template: "auction-reserve-not-met-bidder",
      data: {
        itemName: auction.item.name,
        yourBid: auction.highestBid,
      },
    });
  }
}

Best Practices

1. Prevent Gaming and Fraud

// Draw fraud prevention
const drawFraudPrevention = {
  // Require verified email
  requireEmailVerification: true,

  // Block disposable emails
  blockDisposableEmails: true,

  // Limit entries by IP
  maxEntriesPerIP: 3,

  // Require account age
  minAccountAgeDays: 7,

  // Geographic restrictions
  allowedCountries: ["US", "CA", "UK"],
};

// Auction fraud prevention
const auctionFraudPrevention = {
  // Require verified payment method
  requirePaymentMethod: true,

  // Bidder deposit
  bidderDeposit: 100,

  // Account verification
  requireIdentityVerification: true,

  // Shill bidding detection
  detectShillBidding: true,
};

2. Clear Communication

// Communication timeline for draws
const drawCommunicationSchedule = [
  { trigger: "entry_open", template: "draw-entry-open", channels: ["email"] },
  { trigger: "24h_before_close", template: "draw-reminder", channels: ["email", "push"] },
  { trigger: "entry_closed", template: "draw-closed", channels: ["email"] },
  { trigger: "draw_complete_winner", template: "draw-winner", channels: ["email", "sms", "push"] },
  { trigger: "draw_complete_non_winner", template: "draw-non-winner", channels: ["email"] },
  { trigger: "24h_before_claim_deadline", template: "claim-reminder", channels: ["email", "sms"] },
  { trigger: "claim_expired", template: "claim-expired-waitlist", channels: ["email", "sms"] },
];

3. Transparent Rules

function DrawRules({ draw }: { draw: Draw }) {
  return (
    <section className="draw-rules">
      <h2>Official Rules</h2>

      <dl>
        <dt>Eligibility</dt>
        <dd>Must be 18+ and resident of eligible countries</dd>

        <dt>Entry Period</dt>
        <dd>
          {new Date(draw.entryStart).toLocaleString()} - {new Date(draw.entryEnd).toLocaleString()}
        </dd>

        <dt>Entry Limit</dt>
        <dd>One entry per person</dd>

        <dt>Selection</dt>
        <dd>Winners selected randomly using certified random number generator</dd>

        <dt>Notification</dt>
        <dd>Winners notified via email and SMS within 1 hour of draw</dd>

        <dt>Claim Period</dt>
        <dd>Winners must complete purchase within 48 hours</dd>

        <dt>Price</dt>
        <dd>${draw.prize.price} (no additional fees)</dd>
      </dl>
    </section>
  );
}

Troubleshooting

Draw Issues

ProblemCauseSolution
Duplicate entriesMultiple accountsImplement email/phone verification
Low participationPoor marketing timingAnnounce earlier, extend entry period
Many unclaimed prizesShort claim windowExtend to 72h, improve notifications

Auction Issues

ProblemCauseSolution
Sniping complaintsLast-second bidsEnable anti-sniping extension
Low bidding activityHigh starting priceLower start, keep reserve
Winner doesn’t payNo deposit requiredImplement bidder deposits

What’s Next