# Payout bid modification

> **Quick start**: Payout Bid Modification is about optimizing your dynamic RTB
> payouts/bids to publishers so you can increase your margin and make more money.

## Who should read this

This guide is written for anyone running a network or brokering calls on
Retreaver — as well as, indirectly, the publishers and buyers on the other side of
that traffic, since this feature changes what shows up in their numbers. It is worth the read is this is how you could directly increase the revenue of your networking business or as publisher to know what tools are available to networks to optimize their payout to you. Let's be real - you will probably ask your AI agent to summarize it for you and you are right to do this, but the information should be here for us to refer AI Agents and real people to it. 

Let' start with the most important question before diving into this pages and pages of information.

> **"I'm not technical. I don't understand APIs, RTB, or the math here. Why can't I
> just press a 'Make more money' button and let Retreaver handle it?"**
>
> You can. Ideally, that button is you telling your integration partner "make more
> money for me" — most of the mechanics and details and examples and rules in this guide (the API/MCP calls, the table
> format, the math behind a rule) are really written for that partner, not for you.
>
> But you're still the one running and responsible for the the network. This means means you're still the one who
> has to answer for it. When a publisher asks in a Telegram group why their payout changed today compared to yesterday and why is it not what you've agreed before hand, or why you're
> bidding what you're bidding on their calls, or a buyer asks why their traffic just
> went up or down — that's you, not your partner that has to face the question. You're also
> the one in control of the feature: whether it's on and is it applied.
>
> So no, you don't have to understand how Payout Bid Modification works underneath. But
> if you come to us, or to your integration partner, with questions about what's
> happening to your traffic or your payouts, this is the guide we'll point you to. And
> this process can realistically move your margin 2–3x in either direction — up(good) or
> down(bad) — so it's worth actually understanding what you're turning on before you flip
> the switch. I hate to say it but with great power... let's just say you'll want to have read this first and eventually if you go down the payout bid modification route you would need to read this.

## Why: growing your margin by testing dynamic payouts

When you broker calls as a network or just a publisher with some additional traffic sources, you collect revenue from buyers, keep a margin, and pass the rest
to your publishers as payout. That payout is often a simple, static percentage of
revenue — say 15%. This is configured on the conversion groups on retreaver. It could be dynamic in a sense that it could from a webhook. Generally this 15% is easy to reason about, but it could, leaves money on the table: some
publishers might (but we are not sure) keep sending you the exact same calls for a lower payout, and you'd
never know it because you never tested it.

In the bidding world, deliberately paying out less than the "sticker" rate to see how
much margin the market will actually bear has a known name — **bid shading**. On
Retreaver we call it **Payout Bid Modification**.

The idea: instead of always paying a publisher a flat 15%, you dynamically adjust the
payout — maybe up to 20%, 25%, or more — for some slice of traffic, and watch what
happens. This is not about blindly changing payout percentages because it feels right —
it's a controlled, data-driven experiment: you analyze real traffic first, form a
hypothesis about what the market will bear, then test it deliberately on a defined slice
of traffic before rolling any change out further. You do this continuesly, every 10 minutes, every hour, a few times a day, every day. If you are doing it every week or every month it is much better to use the conversion groups - they are easier to reason about. Payout Bid Modification is advanced feature that we've made simple, but it is still more advanced than "My payout will be 15%".

So what are the upside and the risk:

- **The upside**: every point you lower the payout is pure margin. Modifying a $30
  call's payout from 85% down to 50% takes your margin from $4.50 to $15 — more than
  3x — on that single call.
- **The risk**: modify too aggressively and the publisher notices their effective payout
  dropped, decides it's no longer worth it, and stops sending you traffic. What worked
  an hour ago might not work now, because the publisher's own tolerance shifts hour to
  hour, day to day. Some publishers will never tolerate this at all: on their end
  they're tracking an Earnings Per Call / Revenue Per Call number for you, and if you
  drop the payout too far that number falls with it — at this point you're no longer
  their best-paying option and they will route their traffic to someone else first.

So the real problem isn't "should I lower the payout" — it's "how do I find the right
number, safely." That means splitting traffic and experimenting: send 15% payout on one slice of
requests and 20% (or 18%, or 25%, or 50%) on another, measure whether the publisher's
volume holds up, and iterate.

