What it is
An experimentation primitive built on /api/ints. Pass clientSeed = experimentId + ":" + userId and get a uniform integer the same way, every time, forever. Combine that with a weighted variant table and you have a reproducible bucket assignment that's also verifiable by a third party.
Internal hash-based bucketing (the standard "hash userId with an experiment salt") gives you reproducibility but not verifiability — change the salt or lose the code path and historical assignments become unprovable. The A/B test bucketing API solves both.
The pain point
Three things usually break in homegrown bucketing:
- Audit gaps. A regulator asks "show me that user 8821 was really in the treatment arm of the checkout v2 test on March 14" — and you have to dig through old code, hope the salt hasn't changed, and re-run the hash by hand.
- Cross-platform drift. Server and client implement the hash slightly differently and users flip variants depending on where they're served.
- Trust on external readouts. Sharing experiment results with a partner or a board member is a "trust me, the numbers say so" exercise.
A verifiable RNG fixes all three by making the assignment a deterministic, signed function of inputs everyone can see.
Try it live
Two users, same experiment, deterministic buckets. Hit Run twice — same input, same output, every time.
curl "https://api.provable.io/api/ints?clientSeed=exp_checkout_v2:user_8821&count=1&min=1&max=100"
curl "https://api.provable.io/api/ints?clientSeed=exp_checkout_v2:user_4410&count=1&min=1&max=100"
Integration snippet
// Assign a user to a variant of a multi-arm experiment.
async function bucketFor(experimentId, userId, variants) {
// variants = [{ name: "control", weight: 50 }, { name: "treatment", weight: 50 }]
const total = variants.reduce((s, v) => s + v.weight, 0);
const clientSeed = `${experimentId}:${userId}`;
const url = new URL("https://api.provable.io/api/ints");
url.searchParams.set("clientSeed", clientSeed);
url.searchParams.set("count", "1");
url.searchParams.set("min", "1");
url.searchParams.set("max", String(total));
const res = await fetch(url, {
headers: { "x-api-key": process.env.PROVABLE_KEY }
});
const { outcome, serverHash } = await res.json();
let acc = 0;
for (const v of variants) {
acc += v.weight;
if (outcome[0] <= acc) {
return { variant: v.name, clientSeed, serverHash };
}
}
}
const { variant, serverHash } = await bucketFor(
"exp_checkout_v2",
"user_8821",
[{ name: "control", weight: 50 }, { name: "treatment", weight: 50 }],
);
// Persist `variant` + `serverHash` with the assignment event.
Why this is fair
- Same input, same output. The HMAC stream is deterministic. The first bucket call defines the assignment; cache it locally and you never have to ask again — but you can, and you'll get the same answer.
- Pre-commitment. Publish the experiment's
serverHashwhen you launch (a doc, a deploy log, an audit channel). Anyone who later doubts an assignment can re-derive it from the published seed. - Auditor-ready. "Show me your seed and your inputs" is a finite, well-defined request. No internal salt to dig up, no version of the hashing code to recover.
Rollout patterns
- Sticky expansions. Start with treatment = rolls
1..10, then grow to1..50. Everyone in the first 10% is still in treatment under the expanded band; no one gets reshuffled. - Mutually exclusive experiments. Use distinct
experimentIdprefixes — each gets its own independent random stream. - Stratified sampling. Add a stratum tag to the seed:
exp_x:cohort_us:user_42. Each stratum gets a deterministic, balanced split. - Holdouts. Reserve a fixed band (e.g.
91..100) per experiment as a global holdout that no variant touches.
The full recipe
This page is the keyword-targeted entry point. For the long-form walkthrough — including stickiness across rollouts, deterministic holdouts, and how to wire bucketing into a real experimentation pipeline — see the recipe: Deterministic A/B Buckets.
Where it fits
- Growth and product experimentation at companies that want auditable assignments without paying for a heavyweight platform.
- Regulated industries (fintech, health, gaming) where bucket assignments may need to survive a compliance review.
- Multi-region SaaS where server-side and client-side bucketing must produce identical results.
- Open-source projects running experiments transparently with their community.
Related
- Recipe: Deterministic A/B Buckets
- Raffle Picker API
- How provably fair works
- Verify a published assignment →
FAQ
What clientSeed should I use for A/B bucketing?
Combine experiment ID with user ID: clientSeed = experimentId + ':' + userId. The HMAC stream is deterministic, so the same seed always returns the same bucket, and unrelated experiments stay independent.
Should I cache the bucket assignment?
Yes. The first call defines the assignment — cache it locally to avoid the round trip on every page view. The API stays deterministic, so re-derivation is always possible from the committed serverHash.
How do I grow treatment from 10% to 50% without reshuffling existing users?
Use sticky bands. Treatment = rolls 1..10 at 10%; bump to 1..50 at 50%. Everyone in the first 10% remains in treatment under the expanded band; nobody flips variants.
How can a regulator audit an assignment months later?
Hand them the published serverHash, the clientSeed (experimentId:userId), and the variant weight table. They re-run /api/ints and confirm the bucket — no internal salts or legacy hashing code required.