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:

  1. 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.
  2. Cross-platform drift. Server and client implement the hash slightly differently and users flip variants depending on where they're served.
  3. 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

Rollout patterns

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

Related