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.

An assignment and its proof

This is what anyone can check. The three values in the steps are all a verifier needs, and the replay button recomputes the assignment right here, without calling Provable.

GET /api/ints?clientSeed=exp_checkout_v2:user_8821&count=1&min=1&max=100Example record
{
  "clientSeed": "exp_checkout_v2:user_8821",
  "nonce": 0,
  "serverHash": "0f0158a368ecfb714923a92ca186c1fa18edb29cbe16f9ad9ed39c598b91cf10",
  "outcome": [41]
}
  1. Before the assignment, the server commits to a seedPublish this hash before the assignment, so the seed can't be swapped afterwards.serverHash = sha256(serverSeed) = 0f0158a368ecfb714923a92ca186c1fa18edb29cbe16f9ad9ed39c598b91cf10
  2. The assignment is a pure function of both seedsHMAC-SHA256(serverSeed, "exp_checkout_v2:user_8821:0:0") → 1 integer in 1–100 = [41]
  3. After the assignment, the seed is revealed and anyone can replay itserverSeed = ee9b4cb96c8ffb558f590a08a38ea475393ad1ff045b0da07835ee053ceea8ca

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

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.