We are used to this in the Affiliate space - we are constantly running control A/B tests and variants. This is what the feature is about - allowing you to run, you, your AI agent, your integration partner, a well controlled experiment.

Doing this properly and safely is genuinely hard — it involves a lot of moving parts:
segmenting traffic correctly, running the analysis, defining the test rules, applying
them without disrupting the rest of your traffic, and then measuring the outcome.
Retreaver's job is to make that process easy by giving you the right set of tools for
every step of it.

We would like to make something very clear:

**Retreaver does not tell you what the modification should be.** This needs to be said
plainly: Retreaver does not decide, calculate, or recommend what your new payout
percentages should be. Figuring out the right number — the math and the analysis behind
it — is entirely on you, or on a third party you choose to bring in for that work. What
Retreaver does is apply the rules that you or that third party come up with, and track
the impact of those rules on live traffic.

## Enabling the feature

Payout Bid Modification is an opt-in add-on, turned on per company from the **Store**
(`/store`). It's off by default — find it in your store, and use the toggle to enable
it.

The feature has a cost: we charge per rule applied. You can try it for free during an
initial trial period, but for ongoing use you'll need to get in touch with your account
manager to work out pricing — pricing isn't self-serve yet.
![payout bid modification guide](/media/5f/5f71eafef2e369b6666b69909c37b382a0b37dda7ea7d8545ee06cf3480f9965.png)

## How it works, at a high level

![pbm loop](/media/80/802e98887fd66791b233083592907ad653ded2f63dce48d3786ce5702da84542.png)

1. **Download your data.** Export your RTB inbound requests and your Calls data from
   Retreaver. (Covered in a later section of this guide.)
2. **Analyze it.** Figure out which segments of traffic can tolerate a lower payout
   without losing volume, and by how much. This is a modeling/statistics exercise — do
   it yourself, or hand the export to a third party or integration partner who
   specializes in this kind of analysis.
3. **Build a Payout Bid Modification table.** The output of that analysis is a set of
   rules — a table — that tells Retreaver which payout to apply to which traffic
   segment. (The exact table format is covered later in this guide.)
4. **Upload it, then repeat.** After a few hours or a few days of the table running live,
   pull fresh data, re-analyze, and upload a new table. This isn't "set once" —
   publisher tolerance drifts, so the loop keeps going.

## Where it fits in the RTB flow

To understand *why* Payout Bid Modification is built the way it is, it helps to see the
full standard RTB flow it slots into:

![pbm rtb flow](/media/2f/2fc094a269f3fa9805ae9a531bb1779fd492e4848987cc7fd0963c26104032c0.png)

1. A publisher sends an RTB request.
2. Retreaver matches candidate buyers by business hours and caps.
3. Retreaver pings the buyers that are potentially available.
4. Retreaver matches buyers by availability and bid, and selects the winning buyer based
   on the campaign's routing method — route by priority and weight, route by bid, or
   route by performance.
5. Retreaver calculates the payout to the publisher from the winning bid, using the
   campaign's configured conversion group. For example, a $30 revenue call at an 85%
   payout becomes `30 * 0.85 = $25.50`.
6. **Retreaver applies Payout Bid Modification**, using the active table for the
   campaign, to adjust that payout. **This is the new feature on Retreaver**.
7. Retreaver returns the (possibly modified) payout.

### Why this happens instantly, in-process, with no webhook

Step 6 is deliberately *not* an external call to a third-party service. It's applied
in-memory, from a table Retreaver already holds, in microseconds — genuinely
sub-millisecond, not just "fast."

This matters because of where it sits in the flow: it's sequential, at the tail end of
an already time-boxed RTB request. Even a very fast external service adds real cost
here:

- A service that returns in 300ms on average is already burning roughly 10% of a
  typical RTB request's total time budget just for this one adjustment.
- And "300ms on average" is optimistic — we've observed that even the fastest dialers,
  serving availability out of an in-memory Redis cache, see P95 latencies around 300ms
  once real network communication is involved. Network hops add latency that's easy to
  underestimate when you're only looking at the happy-path average.

Because Payout Bid Modification rules are uploaded ahead of time and evaluated entirely
in-memory on the Retreaver side, none of that network cost applies. There's nothing to
ping, nothing to wait on — the modification is resolved as part of the same request that's
already computing the payout.

### The result

