Partner Integration Kit
Once OnChainWin deploys a dedicated B2BFreeRaffle contract for you, this kit shows how to read state, send transactions, listen to events, and ship a working integration on Base. Replace CONTRACT_ADDRESS with the address we send you.
You don't have to build a full on-chain indexer to ship. OnChainWin gives you two integration paths, and you can mix them however you want depending on your engineering capacity and the freshness your product needs.
Option A · Direct on-chain
Read the contract yourself
Use viem, wagmi, ethers, or any RPC client to call the read functions and subscribe to events directly on Base. Maximum freshness, zero dependency on us, but you carry the indexing and uptime cost. Recommended if you already have a web3 stack.
Option B · Managed database
Pull from our database
If you prefer, we index every entry, winner, and round on your contract into our database and expose it through a partner API. You hit a single endpoint, get back JSON, and map wallet addresses to your own user records on your side. No web3 knowledge required on your backend.
The rest of this page documents Option A in full. If you want Option B, share your contract address and a mapping of which fields you need with the OnChainWin team and we will provision an API key plus the partner endpoint for your contract. Most partners start with Option B and graduate to Option A once they have engineers comfortable with on-chain reads.
Setup & Constants
Install viem and wagmi. Pin Base mainnet (Chain ID 8453). The ABI is the same across every partner deployment, only the contract address changes.
# package install
npm install viem wagmi @tanstack/react-query
# or
pnpm add viem wagmi @tanstack/react-queryimport type { Address } from "viem";
// Replace with the address OnChainWin sends you
export const CONTRACT_ADDRESS =
"0xYourDedicatedContractAddressHere" as Address;
export const CHAIN_ID = 8453; // Base mainnetThe reference demo contract is publicly viewable here:
ABI
The full ABI is published in our SDK. You can also copy it directly from BaseScan's verified source tab. Below is the minimal subset you need.
export const B2B_FREE_RAFFLE_ABI = [
// ─── Reads ───────────────────────────────────────────────
{ type: "function", name: "owner", stateMutability: "view",
inputs: [], outputs: [{ type: "address" }] },
{ type: "function", name: "authorizedRaffleStarter", stateMutability: "view",
inputs: [], outputs: [{ type: "address" }] },
{ type: "function", name: "raffleStatus", stateMutability: "view",
inputs: [], outputs: [{ type: "bool" }] },
{ type: "function", name: "raffleRound", stateMutability: "view",
inputs: [], outputs: [{ type: "uint256" }] },
{ type: "function", name: "prizeAmount", stateMutability: "view",
inputs: [], outputs: [{ type: "uint256" }] },
{ type: "function", name: "raffleEndTime", stateMutability: "view",
inputs: [], outputs: [{ type: "uint256" }] },
{ type: "function", name: "maxParticipants", stateMutability: "view",
inputs: [], outputs: [{ type: "uint32" }] },
{ type: "function", name: "numberOfWinnersToSelect", stateMutability: "view",
inputs: [], outputs: [{ type: "uint32" }] },
{ type: "function", name: "getContractBalance", stateMutability: "view",
inputs: [], outputs: [{ type: "uint256" }] },
{ type: "function", name: "getContractInfo", stateMutability: "view",
inputs: [],
outputs: [
{ name: "contractOwner", type: "address" },
{ name: "authorizedStarter", type: "address" },
{ name: "contractBalance", type: "uint256" },
{ name: "isRaffleActive", type: "bool" },
{ name: "vrfSubscriptionId", type: "uint256" },
] },
{ type: "function", name: "getCurrentRaffleInfo", stateMutability: "view",
inputs: [{ name: "user", type: "address" }],
outputs: [
{ name: "isActive", type: "bool" },
{ name: "prizePool", type: "uint256" },
{ name: "endTimestamp", type: "uint256" },
{ name: "winnersCount", type: "uint32" },
{ name: "maxParticipantsCount", type: "uint32" },
{ name: "currentParticipants", type: "uint256" },
{ name: "remainingSlots", type: "uint32" },
{ name: "hasUserEntered", type: "bool" },
] },
{ type: "function", name: "getCurrentPlayersCount", stateMutability: "view",
inputs: [], outputs: [{ type: "uint256" }] },
{ type: "function", name: "hasPlayerEntered", stateMutability: "view",
inputs: [{ name: "player", type: "address" }],
outputs: [{ type: "bool" }] },
{ type: "function", name: "isAuthorizedToStartRaffle", stateMutability: "view",
inputs: [{ name: "_address", type: "address" }],
outputs: [{ type: "bool" }] },
{ type: "function", name: "players", stateMutability: "view",
inputs: [{ type: "uint256" }], outputs: [{ type: "address" }] },
{ type: "function", name: "winnerAddresses", stateMutability: "view",
inputs: [{ type: "uint256" }], outputs: [{ type: "address" }] },
{ type: "function", name: "getTotalWinners", stateMutability: "view",
inputs: [], outputs: [{ type: "uint256" }] },
{ type: "function", name: "unclaimedPrizes", stateMutability: "view",
inputs: [{ type: "address" }], outputs: [{ type: "uint256" }] },
// ─── Writes ──────────────────────────────────────────────
{ type: "function", name: "getFreeTicket", stateMutability: "nonpayable",
inputs: [], outputs: [] },
{ type: "function", name: "startRaffle", stateMutability: "nonpayable",
inputs: [
{ name: "_prizeAmount", type: "uint256" },
{ name: "_duration", type: "uint128" },
{ name: "_numberOfWinners", type: "uint32" },
{ name: "_maxParticipants", type: "uint32" },
], outputs: [] },
{ type: "function", name: "depositPrizeFunds", stateMutability: "payable",
inputs: [], outputs: [] },
{ type: "function", name: "withdrawFunds", stateMutability: "nonpayable",
inputs: [], outputs: [] },
{ type: "function", name: "setAuthorizedRaffleStarter", stateMutability: "nonpayable",
inputs: [{ name: "_newStarter", type: "address" }], outputs: [] },
{ type: "function", name: "claimPrize", stateMutability: "nonpayable",
inputs: [], outputs: [] },
// ─── Events ──────────────────────────────────────────────
{ type: "event", name: "RaffleStarted",
inputs: [
{ name: "prizeAmount", type: "uint256", indexed: false },
{ name: "endTime", type: "uint256", indexed: false },
{ name: "numberOfWinners", type: "uint32", indexed: false },
{ name: "maxParticipants", type: "uint32", indexed: false },
{ name: "startedBy", type: "address", indexed: false },
] },
{ type: "event", name: "NewEntry",
inputs: [
{ name: "player", type: "address", indexed: true },
{ name: "totalPlayers", type: "uint256", indexed: false },
] },
{ type: "event", name: "WinnerSelected",
inputs: [
{ name: "winner", type: "address", indexed: true },
{ name: "prizeAmountReceived", type: "uint256", indexed: false },
] },
{ type: "event", name: "RaffleEnded", inputs: [] },
{ type: "event", name: "PrizeClaimed",
inputs: [
{ name: "winner", type: "address", indexed: true },
{ name: "amount", type: "uint256", indexed: false },
] },
] as const;Read Functions
All view functions are free to call. Use them to render UI state.
| Signature | Returns / Caller | Notes |
|---|---|---|
| owner() | address | Contract owner (OnChainWin) |
| authorizedRaffleStarter() | address | Wallet allowed to start raffles |
| raffleStatus() | bool | true if a raffle is currently active |
| raffleRound() | uint256 | Current round counter |
| prizeAmount() | uint256 | Total prize pool of active raffle (wei) |
| raffleEndTime() | uint256 | Unix timestamp when raffle ends |
| maxParticipants() | uint32 | Cap for the active raffle |
| numberOfWinnersToSelect() | uint32 | Winners to draw |
| getCurrentPlayersCount() | uint256 | Live entry count |
| getContractBalance() | uint256 | Contract ETH balance (wei) |
| getContractInfo() | tuple | Owner, starter, balance, active, sub id |
| getCurrentRaffleInfo(user) | tuple | Full snapshot incl. user's hasEntered |
| hasPlayerEntered(addr) | bool | Did `addr` enter the active round |
| isAuthorizedToStartRaffle(addr) | bool | Is `addr` the starter (or owner) |
| players(i) | address | i-th entrant in current round |
| winnerAddresses(i) | address | i-th winner of last completed round |
| getTotalWinners() | uint256 | Total winners ever drawn |
| unclaimedPrizes(addr) | uint256 | Pending prize for `addr` to claim |
viem · public client
import { createPublicClient, http, formatEther } from "viem";
import { base } from "viem/chains";
import { CONTRACT_ADDRESS } from "./contract";
import { B2B_FREE_RAFFLE_ABI } from "./abi";
const client = createPublicClient({
chain: base,
transport: http(),
});
export async function getRaffleSnapshot(user?: `0x${string}`) {
const [info, playerCount, prizePerWinner] = await Promise.all([
client.readContract({
address: CONTRACT_ADDRESS,
abi: B2B_FREE_RAFFLE_ABI,
functionName: "getCurrentRaffleInfo",
args: [user ?? "0x0000000000000000000000000000000000000000"],
}),
client.readContract({
address: CONTRACT_ADDRESS,
abi: B2B_FREE_RAFFLE_ABI,
functionName: "getCurrentPlayersCount",
}),
client.readContract({
address: CONTRACT_ADDRESS,
abi: B2B_FREE_RAFFLE_ABI,
functionName: "prizeAmount",
}),
]);
return {
isActive: info[0],
prizePoolEth: formatEther(info[1]),
endsAt: new Date(Number(info[2]) * 1000),
winners: info[3],
maxParticipants: info[4],
entries: Number(info[5]),
remainingSlots: info[6],
hasUserEntered: info[7],
livePlayers: Number(playerCount),
prizeWei: prizePerWinner,
};
}wagmi · React hook
import { useReadContract, useAccount } from "wagmi";
import { CONTRACT_ADDRESS } from "./contract";
import { B2B_FREE_RAFFLE_ABI } from "./abi";
export function useRaffle() {
const { address } = useAccount();
const { data, isLoading, refetch } = useReadContract({
address: CONTRACT_ADDRESS,
abi: B2B_FREE_RAFFLE_ABI,
functionName: "getCurrentRaffleInfo",
args: [address ?? "0x0000000000000000000000000000000000000000"],
query: { refetchInterval: 15_000 }, // poll every 15s
});
if (!data) return { isLoading, refetch };
const [
isActive, prizePool, endTimestamp, winnersCount,
maxParticipantsCount, currentParticipants, remainingSlots, hasUserEntered,
] = data;
return {
isLoading,
refetch,
isActive,
prizePool, // bigint, wei
endTimestamp: Number(endTimestamp),
winnersCount, // number
maxParticipantsCount,
entries: Number(currentParticipants),
remainingSlots,
hasEntered: hasUserEntered,
};
}Write Functions
| Signature | Returns / Caller | Notes |
|---|---|---|
| getFreeTicket() | any wallet | Enter active raffle (1/wallet) |
| startRaffle(prize, duration, winners, maxP) | owner / starter | Begin a new round |
| depositPrizeFunds() | payable, owner | Top up contract balance |
| withdrawFunds() | owner | Pull idle ETH (only when no raffle active) |
| setAuthorizedRaffleStarter(addr) | owner | Grant starter role |
| claimPrize() | winner | Pull unclaimed prize (fallback path) |
Enter the raffle
import { useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { CONTRACT_ADDRESS } from "./contract";
import { B2B_FREE_RAFFLE_ABI } from "./abi";
export function EnterButton() {
const { writeContract, data: hash, isPending, error } = useWriteContract();
const { isLoading: isConfirming, isSuccess } =
useWaitForTransactionReceipt({ hash });
const enter = () =>
writeContract({
address: CONTRACT_ADDRESS,
abi: B2B_FREE_RAFFLE_ABI,
functionName: "getFreeTicket",
});
return (
<button onClick={enter} disabled={isPending || isConfirming}>
{isPending ? "Confirm in wallet…"
: isConfirming ? "Mining…"
: isSuccess ? "Entered ✓"
: "Enter Free"}
</button>
);
}Start a raffle (authorized starter)
import { parseEther } from "viem";
import { writeContract } from "@wagmi/core";
import { CONTRACT_ADDRESS } from "./contract";
import { B2B_FREE_RAFFLE_ABI } from "./abi";
// prize: total ETH (must divide evenly across winners)
// durationMin: 1–60
// winners: 1–50
// maxParticipants: > winners, ≤ 10000
export async function startRaffle(opts: {
prizeEth: string;
durationMin: number;
winners: number;
maxParticipants: number;
}) {
return writeContract(config, {
address: CONTRACT_ADDRESS,
abi: B2B_FREE_RAFFLE_ABI,
functionName: "startRaffle",
args: [
parseEther(opts.prizeEth), // _prizeAmount (wei)
BigInt(opts.durationMin * 60), // _duration (seconds)
opts.winners, // _numberOfWinners
opts.maxParticipants, // _maxParticipants
],
});
}Fund the contract (owner)
import { parseEther } from "viem";
await writeContract(config, {
address: CONTRACT_ADDRESS,
abi: B2B_FREE_RAFFLE_ABI,
functionName: "depositPrizeFunds",
value: parseEther("0.01"), // attach ETH
});Events & Indexing
Subscribe to events to update UI in real time without polling. All events are emitted on the partner contract; index them with viem, ethers, or any indexer (Goldsky, Ponder, The Graph, Envio).
| Signature | Returns / Caller | Notes |
|---|---|---|
| RaffleStarted(prizeAmount, endTime, winners, maxParticipants, startedBy) | · | New round began |
| NewEntry(player, totalPlayers) | indexed player | Wallet entered |
| MaxParticipantsReached(totalParticipants) | · | Cap hit; VRF pending |
| RandomnessRequested(requestId) | · | VRF request submitted |
| RandomnessFulfilled(requestId) | · | VRF callback received |
| WinnerSelected(winner, prizeAmountReceived) | indexed winner | Winner drawn + paid |
| RaffleEnded() | · | Round closed |
| PrizeClaimed(winner, amount) | indexed winner | Pull-payment claim |
| FundsDeposited(depositor, amount) | indexed depositor | Owner top-up |
| AuthorizedRaffleStarterChanged(old, new) | indexed both | Starter rotated |
viem · subscribe to live entries
import { createPublicClient, webSocket, parseAbiItem } from "viem";
import { base } from "viem/chains";
import { CONTRACT_ADDRESS } from "./contract";
const client = createPublicClient({
chain: base,
transport: webSocket(process.env.BASE_WSS!), // any Base WSS RPC
});
const unsubscribe = client.watchEvent({
address: CONTRACT_ADDRESS,
event: parseAbiItem(
"event NewEntry(address indexed player, uint256 totalPlayers)"
),
onLogs: (logs) => {
for (const log of logs) {
console.log("entry", log.args.player, "total:", log.args.totalPlayers);
}
},
});
// later: unsubscribe();wagmi · React event listener
import { useWatchContractEvent } from "wagmi";
import { CONTRACT_ADDRESS } from "./contract";
import { B2B_FREE_RAFFLE_ABI } from "./abi";
export function useLiveEntries(onEntry: (player: `0x${string}`, total: bigint) => void) {
useWatchContractEvent({
address: CONTRACT_ADDRESS,
abi: B2B_FREE_RAFFLE_ABI,
eventName: "NewEntry",
onLogs: (logs) => {
for (const log of logs) {
const { player, totalPlayers } = log.args;
if (player) onEntry(player, totalPlayers ?? 0n);
}
},
});
}Historical fetch (last 24h winners)
import { parseAbiItem } from "viem";
const fromBlock = (await client.getBlockNumber()) - 43_200n; // ~24h on Base
const logs = await client.getLogs({
address: CONTRACT_ADDRESS,
event: parseAbiItem(
"event WinnerSelected(address indexed winner, uint256 prizeAmountReceived)"
),
fromBlock,
toBlock: "latest",
});Authorization Flow
On a partner-dedicated contract you become the owner directly, so there is no server-side step. The flow below applies only when OnChainWin retains ownership and delegates the starter role to your wallet.
import { createWalletClient, createPublicClient, http } from "viem";
import { base } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { B2B_FREE_RAFFLE_ABI } from "./abi";
import { CONTRACT_ADDRESS } from "./contract";
const account = privateKeyToAccount(process.env.OWNER_PRIVATE_KEY as `0x${string}`);
const wallet = createWalletClient({ account, chain: base, transport: http() });
const pub = createPublicClient({ chain: base, transport: http() });
export async function grantStarter(partnerWallet: `0x${string}`) {
const { request } = await pub.simulateContract({
account,
address: CONTRACT_ADDRESS,
abi: B2B_FREE_RAFFLE_ABI,
functionName: "setAuthorizedRaffleStarter",
args: [partnerWallet],
});
return wallet.writeContract(request);
}Self-served partners (full ownership) skip this entirely. The deployer is the owner and can call startRaffle directly.
End-to-End Component
Drop-in React component covering wallet connect, live state, entering, and result handling. Assumes wagmi is configured with Base in your provider tree.
"use client";
import { useEffect } from "react";
import { useAccount, useChainId, useSwitchChain,
useReadContract, useWriteContract,
useWaitForTransactionReceipt, useWatchContractEvent } from "wagmi";
import { formatEther } from "viem";
import { CONTRACT_ADDRESS, CHAIN_ID } from "./contract";
import { B2B_FREE_RAFFLE_ABI } from "./abi";
export default function RaffleWidget() {
const { address, isConnected } = useAccount();
const chainId = useChainId();
const { switchChain } = useSwitchChain();
const { data: info, refetch } = useReadContract({
address: CONTRACT_ADDRESS,
abi: B2B_FREE_RAFFLE_ABI,
functionName: "getCurrentRaffleInfo",
args: [address ?? "0x0000000000000000000000000000000000000000"],
query: { refetchInterval: 15_000 },
});
const { writeContract, data: hash, isPending } = useWriteContract();
const { isLoading: mining, isSuccess } =
useWaitForTransactionReceipt({ hash });
// refetch on confirmation
useEffect(() => { if (isSuccess) refetch(); }, [isSuccess, refetch]);
// realtime: refetch on each new entry
useWatchContractEvent({
address: CONTRACT_ADDRESS,
abi: B2B_FREE_RAFFLE_ABI,
eventName: "NewEntry",
onLogs: () => refetch(),
});
if (!isConnected) return <ConnectButton />;
if (chainId !== CHAIN_ID)
return <button onClick={() => switchChain({ chainId: CHAIN_ID })}>
Switch to Base
</button>;
if (!info) return <p>Loading…</p>;
const [isActive, prizePool, endTs, winners, maxP, entries, slots, entered] = info;
return (
<div>
<h2>Prize: {formatEther(prizePool)} ETH</h2>
<p>Status: {isActive ? "Active" : "Inactive"}</p>
<p>Entries: {entries.toString()} / {maxP}</p>
<p>Ends: {new Date(Number(endTs) * 1000).toLocaleString()}</p>
<button
disabled={!isActive || entered || isPending || mining}
onClick={() =>
writeContract({
address: CONTRACT_ADDRESS,
abi: B2B_FREE_RAFFLE_ABI,
functionName: "getFreeTicket",
})
}
>
{entered ? "Entered ✓"
: isPending ? "Confirm…"
: mining ? "Mining…"
: "Enter Free"}
</button>
</div>
);
}startRaffle Validation Rules
The contract reverts on every violation below. Validate client-side before calling to give users a clean error path.
| Signature | Returns / Caller | Notes |
|---|---|---|
| raffleStatus == false | · | No active raffle |
| _prizeAmount > 0 | wei | Strictly positive |
| _prizeAmount % _numberOfWinners == 0 | · | Must divide evenly |
| _duration > 0 | seconds | Recommended 60–3600 |
| _numberOfWinners >= 1 | uint32 | 1–50 in production |
| _maxParticipants > _numberOfWinners | uint32 | Strict inequality |
| _maxParticipants <= 10_000 | uint32 | Gas-bound cap |
| balance − totalUnclaimedPrizes >= _prizeAmount | wei | Fund first |
Common Reverts
Decode revert reasons in your UI. wagmi exposes them via error.message and error.shortMessage.
| Signature | Returns / Caller | Notes |
|---|---|---|
| Raffle not active | getFreeTicket | Wait for next round |
| Already entered | getFreeTicket | 1 ticket per wallet per round |
| Max participants reached | getFreeTicket | Cap hit; VRF pending |
| Not authorized | startRaffle | Caller is not owner/starter |
| Insufficient balance | startRaffle | Call depositPrizeFunds first |
| Prize not divisible | startRaffle | prizeAmount % winners ≠ 0 |
| Cannot withdraw during active raffle | withdrawFunds | Wait for RaffleEnded |
| ZeroAddress | setAuthorizedRaffleStarter | 0x0 not allowed |
import { decodeErrorResult } from "viem";
import { B2B_FREE_RAFFLE_ABI } from "./abi";
try {
await writeContract({ /* … */ });
} catch (e: any) {
// wagmi: prefer e.shortMessage; raw revert data on e.cause?.data
console.error(e.shortMessage ?? e.message);
}Round Lifecycle
┌──────────────────┐
│ depositPrizeFunds│ owner tops up
└────────┬─────────┘
│
▼
┌──────────────────┐ RaffleStarted
│ startRaffle │ ───────────────►
└────────┬─────────┘
│
▼
┌──────────────────┐ NewEntry (×N)
│ getFreeTicket │ ───────────────►
└────────┬─────────┘
│
timer ends OR maxParticipants reached
│
▼
┌──────────────────┐ RandomnessRequested
│ VRF request │ ───────────────►
└────────┬─────────┘
│ ~30s on Base
▼
┌──────────────────┐ RandomnessFulfilled
│ VRF callback │ ───────────────►
└────────┬─────────┘ WinnerSelected (×W)
▼ RaffleEnded
┌──────────────────┐
│ Prize payout │ push, then pull (claimPrize)
└──────────────────┘