Partner API Kit
OnChainWin deploys your dedicated raffle contracts and operates them on your behalf. You receive a single Bearer API key and a set of opaque contract identifiers (e.g. c1, c2, c3). Use these to read live raffle data and trigger new draws from your own admin panel. You never need to handle private keys, gas, or on-chain addresses.
What You Receive
When your account is provisioned, OnChainWin sends you exactly two things:
1 · Bearer API Key
ocw_live_xxx
A single secret per company. Treat it like a database password. Store it in your server environment, never in the browser bundle. If it leaks, request rotation.
2 · Contract Keys
c1, c2, c3
Opaque identifiers for the contracts deployed for you. Use them in every API URL. The underlying on-chain address is held by OnChainWin and is not exposed.
You will never see the contract address or the internal raffle tag in any API response. Everything is referenced by your key.
Authentication
Every request must include your bearer token in the Authorization header.
GET https://app.onchainwin.com/api/partner/v1/me HTTP/1.1
Authorization: Bearer ocw_live_xxxxxxxxxxxxxxxxxxxxxxxxThe base URL for every endpoint is:
https://app.onchainwin.com/api/partner/v1Replace the host with the production domain you were sent. The path prefix /api/partner/v1 is stable across environments.
Endpoint Reference
| Method | Path | Description |
|---|---|---|
| GET | /me | Account profile + contract key list |
| GET | /contracts/:key/tickets | Paginated entries for one contract |
| GET | /contracts/:key/winners | Paginated winners + prize amounts |
| GET | /contracts/:key/raffle | Live on-chain state of the contract |
| POST | /contracts/:key/raffle | Start a new raffle on the contract |
GET /me
Returns your account profile and the list of contract keys you have access to. Call this first to discover what is available.
curl -H "Authorization: Bearer $OCW_KEY" \
https://app.onchainwin.com/api/partner/v1/me{
"name": "ILN",
"slug": "iln",
"apiKeyPrefix": "ocw_live_abcd",
"contracts": [
{ "key": "c1", "chain": "base", "label": "Production" },
{ "key": "c2", "chain": "base", "label": "Staging" },
{ "key": "c3", "chain": "base", "label": "Test" }
],
"rateLimitPerMinute": 120,
"isActive": true
}GET /contracts/:key/tickets
Paginated list of every wallet that entered raffles on this contract. Sorted newest first. Use cursor to walk older pages and since for delta sync against your own database.
| Query param | Type | Notes |
|---|---|---|
| limit | 1..500 (default 50) | Page size |
| cursor | ISO timestamp | Fetch entries strictly older than this |
| since | ISO timestamp | Fetch entries newer than or equal |
curl -H "Authorization: Bearer $OCW_KEY" \
"https://app.onchainwin.com/api/partner/v1/contracts/c1/tickets?limit=100&since=2026-05-22T00:00:00Z"{
"contract": "c1",
"count": 3,
"hasMore": false,
"nextCursor": null,
"tickets": [
{
"walletAddress": "0x1234...abcd",
"ticketCount": 1,
"timestamp": "2026-05-22T10:15:00.000Z",
"transactionHash": "0xa1b2..."
}
]
}GET /contracts/:key/winners
Same shape as tickets, but with prize amounts. Wei and ETH are both returned so you can render or accumulate without conversion mistakes.
curl -H "Authorization: Bearer $OCW_KEY" \
"https://app.onchainwin.com/api/partner/v1/contracts/c1/winners?limit=20"{
"contract": "c1",
"count": 2,
"hasMore": false,
"nextCursor": null,
"winners": [
{
"walletAddress": "0xdead...beef",
"prizeAmountWei": "1000000000000",
"prizeAmountEth": "0.000001",
"timestamp": "2026-05-22T10:30:00.000Z",
"transactionHash": "0xc3d4..."
}
]
}GET /contracts/:key/raffle
Live on-chain state of the contract. When no raffle is active, the prize and timer fields return null so you never see stale values from a previous round.
{
"contract": "c1",
"isActive": true,
"round": 42,
"contractBalanceWei": "5000000000000",
"contractBalanceEth": "0.000005",
"prizeAmountWei": "1000000000000",
"prizeAmountEth": "0.000001",
"endsAt": "2026-05-22T11:00:00.000Z",
"numberOfWinners": 3,
"maxParticipants": 100,
"currentParticipants": 42,
"remainingSlots": 58
}{
"contract": "c1",
"isActive": false,
"round": 42,
"contractBalanceWei": "5000000000000",
"contractBalanceEth": "0.000005",
"prizeAmountWei": null,
"prizeAmountEth": null,
"endsAt": null,
"numberOfWinners": null,
"maxParticipants": null,
"currentParticipants": null,
"remainingSlots": null
}POST /contracts/:key/raffle
Trigger a new raffle on the contract identified by key. OnChainWin signs the on-chain transaction for you, serialized through a per-wallet mutex so parallel calls to c1, c2, and c3 never collide.
| Body field | Type | Notes |
|---|---|---|
| prizeAmountWei | string | Decimal wei. Either this or prizeAmountEth. |
| prizeAmountEth | string | Decimal ETH, e.g. "0.001". Parsed with parseEther. |
| durationSec | number | 1..604800 (7 days max) |
| numberOfWinners | number | 1..50 |
| maxParticipants | number | Must be > winners, ≤ 10000 |
curl -X POST \
-H "Authorization: Bearer $OCW_KEY" \
-H "Content-Type: application/json" \
-d '{
"prizeAmountEth": "0.000003",
"durationSec": 600,
"numberOfWinners": 3,
"maxParticipants": 100
}' \
https://app.onchainwin.com/api/partner/v1/contracts/c1/raffle{
"ok": true,
"contract": "c1",
"transactionHash": "0xb33f...",
"explorer": "https://basescan.org/tx/0xb33f...",
"raffle": {
"prizeAmountWei": "3000000000000",
"prizeAmountEth": "0.000003",
"durationSec": 600,
"numberOfWinners": 3,
"maxParticipants": 100
}
}Why divisibility matters
The contract enforces prizeAmountWei % numberOfWinners == 0. If you ask for 0.000001 ETH split across 3 winners you will get a 400 with invalid_parameters. Round your prize to a multiple of the winner count.
Errors
Every error response is a plain JSON object with error (machine code) and message(human-readable). HTTP status reflects severity.
| Status | Code | When |
|---|---|---|
| 401 | missing_api_key | Authorization header not sent |
| 401 | invalid_api_key | Key not recognized or rotated |
| 401 | invalid_api_key_format | Key does not start with ocw_live_ |
| 403 | partner_disabled | Account is suspended |
| 404 | unknown_contract_key | Key not in your scope |
| 400 | invalid_parameters | Validation failed (see message) |
| 400 | invalid_prize | Could not parse prizeAmount |
| 402 | insufficient_balance | Contract needs a top-up |
| 409 | raffle_active | A round is already running |
| 503 | starter_misconfigured | Contact OnChainWin support |
| 500 | server_error | Internal error logged on our side |
Drop-in TypeScript SDK
Copy this single file into your project. No npm package, no dependency beyond fetch. Works in Node 18+, Bun, Deno, and modern edge runtimes.
// Minimal OnChainWin Partner API client.
// Drop into your backend (Node 18+, Bun, Deno, etc).
export interface OcwTicket {
walletAddress: string;
ticketCount: number;
timestamp: string;
transactionHash: string | null;
}
export interface OcwWinner {
walletAddress: string;
prizeAmountWei: string;
prizeAmountEth: string;
timestamp: string;
transactionHash: string | null;
}
export interface OcwRaffleState {
contract: string;
isActive: boolean;
round: number;
contractBalanceWei: string;
contractBalanceEth: string;
prizeAmountWei: string | null;
prizeAmountEth: string | null;
endsAt: string | null;
numberOfWinners: number | null;
maxParticipants: number | null;
currentParticipants: number | null;
remainingSlots: number | null;
}
export interface OcwMe {
name: string;
slug: string;
apiKeyPrefix: string;
contracts: { key: string; chain: string; label: string | null }[];
rateLimitPerMinute: number;
isActive: boolean;
}
export interface OcwStartRaffleInput {
prizeAmountEth?: string;
prizeAmountWei?: string;
durationSec: number;
numberOfWinners: number;
maxParticipants: number;
}
export class OcwError extends Error {
constructor(
public status: number,
public code: string,
message: string,
) {
super(message);
}
}
export class OcwClient {
constructor(
private apiKey: string,
private baseUrl = "https://app.onchainwin.com/api/partner/v1",
) {}
private async req<T>(
path: string,
init?: RequestInit & { query?: Record<string, string | number | undefined> },
): Promise<T> {
const url = new URL(this.baseUrl + path);
if (init?.query) {
for (const [k, v] of Object.entries(init.query)) {
if (v !== undefined) url.searchParams.set(k, String(v));
}
}
const res = await fetch(url, {
...init,
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
...(init?.headers ?? {}),
},
});
const body = await res.json().catch(() => ({}));
if (!res.ok) {
throw new OcwError(
res.status,
body.error ?? "unknown",
body.message ?? res.statusText,
);
}
return body as T;
}
me() {
return this.req<OcwMe>("/me");
}
tickets(key: string, opts: { limit?: number; cursor?: string; since?: string } = {}) {
return this.req<{
contract: string;
count: number;
hasMore: boolean;
nextCursor: string | null;
tickets: OcwTicket[];
}>(`/contracts/${key}/tickets`, { query: opts });
}
winners(key: string, opts: { limit?: number; cursor?: string; since?: string } = {}) {
return this.req<{
contract: string;
count: number;
hasMore: boolean;
nextCursor: string | null;
winners: OcwWinner[];
}>(`/contracts/${key}/winners`, { query: opts });
}
raffle(key: string) {
return this.req<OcwRaffleState>(`/contracts/${key}/raffle`);
}
startRaffle(key: string, input: OcwStartRaffleInput) {
return this.req<{
ok: true;
contract: string;
transactionHash: string;
explorer: string;
}>(`/contracts/${key}/raffle`, {
method: "POST",
body: JSON.stringify(input),
});
}
}Usage
import { OcwClient } from "./lib/ocw-client";
const ocw = new OcwClient(process.env.OCW_API_KEY!);
const me = await ocw.me();
console.log("Available contracts:", me.contracts.map((c) => c.key));
const { tickets } = await ocw.tickets("c1", { limit: 100 });
const state = await ocw.raffle("c1");
if (!state.isActive) {
await ocw.startRaffle("c1", {
prizeAmountEth: "0.000003",
durationSec: 600,
numberOfWinners: 3,
maxParticipants: 100,
});
}Wiring Into Your Admin Panel
The pattern is the same regardless of framework. Put the API key in a server-only environment variable, expose a thin proxy or server action to your admin UI, and never ship the key to the browser.
Next.js · server action
"use server";
import { OcwClient } from "@/lib/ocw-client";
const ocw = new OcwClient(process.env.OCW_API_KEY!);
export async function startRaffleAction(
key: string,
prizeEth: string,
durationMin: number,
winners: number,
maxParticipants: number,
) {
return ocw.startRaffle(key, {
prizeAmountEth: prizeEth,
durationSec: durationMin * 60,
numberOfWinners: winners,
maxParticipants,
});
}
export async function listEntries(key: string, since?: string) {
return ocw.tickets(key, { since, limit: 500 });
}Express · proxy endpoint
import express from "express";
import { OcwClient } from "./lib/ocw-client";
const app = express();
app.use(express.json());
const ocw = new OcwClient(process.env.OCW_API_KEY!);
// Your own admin auth lives in front of this — verify a session
// cookie, JWT, or whatever your panel uses BEFORE proxying.
app.get("/admin/contracts", async (_req, res) => {
const me = await ocw.me();
res.json(me.contracts);
});
app.get("/admin/contracts/:key/state", async (req, res) => {
res.json(await ocw.raffle(req.params.key));
});
app.post("/admin/contracts/:key/start", async (req, res) => {
res.json(await ocw.startRaffle(req.params.key, req.body));
});Mapping wallets to your users
Every ticket and winner row exposes walletAddress. Join it against your own user table on your side. We never receive your user PII.
const { tickets } = await ocw.tickets("c1", { since: lastSync });
for (const t of tickets) {
const user = await db.users.findOne({ wallet: t.walletAddress.toLowerCase() });
if (user) {
await db.entries.upsert({
userId: user.id,
contractKey: "c1",
txHash: t.transactionHash,
enteredAt: new Date(t.timestamp),
});
}
}
await db.syncState.update({ lastSync: new Date().toISOString() });Recommended Sync Pattern
Keep your own copy of the data instead of hitting the API on every page load. Two patterns work well.
A · Periodic delta
Every 30 seconds or 1 minute, call tickets and winners with the since param set to your last sync timestamp. Cheap, simple, lossless.
B · Backfill + delta
On first run, page through history with cursor until hasMore is false. Then switch to pattern A for continuous updates.
import { OcwClient } from "./lib/ocw-client";
const ocw = new OcwClient(process.env.OCW_API_KEY!);
const KEYS = ["c1", "c2", "c3"];
export async function syncOnce() {
for (const key of KEYS) {
const last = await db.syncState.findOne({ contractKey: key });
const since = last?.lastTimestamp ?? "1970-01-01T00:00:00Z";
let cursor: string | undefined;
do {
const { tickets, hasMore, nextCursor } = await ocw.tickets(key, {
since,
cursor,
limit: 500,
});
await db.entries.bulkUpsert(
tickets.map((t) => ({ ...t, contractKey: key })),
);
cursor = nextCursor ?? undefined;
if (!hasMore) break;
} while (cursor);
await db.syncState.upsert({
contractKey: key,
lastTimestamp: new Date().toISOString(),
});
}
}
setInterval(syncOnce, 30_000);Security Checklist
- Store the API key in a server-side environment variable only. Never inline it into client-side JS.
- Put your own admin authentication in front of any endpoint that proxies our API. The OnChainWin key authenticates your company, not your individual operators.
- If a key is exposed in a log, screenshot, or commit, request immediate rotation via OnChainWin support.
- Use TLS (https) on every call. The server rejects http in production.
- Treat 5xx responses as transient. Retry with exponential backoff. The mutex queue may briefly delay your call when another contract is mid-broadcast.
- Keep clocks in sync. The since and cursor parameters are timestamp-based; large drift can cause you to miss rows.