Going back to the example: a call that would have paid out `$30 * 0.85 = $25.50` (a
$4.50 margin at a hypothetical 15% take rate) instead gets modified down to
`$30 * 0.50 = $15.00`. If the underlying margin logic is "revenue minus payout," that's
the difference between a $4.50 margin and a $15 margin — over 3x — on traffic the
publisher was, per the analysis, still willing to send.

## Increased margin has a cost

The margin math above is only half the story — a bigger margin isn't free money, and
Retreaver's routing options are exactly what make that true.

On Retreaver, a publisher's campaign isn't locked into a single buyer. Traffic can be
routed by priority and weight, by bid, or by **performance** — and performance is the
natural counterweight to an overly aggressive Payout Bid Modification rule. Performance
is measured simply as total revenue divided by number of calls: effectively, the
average payout the buyer/broker has actually been sending the publisher. Retreaver
recalculates it every few minutes. So the moment a Payout Bid Modification rule pushes
payouts down too far, that performance score drops for the same call volume, and
Retreaver automatically starts steering more of the traffic to the next best-performing
buyer instead.

In other words, route-by-performance already protects publishers against a buyer
shading too aggressively — no one has to notice and intervene, the market corrects
itself within a few minutes. Any margin gained by cutting the payout too deep tends to
be short-lived: it costs you volume before it can compound into real profit.

Publishers who want more direct control on top of that automatic protection have a few
more tools available today:

- **Add more buyers to the campaign.** With several buyers competing for the same
  traffic, one buyer shading too aggressively just means performance routing shifts
  that share to another buyer — the traffic isn't lost, it's protected by
  diversification.
- **Route by bid.** Route dynamically to whichever buyer is paying the most right now,
  instead of relying on a rolling performance average to catch up.
- **Monitor actively.** Have your team, or an automated/AI agent, watch revenue per
  call hour by hour, and cut off a buyer the moment its payout drops.

## Built for both sides of the market

On Retreaver, we care about the whole market — publishers, brokers/networks, and
buyers alike.

Publishers already have tools to maximize the return on their own traffic: they choose
how it's routed — by priority and weight, by bid, or by performance — to consistently
find whichever buyer is paying them best. That's what publishers care about, and
Retreaver gives them the levers to act on it.

This is how easy it is for publishers to turn on route by performance.
![Screenshot 2026 09 08 at 15.41.54](/media/b3/b36210215d875b08cd1f886b237ffff6bfea1771cccb5f7ddc0277605bee0a83.png)

Payout Bid Modification is the equivalent lever for brokers/networks: a tool to
optimize margin, which is what brokers/networks care about.

Both sides are free to use their tools to compete for the best outcome — a publisher
optimizing which buyer to send traffic to, and a broker optimizing what to pay for that
traffic. Retreaver doesn't decide who wins that; it makes sure both sides have real
tools to play with, and that the market settles in near real time rather than being
decided once and left static.

## Downloading your RTB inbounds data

Step 1 of the workflow above is getting your data out of Retreaver. You have two ways
to do that: an **export** (recommended) or **direct paginated API requests**. Both are
available over the regular API and over MCP.

### A note on API keys and third parties

Be deliberate about who you hand an API key to here, because of what this data plus
this feature adds up to. A third party who can pull your Calls and RTB inbound data
*and* upload a Payout Bid Modification table has, in practice, full visibility into your
revenue, your payouts, and your margins — and the ability to change what you pay out.
That's effectively everything about how your business runs.

- **If you're comfortable giving a third party that level of access**, issue them a key
  tied to a superuser in your organization. A superuser's API key can act across the
  whole company, which is the point — they need to both read the data and, once they've
  done the analysis, upload the resulting table.
- **If you're not comfortable with that**, don't hand out a key at all. Run the
  download side yourself, and only send the third party the exports you're willing to
  share — say, just specific campaigns or specific publishers — instead of company-wide
  access. You'd then take their resulting table and upload it yourself. We can also
  issue more narrowly-scoped keys to manage this a bit more finely. Either way, keep in
  mind the ceiling on how fine-grained this can get: the more you restrict what a third
  party can see, the less complete their analysis can be, and a partner doing this
  properly ultimately needs a fairly full picture of your operation to calculate a
  Payout Bid Modification table that actually works.

