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.