Run the whole rolling flow programmatically: pull every retweeter, commit a Solana block, draw winners, and hand each entrant a proof they can re-run. Same provably-fair engine as the dashboard — one REST call at a time.
Getting started
The retweet.gg API is organized around the rolling flow — the same pipeline the app uses to turn a tweet into a verifiable draw. You paste a tweet, we pull the retweeters, a Solana block seeds the roll, and the finished giveaway is its own proof: everything an outsider needs to recompute every winner ships in one object.
It is a predictable, resource-oriented REST API. It accepts JSON request bodies, returns JSON responses, and uses standard HTTP verbs, status codes, and authentication. Every giveaway is addressed by its public code (e.g. ROLL-8DO2), which is also its verify URL: retweet.gg/verify/{code}.
One engine, two front doors
Getting started
Authenticate with a secret API key in the Authorization header as a bearer token. Create and roll keys from your account settings. Keys carry your credit balance and your ban lists, so treat them like a password.
curl https://api.retweet.gg/v1/giveaways \
-H "Authorization: Bearer rtg_live_xxxxxxxxxxxx"rtg_live_…stringrequiredrtg_test_…stringoptionalKeep keys server-side
Getting started
Each key may make up to 120 requests per minute. Every response carries your current budget so you can back off gracefully; a 429 means you should retry after the window resets.
X-RateLimit-LimitintegeroptionalX-RateLimit-RemainingintegeroptionalX-RateLimit-ResetintegeroptionalThe retweeter pull runs asynchronously and does not count against this limit while it works — you only spend a request to start it and to poll for the result.
Getting started
retweet.gg uses conventional HTTP status codes. 2xx means success, 4xx means the request was rejected (and the body says why), and 5xx means something failed on our end. Every error body has the same shape.
{
"error": {
"type": "insufficient_credits",
"code": "credits_required",
"message": "Pulling ~1300 retweeters needs ~1300 credits; your balance is 420.",
"param": null
}
}400invalid_requestoptional401authentication_erroroptional402insufficient_creditsoptional404not_foundoptional409invalid_stateoptional422validation_erroroptional429rate_limit_erroroptional500api_erroroptionalThe rolling flow
A giveaway moves through four states. You can drive each transition explicitly (the granular endpoints below), react to webhooks, or poll Retrieve a giveaway for the current status.
pullingstatusoptionalpulledstatusoptionalcommittedstatusoptionaldrawnstatusoptionalCommit → reveal, in two calls
The rolling flow
End to end in one script: create a giveaway, wait for the pull, commit a block, draw the winners, and print the public proof URL.
const RTG = "https://api.retweet.gg/v1";
const key = process.env.RTG_API_KEY; // rtg_live_...
const headers = {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
};
// 1. Create the giveaway — this starts the retweeter pull.
let g = await fetch(`${RTG}/giveaways`, {
method: "POST",
headers,
body: JSON.stringify({
tweet_url: "https://x.com/retweetgg/status/1799887766554433221",
winner_count: 3,
prizes: ["1 SOL", "0.5 SOL", "Whitelist spot"],
exclude_bots: true,
}),
}).then((r) => r.json());
// 2. Poll until the pull finishes (or subscribe to the giveaway.pulled webhook).
while (g.status === "pulling") {
await new Promise((r) => setTimeout(r, 2000));
g = await fetch(`${RTG}/giveaways/${g.code}`, { headers }).then((r) => r.json());
}
// 3. Commit a Solana block (the commit), then draw once it finalizes (the reveal).
await fetch(`${RTG}/giveaways/${g.code}/commit`, { method: "POST", headers });
const drawn = await fetch(`${RTG}/giveaways/${g.code}/draw`, {
method: "POST",
headers,
}).then((r) => r.json());
console.log(drawn.winners.map((w) => `#${w.position} @${w.entrant.handle}`));
console.log(`Proof: https://retweet.gg/verify/${g.code}`);Prefer one call?
https://api.retweet.gg/v1/rolls with the same body as Create a giveaway to run pull → commit → draw synchronously and get the finished proof in a single response. Handy for small pools; use the granular endpoints when you want to show progress or handle big giveaways.The rolling flow
The draw engine is pure and deterministic — every winner is a function of the committed blockhash, the entry-list hash, and a nonce. The verify endpoint (and the public verify page) re-runs exactly this math.
index(nonce) = HMAC_SHA256(
key = blockhash,
msg = entries_hash + ":" + nonce
) mod N // interpreted big-endian, N = number of entrantsid:handle lines) is the entries_hash we commit to.The 256-bit HMAC output dwarfs any realistic entry count, so modulo bias is cryptographically negligible. The algorithm is versioned as rtg-draw-v1 and stamped into every proof.
Endpoints
/v1/giveawaysCreates a giveaway from a tweet and immediately starts the retweeter pull. Returns right away with status: "pulling"; watch for the giveaway.pulled webhook or poll the giveaway to know when the entry list is ready.
tweet_urlstringrequiredwinner_countintegerrequiredprizesstring[]optionalexclude_botsbooleanoptionalfalse. When true, accounts flagged by the authenticity heuristic are moved to the excluded set before the draw.requirementsobjectoptionalban_list_idsstring[]optionalbanned.webhook_urlstringoptionalmin_followersintegeroptionalmin_account_age_daysintegeroptionalmust_follow_handlesstring[]optionalmin_media_countintegeroptionalmust_be_verifiedbooleanoptionalmust_have_pfpbooleanoptionalcurl -X POST https://api.retweet.gg/v1/giveaways \
-H "Authorization: Bearer rtg_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"tweet_url": "https://x.com/retweetgg/status/1799887766554433221",
"winner_count": 3,
"prizes": ["1 SOL", "0.5 SOL", "Whitelist spot"],
"exclude_bots": true,
"requirements": {
"min_followers": 100,
"must_follow_handles": ["retweetgg"]
},
"webhook_url": "https://your.app/hooks/retweet"
}'{
"code": "ROLL-8DO2",
"status": "pulling",
"tweet_url": "https://x.com/retweetgg/status/1799887766554433221",
"tweet_id": "1799887766554433221",
"author_handle": "retweetgg",
"winner_count": 3,
"prizes": ["1 SOL", "0.5 SOL", "Whitelist spot"],
"entrant_count": null,
"entries_hash": null,
"algo_version": "rtg-draw-v1",
"created_at": 1719936000000,
"created_by_handle": "yourhandle"
}Endpoints
/v1/giveaways/{code}Fetches the current state of a giveaway. While status is pulled or later, the response includes the entries_hash, the exclusion summary, and (once drawn) the winners. Poll this to follow a giveaway to completion.
{
"code": "ROLL-8DO2",
"status": "pulled",
"entrant_count": 1284,
"raw_count": 1309,
"duplicates_removed": 25,
"entries_hash": "9f3c1b0e…a71e",
"excluded": [
{ "entry_index": 12, "handle": "airdrop_bot_4821", "reason": "bot", "detail": "Flagged as likely bot" },
{ "entry_index": 77, "handle": "blocked_partner", "reason": "banned", "detail": "Your ban list" }
],
"credits_charged": 1309
}Endpoints
/v1/giveawaysReturns your giveaways, newest first, with cursor pagination.
limitintegeroptionalstarting_afterstringoptionalcode to page after.statusstringoptionalpulling, pulled, committed, drawn).{
"object": "list",
"has_more": true,
"data": [
{ "code": "ROLL-8DO2", "status": "drawn", "winner_count": 3, "entrant_count": 1284, "created_at": 1719936000000 },
{ "code": "ROLL-2K9D", "status": "drawn", "winner_count": 1, "entrant_count": 842, "created_at": 1719849600000 }
]
}Endpoints
/v1/giveaways/{code}/commitPublishes the commit: the entry-list hash plus a future Solana slot number (~60s out). Neither you nor we can know that slot’s blockhash yet, which is exactly what makes the eventual roll ungrindable. Requires status: pulled.
curl -X POST https://api.retweet.gg/v1/giveaways/ROLL-8DO2/commit \
-H "Authorization: Bearer rtg_live_xxxxxxxxxxxx"{
"code": "ROLL-8DO2",
"status": "committed",
"entries_hash": "9f3c1b0e…a71e",
"round": {
"round": 0,
"slot": 296214877,
"blockhash": null,
"source": "solana-mainnet",
"committed_at": 1719936120000,
"fetched_at": null
}
}blockhash and fetched_at stay null until the block is revealed at draw time. The committed slot is public immediately — anyone can watch that slot land on-chain.Endpoints
/v1/giveaways/{code}/drawPerforms the reveal: reads the committed slot’s blockhash and selects winners with the selection algorithm. Requires status: committed. If the committed slot has not finalized yet, you get a 409 invalid_state — retry in a few seconds.
curl -X POST https://api.retweet.gg/v1/giveaways/ROLL-8DO2/draw \
-H "Authorization: Bearer rtg_live_xxxxxxxxxxxx"{
"code": "ROLL-8DO2",
"status": "drawn",
"rounds": [
{
"round": 0,
"slot": 296214877,
"blockhash": "7Xn2…q9Ah",
"source": "solana-mainnet",
"committed_at": 1719936120000,
"fetched_at": 1719936182000
}
],
"winners": [
{ "position": 1, "round": 0, "nonce": 0, "entry_index": 847, "prize": "1 SOL",
"entrant": { "id": "1700000000000000847", "handle": "degenmaxi", "display_name": "Degen Maxi", "x_verified": true } },
{ "position": 2, "round": 0, "nonce": 3, "entry_index": 219, "prize": "0.5 SOL",
"entrant": { "id": "1700000000000000219", "handle": "solbuilder", "display_name": "Sol Builder" } },
{ "position": 3, "round": 0, "nonce": 4, "entry_index": 1102, "prize": "Whitelist spot",
"entrant": { "id": "1700000000000001102", "handle": "wagmiwren", "display_name": "Wagmi Wren" } }
]
}Endpoints
/v1/giveaways/{code}/rerollsReplaces one winner slot — for a no-show, a bot that slipped through, or a manual swap. The reroll reuses the committed block and advances the nonce; the outgoing winner is excluded so they can’t reappear. It is appended to the proof’s roll log, so every reroll stays independently verifiable.
positionintegerrequiredreasonstringrequiredmanual, bot, no_response, fake_account. Tallied in the proof so a giveaway’s replacement rate is auditable.curl -X POST https://api.retweet.gg/v1/giveaways/ROLL-8DO2/rerolls \
-H "Authorization: Bearer rtg_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "position": 2, "reason": "bot" }'{
"code": "ROLL-8DO2",
"position": 2,
"reason": "bot",
"from": "solbuilder",
"to": "novalabs",
"winner": {
"position": 2, "round": 0, "nonce": 7, "entry_index": 640, "prize": "0.5 SOL",
"entrant": { "id": "1700000000000000640", "handle": "novalabs", "display_name": "Nova Labs" },
"replaced": { "handle": "solbuilder", "reason": "bot" }
}
}Endpoints
/v1/giveaways/{code}/proofReturns the full giveaway object — the complete, self-contained proof. It is byte-for-byte what the public verify page renders, including the canonical entrant list, every committed block, every roll, and the exclusion set. Anyone can feed it straight into verify.
Public by design
retweet.gg/verify/{code} or pass the code to the verify endpoint from their own infrastructure.Endpoints
/v1/verifyIndependently recomputes a finished draw from its proof. Pass a code (we load the published proof) or an inline proof object. We re-hash the entry list, re-derive every winner and every roll from its block + nonce, and confirm no index was won twice.
curl -X POST https://api.retweet.gg/v1/verify \
-H "Authorization: Bearer rtg_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "code": "ROLL-8DO2" }'{
"ok": true,
"entries_hash_ok": true,
"recomputed_entries_hash": "9f3c1b0e…a71e",
"unique_ok": true,
"entrant_count": 1284,
"winners": [
{ "position": 1, "handle": "degenmaxi", "round": 0, "nonce": 0,
"claimed_index": 847, "recomputed_index": 847, "ok": true }
],
"rolls": [
{ "seq": 1, "id": "ROLL-8DO2·R1", "position": 1, "kind": "initial",
"handle": "degenmaxi", "round": 0, "nonce": 0,
"claimed_index": 847, "recomputed_index": 847, "superseded": false, "ok": true }
]
}ok is the single source of truth: it is true only when the entry hash matches, all winner indices are unique, and every roll recomputes to its claimed index. A skeptic can run the same check offline with the published proof — this endpoint just does it for you.
Objects
The giveaway object is the proof. Once status is drawn, it carries everything needed to recompute the result from scratch.
codestringoptionalstatusstringoptionaltweet_urlstringoptionaltweet_idstringoptionalauthor_handlestring | nulloptionalprizestringoptionalprizesstring[]optionalwinner_countintegeroptionalentrantsEntrant[]optionalentries_hashstringoptionalentrants.excludedExclusion[]optionalroundsRound[]optionalwinnersWinner[]optionalwinner_count once complete).rerollsRerollEvent[]optionalrollsRoll[]optionalrequirementsobject | nulloptionalcreated_atintegeroptionalcreated_by_handlestringoptionalalgo_versionstringoptionalrtg-draw-v1).Objects
One retweeter in the entry pool. The id is the stable X user id used for ban matching and the canonical sort.
idstringoptionalhandlestringoptionaldisplay_namestringoptionalavatar_urlstringoptionalx_verifiedbooleanoptionalfollowers_countintegeroptionalaccount_created_atintegeroptionalmedia_countintegeroptionalfollowingstring[]optionalhas_pfpbooleanoptionalfollowing_countintegeroptionalbiostringoptionalObjects
An entry index removed from contention before drawing. Excluded indices stay in the hashed canonical list (so the hash is reproducible) but the engine skips them.
entry_indexintegeroptionalentrants.handlestringoptionalreasonstringoptionalbanned, bot, duplicate, min_followers, account_too_new, not_following, min_media, not_verified, no_pfp.detailstringoptionalObjects
A committed Solana block — the random seed for up to 10 picks.
roundintegeroptionalslotintegeroptionalblockhashstring | nulloptionalsourcestringoptionalsolana-mainnet or demo (test keys).committed_atintegeroptionalfetched_atinteger | nulloptionalObjects
A single winner, with the full nonce trace needed to reproduce it.
positionintegeroptionalroundintegeroptionalnonceintegeroptionalentry_indexintegeroptionalentrants.entrantEntrantoptionalprizestringoptionalpicked_atintegeroptionalreplacedobject | nulloptional{ handle, reason }.Objects
One roll in the append-only history — one record per initial pick and one per reroll. Every roll is independently checkable from its own (block, nonce).
seqintegeroptionalpositionintegeroptionalkindstringoptionalinitial or reroll.reasonstring | nulloptionalroundintegeroptionalnonceintegeroptionalentry_indexintegeroptionalentrants.entrantEntrantoptionalsupersededbooleanoptionalatintegeroptionalWebhooks
Subscribe once and let retweet.gg push lifecycle updates instead of polling. Set a webhook_url per giveaway or a default in settings; we POST a signed JSON event on each transition.
giveaway.pulledeventoptionalentries_hash is ready.giveaway.committedeventoptionalgiveaway.drawneventoptionalgiveaway.rerolledeventoptional{
"id": "evt_1Lq9x2…",
"type": "giveaway.drawn",
"created": 1719936182000,
"data": {
"code": "ROLL-8DO2",
"status": "drawn",
"winner_count": 3,
"proof_url": "https://retweet.gg/verify/ROLL-8DO2"
}
}2xx within 5 seconds. We retry non-2xx responses with exponential backoff for up to 24 hours; events may arrive more than once, so treat handlers as idempotent (dedupe on id).Webhooks
Every webhook carries an X-RTG-Signature header — an HMAC-SHA256 of {timestamp}.{raw body} keyed by your webhook secret (find it in settings). Verify it against the raw request body before trusting an event.
import crypto from "node:crypto";
function verify(rawBody, signatureHeader, secret) {
// Header format: t=1719936182,v1=<hex hmac>
const parts = Object.fromEntries(
signatureHeader.split(",").map((kv) => kv.split("=")),
);
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1),
);
}Reference
The REST surface is versioned in the path (/v1). Additive, backwards-compatible changes (new fields, new event types) ship without a version bump — write your parsers to ignore unknown fields. Breaking changes ship under a new path prefix. Separately, the draw math is versioned as rtg-draw-v1 inside every proof so a verifier always knows which recompute path to use, even years later.
Reference
The API spends the same credits as the dashboard: 1 credit per retweeter pulled. A 1,300-retweeter giveaway costs ~1,300 credits, charged when the pull runs. Committing, drawing, rerolling, and verifying are free.
Pull1 credit / retweeteroptionalCommit / Draw / RerollfreeoptionalVerify / Retrieve / ListfreeoptionalTest keysfreeoptionalIf a pull would exceed your balance you get a 402 insufficient_credits and nothing is charged. Top up here.
Reference
Safely retry POST requests by sending an Idempotency-Key header with a unique value (e.g. a UUID). If a request with the same key is replayed, you get the original response back instead of a second pull or a second draw — essential for network retries.
curl -X POST https://api.retweet.gg/v1/giveaways \
-H "Authorization: Bearer rtg_live_xxxxxxxxxxxx" \
-H "Idempotency-Key: 6f1a…-8c2b" \
-H "Content-Type: application/json" \
-d '{ "tweet_url": "https://x.com/…/status/17998…", "winner_count": 1 }'Official SDKs