> **Default to exports.** Your RTB inbounds table can hold millions of rows, and
> paginated requests only return 100 rows at a time — walking through millions of
> records 100 at a time is pointless. Exports handle millions of records just fine and
> are the right tool almost always. Only reach for paginated requests when you're
> pulling a genuinely small, tightly-filtered slice — say one publisher's traffic over a
> 10–15 minute window — where a handful of pages is all you'd ever need.
>
> The examples below show the shape of these calls. Endpoints, params, and response
> fields evolve — always check the current API documentation before building against
> them.

### Creating and downloading an export (API)

Create the export, then poll it until it's done, then download the file:

```bash
# 1. Kick off the export
curl -X POST "https://api.retreaver.com/api/v5/exports/rtb_inbounds?api_key=<API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
        "export": {
          "format": "csv",
          "params": { "campaign_id": [123], "created_at": "last_7d" }
        }
      }'
# => { "id": 501, "in_progress": true, "format": "csv" }

# 2. Poll until it's finished
curl "https://api.retreaver.com/api/v5/exports/rtb_inbounds/501?api_key=<API_KEY>"
# => { "id": 501, "in_progress": false, "export_count": 4820, ... }

# 3. Once in_progress is false, download it — this is an authenticated endpoint of
#    its own (not a raw file URL), which redirects you to the actual file
curl -L -o rtb_inbounds.csv \
  "https://api.retreaver.com/api/v5/exports/rtb_inbounds/501/download?api_key=<API_KEY>"
```

### Paginated inbounds requests (API — small segments only)

```bash
curl "https://api.retreaver.com/api/v5/rtb_inbounds?api_key=<API_KEY>&affiliate_id[]=456&created_at_start=2026-09-08T09:00:00Z&created_at_end=2026-09-08T09:15:00Z&page=1"
# => { "rtb_inbounds": [ { "call_uuid": "...", "campaign_id": 123, "affiliate_id": 456,
#      "target_id": 789, "revenue": 30.0, "payout": 25.5, "status": "won", ... }, ... ] }
```

### Creating and downloading an export (MCP)

```
exports.rtb_inbounds.manage(action: "create", format: "csv",
  campaign_id: [123], created_at: "last_7d")
# => { export_id: 501, in_progress: true, format: "csv" }

exports.rtb_inbounds.manage(action: "show", export_id: 501)
# => { export_id: 501, in_progress: false, export_count: 4820, ... }

# Once in_progress is false, download via the same authenticated download endpoint
# used by the API (GET .../exports/rtb_inbounds/501/download?api_key=<API_KEY>) —
# it redirects you to the actual file, it isn't a raw file URL you can fetch directly.
```

### Paginated inbounds requests (MCP — small segments only)

```
rtb_inbounds.read(affiliate_id: [456],
  created_at_start: "2026-09-08T09:00:00Z", created_at_end: "2026-09-08T09:15:00Z")
# => { rtb_inbounds: [ { call_uuid: "...", campaign_id: 123, affiliate_id: 456,
#      target_id: 789, revenue: 30.0, payout: 25.5, status: "won", ... }, ... ] }
```

### Downloading the export from the browser

You don't need the API at all for a one-off pull. From the browser: go to your RTB
Inbounds report, apply your filters, and request a CSV export the same way you would
any other report — once it finishes processing, a download link appears and the CSV
downloads directly.



## Downloading your Calls data

RTB inbounds aren't the only export you need — you also need your **Calls** data.

A Call is the real, completed telephony event a winning bid turned into: an actual
conversation that started at an actual time, ran for an actual duration, and produced
actual billable minutes. That's fundamentally different from an RTB inbound, which is
just a bid — evaluated, won or lost, in milliseconds. It can take thousands of RTB
inbound bid requests behind the scenes for a single call to actually happen, so
naturally there are always far more bid records than there are calls. That's why the
two are kept as separate exports instead of one: RTB inbounds tells you the bidding
mechanics, Calls tells you the real-world outcome.

![export rtb inbounds 1](/media/80/80014f56559c15808055f9b3374160db63460e21915c01fcfd7b19d75ccaa953.png)

![export rtb inbouds 2](/media/1c/1c229b59d7e46335c533f41f50a56af4f05827c5ed99fba2ce3f2559fafd678d.png)

### The join key: `call_uuid` ↔ `uuid`

