Free UI Bits & SDK

Drop-in components and a typed SDK to build provably-fair pools on Base.

Fairplay SDK (Featured)
Typed helpers for building Fairplay apps on Base with viem + wagmi.
Install
npm i @fairplaylabs/sdk viem wagmiyarn add @fairplaylabs/sdk viem wagmipnpm add @fairplaylabs/sdk viem wagmi
Network
Base (8453)
Requires
viem · wagmi · env
Includes
Create · Enter · Reveal · Read
Env needed: NEXT_PUBLIC_CHAIN_ID, NEXT_PUBLIC_RPC_URL, NEXT_PUBLIC_VAULT_ADDRESS.
Show code
// basic usage with wagmi/viem
import { createFairplayClient } from '@fairplaylabs/sdk'
import { http, createConfig } from 'wagmi'
import { base } from 'wagmi/chains'
import { parseAbi } from 'viem'

// Vault ABI (minimal) or import from your app
const FAIRPLAY_VAULT_ABI = parseAbi([
  "function createPool((uint64,uint64,uint64,uint32,uint32,uint96,uint16,uint16,bytes32,uint96,address,bytes32,uint96,address)) returns (uint256)",
  "function enter(uint256 poolId,uint32 quantity)",
  "function revealCreator(uint256 poolId,bytes32 salt)",
  "function pools(uint256) view returns (address creator,address builderFeeRecipient,uint64 deadline,uint64 revealDeadline,uint64 sentinelRevealDeadline,uint32 maxEntries,uint32 minEntries,uint96 entryPrice,uint16 builderFeeBps,uint16 protocolFeeBps,bytes32 creatorCommitHash,bytes32 sentinelCommitHash,address sentinel,uint96 creatorBond,uint96 sentinelBond,uint32 entries,bool creatorRevealed,bool sentinelRevealed,bool drawn,bool canceled,address winner,bytes32 _creatorSalt,bytes32 _sentinelSalt,uint128 grossCollected)"
])

// wagmi config (Base)
export const wagmiConfig = createConfig({
  chains: [base],
  transports: { [base.id]: http(process.env.NEXT_PUBLIC_RPC_URL) },
})

// SDK client
export const fp = createFairplayClient({
  chainId: Number(process.env.NEXT_PUBLIC_CHAIN_ID || 8453),
  vaultAddress: (process.env.NEXT_PUBLIC_VAULT_ADDRESS || '').toLowerCase(),
  wagmiConfig,
  abi: FAIRPLAY_VAULT_ABI,
})