The two exports connect through one shared identifier: the `call_uuid` column on an
RTB inbound is the exact same UUID as the `uuid` column on the Call it produced. Join
`rtb_inbounds.call_uuid = calls.uuid` to line a bid up with the real call that came out
of it.

### Calls listing/pagination (API)

```bash
curl "https://api.retreaver.com/api/v5/calls?api_key=<API_KEY>&target_id[]=789&created_at_start=2026-09-01T00:00:00Z&created_at_end=2026-09-08T00:00:00Z&page=1"
# => [ { "uuid": "...", "start_time": "...", "total_duration": 185, "billable_minutes": 4,
#        "status": "completed", "revenue": 30.0, "payout": 25.5, ... }, ... ]
```

Note the filter set here is different from RTB inbounds — Calls filters by things like
`target_id`, caller/contact number, and date range rather than `campaign_id`/
`affiliate_id` directly. As always, check the current API documentation for the full
filter list.

### Calls listing/pagination (MCP)

```
calls.read(target_id: [789],
  created_at_start: "2026-09-01T00:00:00Z", created_at_end: "2026-09-08T00:00:00Z")
# => { calls: [ { uuid: "...", start_time: "...", total_duration: 185, billable_minutes: 4,
#      status: "completed", revenue: 30.0, payout: 25.5, ... } ] }
```

### Bulk export

As of this writing, Calls doesn't yet have a dedicated bulk-export endpoint over the
API or MCP the way RTB inbounds does — only a browser-based CSV export (from your
Calls report, same "request an export, get a download link" flow shown above), and
creating one there currently requires a superuser. If you need to hand a partner a
large Calls dataset, either export it from the browser yourself and share the CSV
directly, or pull it via paginated API/MCP `calls` requests filtered tightly enough
(by target and a bounded date range) that pagination stays practical. Check the API
documentation for the current state here, since this is likely to change.

## The math is on you — Retreaver applies the result

With both exports in hand — RTB inbounds and Calls, joined on `call_uuid`/`uuid` — you
(or your integration partner) have everything needed to do the actual analysis: which
segments of traffic can take a lower payout without losing volume, and by how much.
This is the modeling step, and as covered earlier, it's entirely on you or your
partner — Retreaver doesn't do this math and has no opinion on how you do it. What
Retreaver does is take whatever you land on and turn it into working rules: a Payout
Bid Modification table.

## Running an experiment: Control, Treatment, and buckets

At its core, running a Payout Bid Modification experiment is simple: split your
traffic in two.

- **Control** — traffic that flows through your existing conversion rules exactly as
  it always has. Nothing modified.
- **Treatment** — traffic that flows through your Payout Bid Modification rules
  instead, getting whatever payout the matching rule prescribes.

Then you measure: pull your exports for both groups and compare whether Control is
generating more, or less, than Treatment. That comparison is the entire point — it's
how you find out whether a rule actually worked, rather than guessing.

That leaves one real question: which slice of your traffic goes into Treatment, and
once it's there, which specific rule — which "medicine" — does it get? That's what
buckets are for.

### How bucketing works

Every RTB ping gets assigned a bucket — a number from 0 to 9999 — before any rule is
checked. The bucket isn't random and it isn't stored anywhere ahead of time; it's
calculated on the fly, deterministically, from two things: your table's `salt` and the
call's `call_uuid`:

```
bucket = SHA256("<salt>:<call_uuid>")[first 4 hex chars].to_i(16) % 10,000
```

Because it's a hash of those two values and nothing else, the same call, under the
same salt, always lands in exactly the same bucket — no matter how many times you
recompute it, no matter who's doing the computing.

That determinism is the entire point. It's what lets you or your integration partner
independently verify that Retreaver really did put a given call in the bucket it
claims to, rather than just trusting our word for it. This matters more here than in
most features: integration partners typically take a cut of the margin lift they
generate, so both sides need to be able to prove — independently — that a call was
bucketed correctly and the right rule fired, not just assume it.

**Worked example.** Say your table's salt is `acme_2026-09-08` and a call comes in
with `call_uuid = 3f2a9c10-4b8e-4d21-9a55-7e6c1b2d3f40`:

```
SHA256("acme_2026-09-08:3f2a9c10-4b8e-4d21-9a55-7e6c1b2d3f40")
  = 17ae626d41f33a4c6da7c084dca37b27ccac4dffff257ec1b0b0ac133aec898c

first 4 hex chars: "17ae" -> 0x17ae = 6062
bucket = 6062 % 10,000 = 6062
```

That call lands in bucket **6062**. Whatever rule in your table matches, say,
`pbm_bucket >= 6000 AND pbm_bucket < 7000` is the rule that fires for it — and anyone
who knows the salt can pull that same call's `call_uuid` from their RTB inbounds
export, run the exact same calculation, and get 6062 back too.

That's the actual job to be done here: after the fact, you or your partner take a
handful of rows from your RTB inbounds export, recompute each one's bucket from its
`call_uuid` and your table's `salt`, and confirm it matches the bucket — and the rule —
Retreaver recorded against it. If it doesn't, something's wrong, and you've caught it
quickly instead of trusting a black box.

## The Payout Bid Modification table: rules and format

A Payout Bid Modification table is a CSV, uploaded per campaign. Each row is one rule,
and rows are checked top to bottom — the **first matching rule wins**, so put your
most specific rules first and your catch-alls last.

Two columns are always present:

- **`rule_id`** — any identifier you want, carried through unchanged into logging and
  reporting so you can trace which rule fired on which call.
- **`payout_pct`** — the percentage of revenue to pay out when this rule matches (e.g.
  `70`), or the literal value `current` to explicitly leave the payout unmodified.
  Rule matching only ever runs for pings whose bucket landed in the Treatment range —
  genuine Control pings (bucket 0–4999, see "Running an experiment" above) never reach
  this table at all. `current` is for a rule that *does* match a Treatment ping but
  chooses not to touch it — the rule provider abstaining on a segment its model isn't
  confident about — not for defining Control itself.

Every other column header is a real Retreaver tag key, used exactly as-is, no
translation layer. Each cell uses the same `key:operator:value` shorthand used
everywhere else in Retreaver for tag conditions, just without the `key:` part (the
column header already says which key).

**To match on affiliate, campaign, target, or number, use the `system_` id** —
`system_affiliate_id`, `system_campaign_id`, `system_target_id`, `system_number_id` —
not the plain `affiliate_id`/`campaign_id`/`target_id`. Retreaver keeps two ids for
each of these: the client-supplied external id (whatever the publisher or buyer sent
us) and Retreaver's own internal id. A Payout Bid Modification table is
Retreaver-side configuration matched against Retreaver's own record of the call, so the
`system_` id is what you want — it's always present and unambiguous, where a
client-supplied id depends entirely on what the other side happened to send. Other
useful keys include `caller_state`, `sub_id`, `revenue`, and time-based keys like
`current_time_hour_utc` (or `_et`/`_ct`/`_mt`/`_pt` for a specific timezone).

For the full, current list of every key you can match on, browse the replacement
tokens page for your campaign — the same "Browse all replacement tokens" link the
table upload form itself provides (`/tags/replacement_tokens?campaign_id=<your
campaign id>`).

- A bare value defaults to equality — `CO` means "caller_state equals CO".
- Prefix an operator for anything else — `!=CA`, `>=20`, `=~^(CO|NY|TX)$`.
- `*`, or leaving the column out of the CSV entirely, both mean "no constraint from
  this key."
- The same key can appear as more than one column, to AND multiple comparators on it
  together in one row — e.g. two `revenue` columns, `>=20` and `<30`, express a range
  (revenue between 20 and 30).

One special key is always available even though it isn't a tag you set yourself:
**`pbm_bucket`** — the deterministic 0–9999 bucket described above. A bucket range is
expressed the same way as any other ranged key — two `pbm_bucket` columns, `>=8000`
and `<8700`, target exactly that 700-wide slice of traffic.

Example table:

```csv
rule_id,caller_state,system_affiliate_id,revenue,revenue,pbm_bucket,pbm_bucket,payout_pct
1,=~^(CO|NY|TX)$,*,*,*,*,*,70
2,ON,482,*,*,*,*,55
3,*,*,>=20,<30,>=5000,<7500,60
4,*,*,*,*,>=7500,<10000,50
5,*,*,*,*,*,*,current
```