// Example: create a pool
// const { poolId } = await fp.createPool({
//   deadline, revealDeadline, sentinelRevealDeadline,
//   maxEntries, minEntries, entryPrice,
//   builderFeeBps, protocolFeeBps,
//   creatorCommitHash, creatorBond,
//   sentinel, sentinelCommitHash, sentinelBond,
//   builderFeeRecipient,
// })
Countdown Badge
Tiny, pulse-on-urgent badge to show time left.
2m 0s
Show code
// CountdownBadge.tsx
'use client'
import { useEffect, useState } from 'react'
export default function CountdownBadge({ to }: { to: number }) {
  const [left, setLeft] = useState(Math.max(0, to - Math.floor(Date.now()/1000)))
  useEffect(() => {
    const t = setInterval(() => setLeft(Math.max(0, to - Math.floor(Date.now()/1000))), 1000)
    return () => clearInterval(t)
  }, [to])
  const m = Math.floor(left / 60), s = left % 60
  const urgent = left > 0 && left <= 60
  return (
    <span className={`inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs ring-1 ${urgent ? 'bg-red-500/15 text-red-200 ring-red-500/30 animate-pulse':'bg-emerald-500/15 text-emerald-200 ring-emerald-500/30'}`}>
      ⏳ {left === 0 ? 'Closed' : `${m}m ${s}s`}
    </span>
  )
}
Pool Progress
Animated ‘filling’ tracker toward a target.
Entries: 37 / 10037%
Filling…
Show code
// PoolProgress.tsx
'use client'
import { motion } from 'framer-motion'
export default function PoolProgress({ entries, target, showGoal = true }:{
  entries: number; target: number; showGoal?: boolean
}) {
  const pct = Math.max(0, Math.min(100, Math.round((entries / Math.max(1, target)) * 100)))
  const color = pct>=100?'bg-emerald-500':pct>=75?'bg-cyan-500':pct>=50?'bg-indigo-500':'bg-slate-600'
  return (
    <div>
      <div className="mb-1 flex items-center justify-between text-xs text-slate-400">
        <span>Entries: <b className="text-slate-200">{entries}</b>{showGoal && ` / ${target}`}</span>
        <span>{pct}%</span>
      </div>
      <div className="h-3 w-full overflow-hidden rounded-full bg-white/5 ring-1 ring-white/10">
        <div className={`h-3 ${color}`} style={{ width: `${pct}%` }} />
      </div>
      <div className="mt-1 text-[11px] text-slate-500">{pct<100?'Filling…':'Target reached 🎉'}</div>
    </div>
  )
}
Status Badge
OPEN / REVEAL / FINALIZED / CANCELED
OPENREVEALFINALIZEDCANCELED
Show code
// StatusBadge.tsx
export type PoolStatus = 'OPEN'|'REVEAL'|'FINALIZED'|'CANCELED'
export default function StatusBadge({ status }:{ status: PoolStatus }) {
  const map = {
    OPEN:'bg-emerald-500/15 text-emerald-200 ring-emerald-500/30',
    REVEAL:'bg-cyan-500/15 text-cyan-200 ring-cyan-500/30',
    FINALIZED:'bg-indigo-500/15 text-indigo-200 ring-indigo-500/30',
    CANCELED:'bg-red-500/15 text-red-200 ring-red-500/30',
  } as const
  return <span className={`inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs ring-1 ${map[status]}`}>{status}</span>
}
Timer Strip
Linear progress bar until a deadline.
Entry closes in1:20
Show code
// TimerStrip.tsx
'use client'
import { useEffect, useMemo, useState } from 'react'
export default function TimerStrip({ start, end, label='Time left' }:{
  start: number; end: number; label?: string
}) {
  const nowS = () => Math.floor(Date.now()/1000)
  const [now, setNow] = useState(nowS())
  useEffect(() => { const t = setInterval(()=>setNow(nowS()),1000); return ()=>clearInterval(t) }, [])
  const pct = useMemo(() => {
    const span = Math.max(1, end-start)
    const used = Math.min(span, Math.max(0, now-start))
    return Math.round((used/span)*100)
  }, [now,start,end])
  const left = Math.max(0, end-now)
  const m = Math.floor(left/60), s = left%60
  return (
    <div>
      <div className="mb-1 flex items-center justify-between text-xs text-slate-400">
        <span>{label}</span><span>{left===0?'0:00':`${m}:${String(s).padStart(2,'0')}`}</span>
      </div>
      <div className="h-2 w-full overflow-hidden rounded-full bg-white/5 ring-1 ring-white/10">
        <div className="h-2 bg-cyan-500" style={{ width: `${pct}%` }} />
      </div>
    </div>
  )
}
Prize Ticker
Counts up to a prize or pot total.
$0.00
Show code
// PrizeTicker.tsx
'use client'
import { useEffect, useRef, useState } from 'react'
export default function PrizeTicker({ to, prefix='$', durationMs=900 }:{
  to: number; prefix?: string; durationMs?: number
}) {
  const [val, setVal] = useState(0)
  const raf = useRef(0)
  useEffect(() => {
    const start = performance.now(), begin = 0, change = to - begin
    const step = (t:number) => {
      const p = Math.min(1, (t-start)/durationMs)
      const eased = 1 - Math.pow(1-p, 3)
      setVal(Math.round((begin + change * eased) * 100) / 100)
      if (p < 1) raf.current = requestAnimationFrame(step)
    }
    raf.current = requestAnimationFrame(step)
    return () => cancelAnimationFrame(raf.current)
  }, [to, durationMs])
  return <span className="tabular-nums font-semibold">{prefix}{val.toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}</span>
}