Reading that top to bottom (remember: only Treatment pings ever reach this table at
all): calls from CO/NY/TX pay out 70% regardless of anything else (rule 1); Retreaver
affiliate 482's Ontario traffic pays out 55% (rule 2); a $20–30 revenue call in bucket
range 5000–7499 pays 60% (rule 3); the same revenue band in bucket range 7500–9999 pays
50% (rule 4); everything else falls through to rule 5, an explicit abstention — a
Treatment ping this table matches but deliberately leaves untouched, payout unchanged.

A couple of practical limits to design your table around: a table can hold at most
1,000 rules, and the CSV itself can't exceed 1MB — every rule is checked in order on
every eligible RTB ping, so keeping the rule count sane keeps that check fast. 1,000 is
a sensible default, not a hard ceiling baked in for its own sake — if your analysis
genuinely needs more rules than that, talk to Kiril rather than working around the
limit.

## Uploading and managing your table (API)

Once you've built your CSV, upload it against the campaign it applies to.

### Create a new table

```bash
curl -s -X POST "https://api.retreaver.com/api/v5/campaigns/16728/payout_bid_modification_tables?api_key=<API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
        "payout_bid_modification_table": {
          "csv_data": "rule_id,caller_state,pbm_bucket,pbm_bucket,payout_pct\n1,=~^(CO|NY|TX)$,*,*,70\n2,*,>=5000,<7500,60\n3,*,*,*,current\n",
          "active": true
        }
      }' | jq
```

`active: true` on create makes this table live immediately — that's what actually
starts routing Treatment pings through it — and automatically deactivates whichever
table was previously active for the campaign.

### List / show tables for a campaign

```bash
curl -s "https://api.retreaver.com/api/v5/campaigns/16728/payout_bid_modification_tables?api_key=<API_KEY>" | jq
curl -s "https://api.retreaver.com/api/v5/campaigns/16728/payout_bid_modification_tables/1?api_key=<API_KEY>" | jq
```

The `1` here is the table's own `id` — not `table_version` (an optional, user-supplied
label that isn't guaranteed to be chronological, so it isn't a reliable way to look up
a specific table).

### Rolling back to a previous version

A table is write-once: `update` can only ever flip `active`, never `csv_data` — a
correction is always a new upload (a new version), not an edit to an old one. So
there's no "undo" on the table you just created. To go back to an earlier table,
activate *that table's own id* instead:

```bash
# Reactivate an older table (id 7) — this automatically deactivates whichever table
# (e.g. id 1) was active before; you don't need a separate call for that.
curl -s -X PATCH "https://api.retreaver.com/api/v5/campaigns/16728/payout_bid_modification_tables/7?api_key=<API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"payout_bid_modification_table": {"active": true}}' | jq
```

Setting a table's own `active` to `false` (rather than activating a different one)
doesn't roll back to anything — it just leaves the campaign with no active table at
all, which makes Payout Bid Modification a no-op on every ping until you activate a
table again.

## What shows up in your exports: the `pbm_*` columns

Once Payout Bid Modification is enabled for your company, every row in your RTB
inbounds export (and the equivalent API/MCP responses) carries a set of `pbm_*`
columns alongside the usual fields — this is how you actually measure what happened,
row by row, without having to reconstruct it yourself:

- **`pbm_table_version`** — the `table_version` of whichever Payout Bid Modification
  table was active when this ping was evaluated.
- **`pbm_bucket`** — the 0–9999 bucket this call was assigned, as described above.
- **`pbm_variant`** — `"control"` or `"treatment"`, depending on which side of the
  split the bucket fell on.
- **`pbm_rule_id`** — the `rule_id` of whichever rule matched, if any. Present even
  when the matched rule's `payout_pct` was `current` (a real match that chose not to
  modify anything) — absent only when no rule matched at all.
- **`pbm_original_payout`** — the baseline payout, i.e. what the call would have paid
  out before any modification was applied.
- **`pbm_percent_of_revenue`** — the `payout_pct` the matched rule actually applied,
  when it modified the payout. Blank when nothing was modified (no rule matched, or the
  matched rule was `current`).

Comparing `pbm_original_payout` against the row's actual `payout`, grouped by
`pbm_variant`, is the measurement described above in miniature: it's exactly how you
see whether Treatment is generating more margin than Control — and if so, how much.

## A full worked example: 4 RTB inbound requests, 2 Control, 2 Treatment

Putting all of the above together — buckets, rules, and the exported `pbm_*`
columns — here's one small batch worked end to end.

Say your table (salt `acme_2026-09-08`) has two rules:

```csv
rule_id,current_time_hour_utc,current_time_hour_utc,caller_zip,system_affiliate_id,revenue,revenue,pbm_bucket,pbm_bucket,payout_pct
1,>=9,<17,90210,*,*,*,>=5000,<7500,50
2,*,*,*,482,>=20,<40,>=7500,<10000,60
```

- **Rule 1** — business-hours (9am–5pm UTC) requests from zip `90210`, in the lower
  half of the Treatment range (bucket 5000–7499) → pay out 50%.
- **Rule 2** — Retreaver affiliate 482's requests with revenue between $20 and $40, in
  the upper half of the Treatment range (bucket 7500–9999) → pay out 60%.

Both campaigns normally pay a flat 85% of revenue. Four RTB inbound requests come in;
each one wins a bid and turns into a real, completed $30-revenue call — this is the
`call_uuid` that ties the RTB inbound row to its Call, exactly as described earlier.
Each request's bucket (computed the same way shown earlier, from `salt` + its
`call_uuid`) happens to land like this:

| RTB inbound (`call_uuid`) | Bucket | Variant | Rule matched | Revenue | Original payout (85%) | Payout | Margin |
|---|---|---|---|---|---|---|---|
| `call-0001` | 4042 | Control | — (never checked) | $30.00 | $25.50 | $25.50 | $4.50 |
| `call-0003` | 2340 | Control | — (never checked) | $30.00 | $25.50 | $25.50 | $4.50 |
| `call-0002` | 5881 | Treatment | Rule 1 (bucket 5000–7499, business hours, zip 90210) | $30.00 | $25.50 | $15.00 | $15.00 |
| `call-0006` | 9182 | Treatment | Rule 2 (bucket 7500–9999, affiliate 482, $20–40 revenue) | $30.00 | $25.50 | $18.00 | $12.00 |

The two Control requests are untouched by design — same 85% payout, same $4.50 margin
each, exactly as if Payout Bid Modification didn't exist. The two Treatment requests
each matched a rule and paid out less: $15.00 and $18.00 instead of $25.50.

**Revenue is unchanged** — all four requests are still worth $30 to the buyer; Payout
Bid Modification never touches revenue, only payout. What moves is the payout, and
therefore the margin:

- Original payout, all 4 (hypothetical, if nothing were modified): `4 × $25.50 = $102.00`
- Payout, all 4 (2 Control + 2 modified Treatment): `$25.50 + $25.50 + $15.00 + $18.00 = $84.00`
- Margin, if nothing were modified: `4 × $4.50 = $18.00`
- Margin, as it actually played out: `$4.50 + $4.50 + $15.00 + $12.00 = $36.00`

That's an **$18.00 increase in margin — exactly double** — on this batch of four,
entirely from the two requests that landed in Treatment and matched a rule. Pulling
the export afterward, you'd see this directly in the `pbm_*` columns: the two Control
rows carry no `pbm_rule_id` and their `payout` equals their `pbm_original_payout`; the
two Treatment rows show `pbm_rule_id` 1 and 2, `pbm_percent_of_revenue` 50 and 60, and
a `payout` below their `pbm_original_payout` — which is precisely the comparison
described above, just with real numbers behind it.

## What you'll see on the call itself

Beyond the exported `pbm_*` columns, a call whose payout Payout Bid Modification
actually changed also gets a record of it directly in the call's own flow log — the
same call flow view you'd already open to see how the rest of the deal on that call
played out.

Retreaver writes it as a single success-level entry:

> Payout Bid Modification applied - tagged `<payout_tag>` with `<payout_pct>`,
> changing payout from `<original payout>` to `<new payout>`.

![call log for payout bid modification](/media/b6/b6d80b4d17b8ac8383e90aa102862fa032251d437f0d1032a4e123c6c0c50478.png)

![payout bid modification call screenshot](/media/4e/4ee64fa840834137d389bb74fa52fea2bf1f3a554c924b90ebfc7307b96f1fa0.png)

This entry only shows up when a rule actually changed the payout. A Control ping, or a
Treatment ping that matched a `current` (abstention) rule, leaves no such entry at
all — so its presence or absence on a given call is a fast, per-call way to confirm
whether Payout Bid Modification touched it, without needing to cross-reference your
export first.
