Rate vs volume attribution: one model line, one month, a $54.50 increase. The two-snapshot decomposition most cost reviews start from calls $40.80 of that disputed, 74.8% of the delta, the rate effect landing anywhere between $1.70 and $42.50 depending on an ordering nobody observed. The dated rows in the same export say $0.00 disputed, rate effect $1.70.
Both numbers are exact. Nothing about the bill changed between them. What changed is the bucket width I asked the question at.
That $0.00 is this export, where the volume in the bucket that repriced happens to sit flat. Give that bucket a wobble and the disputed money comes back as a residual, and the size of the residual is set by the volume the line was running when the price moved, not by the width. I measured that too. It is the part of this post I would read if I only read one part.
AI disclosure. I wrote
resolution_gate.pywith AI assistance and ran it myself before publishing: offline, standard library only, no network, no keys, no funds. It reads your export and writes nothing outside its own directory: its fixtures and output files land there, and--selftestand--mutantsput their deliberately broken inputs in a throwawaymkdtempsandbox that goes away with the run. Every output block below is pasted from a real run on Python 3.13.5. The runner executes 17 scenarios, each one three times, and byte-compares the three copies: it reported17 deterministic, 0 notand exited 0. Code sha256:resolution_gate.pye6b00f2a…d7e2,resolution_limits.py9f1ed6f5…0f50,resolution_counter.py73f25fd3…f256,make_fixtures.py32b715f4…f201,snippet.pya7a688c7…adbd. STDOUT sha256 of the blocks I lean on hardest:out_gate_separated.txtb74b3ca5…1901,out_gate_same_deploy.txt9b6dbc08…17b0,out_gate_spike.txt37342c3a…43f9,out_limits.txtecc540f9…f717,out_counter.txt058e8bee…2128,out_selftest.txtb2ea8600…0eff,out_mutants.txt40d5026e…0a9e. Where a block is trimmed,[...]marks the lines I left out of that same run. The rate levels in the fixtures are plausible public list-price levels used as levels to sweep, not a measurement of anyone's billing; no vendor is named and none is implied. I mark which numbers are exhaustive enumerations of a declared grid, which are draws from a declared range, and which are identities that could not have come out otherwise. A leading~in any output line means the printed decimal is truncated for display; the arithmetic underneath is exact rationals or trapped Decimals. This is the third draft in this family of topics and the second attempt at this tool. Neither of the two before it was ever published; both were killed in my own pre-publication review. The one this tool came out of died over five defects, three of which were in the code, and the review line I have kept since is that the conclusions were wider, and in places different, than what had actually been measured. Two claims that draft made die further down this page. A later round of review, after this page was already written, killed a third: a characterisation I had wired into the tool's own printed output. What replaced it is narrower, and the section on the band now says which axis was never swept. Two runs in the limits section are two-row exports I ran by hand, outside the 17 in the runner, and they are labelled where they appear.
In short:
- How much of a bill delta is genuinely order-dependent is a property of the bucket width you queried at and of which delta you are decomposing. It is not a property of the arithmetic and not a property of your bill.
- Same journey, four widths, three regimes. When the reprice and the traffic ramp landed in different buckets, the disputed share is
~74.8623%at one month and exactly$0.00at a day, an hour and a minute. That$0.00is a construction in which the repricing bucket runs an exactly flat volume; the next bullet prices what happens when it does not. When one deploy moved both factors, it is~74.8623%at every width including the minute. - So the first remedy is a finer query, not an extra billing period. It does not hand you
$0.00though. What survives the split is the volume wobble inside the bucket that repriced, times the rate step, and that residual is~0.395%of the two-snapshot band when the reprice lands on a line running 2 Mtok and~9.881%when the same reprice, at the same wobble in percent, lands on the same line running 50 Mtok. At a 30% wobble on the busy line it is~47.169%. Twenty five times the dollars for the same percentage of wobble in that construction, because the repricing bucket was running twenty five times the volume. All of this is the bucket-over-bucket question only. - The correction that cost me the most: for the invoice-over-invoice question, in the one construction I built, a flat June against a July that steps once, a finer query made the disputed share bigger, from
~70.5%to~72.7%of a $616.50 delta, and the monthly band does not even contain the daily answer. The daily rate-first number$35.70sits$~13.7387below the bottom of the monthly band. I have not swept the constructions, so what transfers is that a finer query is not automatically the remedy for the invoice question, not that it always makes it worse. Two questions, two answers. I had been treating them as one. - The Laspeyres to Paasche band is not a bound. One transient spike during a reprice puts the honest chained answer at
+$85.00against a band of+$1.70 .. +$42.50, wrong by$42.50, which is77.9%of the delta, with no convention changed and exit 0 on the run. - Every path in the containment sweeps moves the rate exactly once, and on that family the escape needs a non-monotone volume path: on the 640 monotone three-step paths of the declared grid the answer left the band 0 times, on the 1040 overshooting paths it left 1040 times. What a file whose rate moves more than once does is not characterised anywhere here and I am not claiming it. I also have no production data on how often real traffic overshoots both endpoints during a reprice, and I am not inventing a percentage for that either.
- Two bugs from the draft I killed before this one are fixed here, both with a regression test that can actually fail: the portfolio band summed per line (worst overstatement
$5841.00on a pair whose executable band is$0.00, and 244 of 27261 pairs where the bug changed the exit code), and a third fail-closed layer that was the identitydelta == delta. - Drift leaks into the band, so a bucket where only one factor moved to the last decimal is a property of my fixture rather than something I would expect from live traffic. Priced:
$0.17at a drift of 0.2 Mtok per bucket, which is~0.371%of the two-snapshot band, rising to~1.298%at 1 Mtok per bucket. That is what--toleranceexists for.
What does two-snapshot rate vs volume attribution actually dispute?
Here is the smallest version. One model line. In the first bucket you paid $0.25 per million tokens on 2 Mtok: $0.50. In the last bucket the line reads $1.10 on 50 Mtok: $55.00. The rate went up 4.4x, the volume 25x, the bill by $54.50.
The rate effect has two exact values and they are 25 times apart. Move the rate at the old volume and it is (1.10 - 0.25) * 2 = $1.70, which leaves $52.80 of the increase to volume. Move it at the new volume and it is (1.10 - 0.25) * 50 = $42.50, which blames the price. The gap between them is the interaction term, (r1-r0)*(q1-q0) = $40.80, and the gate prints it as 74.8% of that $54.50.
Those two ends are the two classical index conventions, named after Étienne Laspeyres and Hermann Paasche, who were arguing about price indices in the nineteenth century, a long time before anyone billed per million tokens. The argument has not changed shape. What has changed is that our exports are timestamped.
Where did the $40.80 go at daily width?
Nowhere. It was never disputed. Here is the same journey as a 30-row daily export: the reprice lands on the 10th, the traffic ramp on the 20th.
$ python3 make_fixtures.py
make_fixtures.py
sep_daily.csv 30 rows total $631.50
same_deploy.csv 30 rows total $887.00
spike.csv 30 rows total $1284.50
drift.csv 31 rows total $4124.55
opp_sign.csv 4 rows total $50.60
one_bucket.csv 1 rows total $55.00
sep_daily.json 30 rows same data, JSON
$ head -2 fixtures/sep_daily.csv
date,line,rate_usd_per_mtok,volume_mtok,cost_usd
2026-07-01,worker,0.25,2,0.50
Run the gate on it:
$ python3 resolution_gate.py fixtures/sep_daily.csv
resolution_gate 1.0 offline, stdlib only, exact Decimal (Inexact trapped)
input : fixtures/sep_daily.csv 30 buckets, 1 line(s), 30 rows
buckets : 2026-07-01 .. 2026-07-30 (bucket width is whatever your export used)
WHAT IS BEING DECOMPOSED
first bucket 2026-07-01 bill $0.50
last bucket 2026-07-30 bill $55.00
delta +$54.50
SCOPE: all 30 buckets together add up to $631.50, which is NOT what
the numbers below decompose. Re-querying at a finer bucket width does not
refine the same answer, it changes the question from invoice-over-invoice to
bucket-over-bucket. Quote them at the width you ran them at.
ENDPOINT VIEW first bucket against last, nothing in between
worker BOTH_MOVED $0.25 -> $1.10 2 -> 50 Mtok rate-effect +$1.70 .. +$42.50
portfolio rate effect, one consistent order: +$1.70 .. +$42.50 band $40.80
CHAINED VIEW every bucket step in the file
2026-07-09 -> 2026-07-10 worker RATE_ONLY rate-effect +$1.70 .. +$1.70 step band $0.00
2026-07-19 -> 2026-07-20 worker VOLUME_ONLY rate-effect +$0.00 .. +$0.00 step band $0.00
2 step-line pairs moved, of which 0 moved BOTH factors inside one bucket
chained rate effect : +$1.70 .. +$1.70 band $0.00
chained volume effect: +$52.80 .. +$52.80 band $0.00
additivity : rate + volume + line == delta, both orderings, chained and endpoint: exact
cost cross-check: rate x volume == cost_usd on every row
CONTAINMENT does the two-snapshot band contain the chained answer?
yes. chained +$1.70 .. +$1.70 sits inside +$1.70 .. +$42.50
VERDICT: RESOLVED AT THE BUCKET WIDTH YOU ALREADY HAVE
rate effect +$1.70, volume effect +$52.80.
Every bucket step moved at most one factor, so every ordering gives this
same split. There is no interaction term to argue about.
The two-snapshot view of this same file calls $40.80 of it disputed.
That difference, $40.80, is a property of the bucket width,
not of your bill.
Nothing was waited for and no policy was applied. The rows were already
in the export.
$ echo $?
0
Two steps moved, each moved exactly one factor, so there is no ordering to assume. The SCOPE block exists because I got this wrong in the first version of the tool and I want the reader warned in the output rather than in a footnote: those 30 buckets total $631.50, and the $54.50 being split is the first bucket against the last, not the month. Re-querying finer does not sharpen the same answer. It asks a different question. I will come back to that with a number, because it is the part that hurt.
The same file as JSON produces output that differs in exactly one line, the input path. I diffed the two runs rather than asserting it.
When does a finer bucket buy you nothing?
When both factors moved in one deploy. Here is same_deploy.csv: identical endpoints, identical delta, but the reprice and the ramp both land on the 15th.
$ python3 resolution_gate.py fixtures/same_deploy.csv
[...]
CHAINED VIEW every bucket step in the file
2026-07-14 -> 2026-07-15 worker BOTH_MOVED rate-effect +$1.70 .. +$42.50 step band $40.80
1 step-line pairs moved, of which 1 moved BOTH factors inside one bucket
chained rate effect : +$1.70 .. +$42.50 band $40.80
[...]
steps where both factors moved inside one bucket, in full:
2026-07-14 -> 2026-07-15 worker: rate $0.25 -> $1.10, volume 2 -> 50 Mtok, delta +$54.50
rate first (old volume): +$1.70 volume first (new volume): +$42.50 interaction +$40.80 = 74.8% of the step delta
VERDICT: IRREDUCIBLE AT THIS BUCKET WIDTH
chained band $40.80 exceeds the tolerance you declared (+$0.00).
1 step-line pair(s) moved rate AND volume inside a single bucket,
so no ordering is observed and the split is a choice.
WHAT TO DO, IN THIS ORDER
1. Re-export at a finer bucket width. If the two moves landed on different
timestamps, what is left of this band is the volume wobble inside the
bucket where the rate moved, times the rate step. That is $0.00 only if
that bucket's volume was exactly flat, so price the residual before you
call it done. Read the SCOPE note above before you quote the new number.
2. If the finest width you can get still shows both factors moving in one
bucket, they shipped together. Waiting a billing period does NOT shrink
this band: it is invariant to the width when the moves are simultaneous.
Split the DEPLOY, not the period.
[...]
$ echo $?
1
Two files, byte-identical endpoints, opposite verdicts. That is the whole argument in two runs.
The invariance in step 2 is not rhetoric. resolution_limits.py takes the same journey, fixes its span at 60 days, and cuts it into buckets of a month, a day, an hour and a minute, under three regimes.
$ python3 resolution_limits.py
PART 1: the same journey, four bucket widths, three regimes
[...]
bucket N delta/bucket separated simultaneous co-drift
1 month 2 $54.5000 $40.80000000 ~74.8623%* $40.80000000 ~74.8623% $40.80000000 ~74.8623%
1 day 60 $~1.8166 $0.00000000 0.0000% $1.36000000 ~74.8623% $~0.02305084 ~1.2688%
1 hour 1440 $~0.0756 $0.00000000 0.0000% $~0.05666666 ~74.8623% $~0.00003937 ~0.0520%
1 minute 86400 $~0.0012 $0.00000000 0.0000% $~0.00094444 ~74.8623% $~0.00000001 ~0.0008%
* DEGENERATE: at N=2 the journey has exactly ONE step, so the two
moves cannot land in different buckets and the separated construction
collapses into the simultaneous one. That is the whole problem in one
cell: two snapshots cannot tell you which regime you are in.
[...]
Read the percentage column and ignore the dollars: dollars per bucket shrink with the bucket by construction, because a minute of traffic costs less than a month of it. The share is what matters, and it does three different things. Separated is exactly zero at every width above the degenerate one, and that is true by construction rather than by luck: put the two moves in different buckets and every step moves one factor. Simultaneous sits at ~74.8623% at every width, minute included. Co-drift falls as 1/(N-1), which is also arithmetic rather than a discovery: N-1 steps each carry an interaction of 1/(N-1) squared of the whole. The measured content of the table is not the zeros and not the decay law. It is that the three regimes are indistinguishable at N=2 and separate completely below it.
The starred cell is the honest centre of this. At two snapshots the separated construction is not merely wide, it is indistinguishable from the simultaneous one. Two aggregates cannot tell you which world you are in, and that is why the disputed number looks like a fact about billing.
I also asked whether the chained answer is just another convention, since I am replacing one number with another. It is not, but the answer is narrower than I wanted. Re-bucketing the same 30 days of the spike fixture at three widths:
$ python3 resolution_counter.py
[...]
ATTACK B: 'the chained answer is just another convention, so you have
replaced one arbitrary number with another'
[...]
1-day buckets, 30 of them: chained rate effect $85.00 delta $ 54.5000 two-snapshot band $ 40.8000 answer inside it: NO
5-day buckets, 6 of them: chained rate effect band $32.64 delta $ 272.5000 two-snapshot band $ 204.0000 ambiguous
15-day buckets, 2 of them: chained rate effect band $~236.53 delta $ 805.5000 two-snapshot band $~236.5365 ambiguous
[...]
VERDICT: NARROWED, computed from the three rows: unique at 1 of 3 widths.
A band at the finest width would print KILLED, a $0.00 at all three
would print SURVIVES. Every number has to carry its bucket width, and
claim (3) is 'query at the width where each step moves one factor',
not 'query as fine as possible'.
So every number in this post carries its bucket width, and the rule is not "query as fine as possible". It is "query at the width where each step moves one factor". At 15-day buckets a coarser bucket genuinely does contain two events and the answer is ambiguous again.
Why is the Laspeyres to Paasche band not a bound?
Because a rollout is a path, and those two ends are only two of the paths through the same two endpoints. Take a path whose rate moves exactly once, in a bucket running volume w. The rate effect is then (r1-r0)*w, which lands inside the endpoint band while w lies between q0 and q1, and outside it when w does not. That family is where the containment sweeps stop. A path whose rate moves more than once prices each of its steps at a different volume, and I have not characterised those at all. Inside the family: let traffic spike above both endpoint volumes during the reprice and the honest answer is outside the band the endpoints print.
This is the part I had backwards in the draft I killed, when I called the band "both of the answers you could have earned". Twenty eight lines, no dependencies:
"""The whole point in one screen: the two-snapshot band is not a bound.
Same two endpoints. Two paths between them. One of the answers is not in the
band the endpoints print, and the difference is not a rounding artifact.
"""
from decimal import ROUND_DOWN
from decimal import Decimal as D
r0, q0 = D("0.25"), D("2") # first bucket: USD per Mtok, Mtok in the bucket
r1, q1 = D("1.10"), D("50") # last bucket
delta = r1 * q1 - r0 * q0
band_lo = (r1 - r0) * q0 # Laspeyres: the rate moves at the OLD volume
band_hi = (r1 - r0) * q1 # Paasche: the rate moves at the NEW volume
# What the dated rows actually said: traffic spiked to 100 before the reprice
# landed, then settled at 50. Three steps, each moving exactly one factor, so
# each step's rate effect is not a matter of convention.
path = [(r0, q0), (r0, D("100")), (r1, D("100")), (r1, q1)]
chained = sum((b[0] - a[0]) * a[1] for a, b in zip(path, path[1:]))
print(f"delta ${delta}")
print(f"two-snapshot band for the rate effect: ${band_lo} .. ${band_hi}")
print(f"chained over the dated rows : ${chained}")
print(f"inside the band: {band_lo <= chained <= band_hi}")
share = ((chained - band_hi) / delta * 100).quantize(D("0.1"), rounding=ROUND_DOWN)
print(f"outside by ${chained - band_hi}, which is {share}% of the delta")
$ python3 snippet.py
delta $54.50
two-snapshot band for the rate effect: $1.70 .. $42.50
chained over the dated rows : $85.00
inside the band: False
outside by $42.50, which is 77.9% of the delta
The full gate says the same thing about the 30-row version of that path, and, this is the uncomfortable bit, says it while returning exit 0:
$ python3 resolution_gate.py fixtures/spike.csv
[...]
CHAINED VIEW every bucket step in the file
2026-07-09 -> 2026-07-10 worker VOLUME_ONLY rate-effect +$0.00 .. +$0.00 step band $0.00
2026-07-14 -> 2026-07-15 worker RATE_ONLY rate-effect +$85.00 .. +$85.00 step band $0.00
2026-07-19 -> 2026-07-20 worker VOLUME_ONLY rate-effect +$0.00 .. +$0.00 step band $0.00
3 step-line pairs moved, of which 0 moved BOTH factors inside one bucket
chained rate effect : +$85.00 .. +$85.00 band $0.00
chained volume effect: -$30.50 .. -$30.50 band $0.00
[...]
CONTAINMENT does the two-snapshot band contain the chained answer?
NO. chained +$85.00 .. +$85.00 falls outside +$1.70 .. +$42.50
FLAG: ENDPOINT_BAND_EXCLUDES_RESOLVED miss $42.50 = 77.9% of the delta
The endpoint band prices the whole rate change at one of the two endpoint
volumes and at nothing else. The chained answer prices every rate step at
the volume in the bucket where that step happened, so it can land outside.
The band is one pair of paths out of many, not a range that contains the
answer.
VERDICT: RESOLVED AT THE BUCKET WIDTH YOU ALREADY HAVE
rate effect +$85.00, volume effect -$30.50.
Every bucket step moved at most one factor, so every ordering gives this
same split. There is no interaction term to argue about.
The two-snapshot view of this same file is not merely wider here. Its band
$40.80 wide does not contain the answer above at all, so quoting either
end of it would have been wrong by at least $42.50.
[...]
$ echo $?
0
The rate effect is $85.00 and the volume effect is negative $30.50, on a bill that went up. That is not a paradox, it is what happens when the reprice catches a traffic peak and the traffic then settles below it. Anyone quoting either end of the two-snapshot band here would be wrong by at least $42.50 on a $54.50 delta.
How far outside can it go? Linear in the peak, (r1-r0)*(w-q1), with no bound inside the two endpoint snapshots:
PART 4: when the two-snapshot band stops being a bound
[...]
endpoints $0.25 x 2M -> $1.10 x 50M, delta $54.50, band $1.70..$42.50
peak volume while repricing rate effect outside by % of delta
50 Mtok (1x the new level) $42.50 $0.00 0.0%
100 Mtok (2x the new level) $85.00 $42.50 ~77.9%
250 Mtok (5x the new level) $212.50 $170.00 ~311.9%
500 Mtok (10x the new level) $425.00 $382.50 ~701.8%
1000 Mtok (20x the new level) $850.00 $807.50 ~1481.6%
5000 Mtok (100x the new level) $4250.00 $4207.50 ~7720.1%
[...]
Now the two attacks that matter on this claim, because a characterisation that only holds on my tidy grid is worth nothing.
Is it a lattice artifact? No. 200000 draws from a fixed-seed 64-bit LCG written out in the file, rates from 0.0001 to 10.0000 in ten-thousandths, volumes and the mid-path peak from 0.001 to 5000.000 in thousandths, so nothing sits on a hand-built diagonal:
ATTACK C: 'claim (2) is a lattice artifact of your tidy grid, like the
28-of-212 flips were'
[...]
usable draws : 200000 (dropped 0 with a factor that did not move)
chained answer outside the band : 133575 (~66.78% +/- 0.1053 pp, 1 SE)
implementation check, draw by draw: 200000 of 200000 (100.00%)
miss as a multiple of the band : median ~0.99x, 90th pct ~9.03x, max ~182254.44x
The share in row two is a property of how wide I let the peak roam and
is not a frequency in anyone's traffic: I report it only to show it is
not zero and not confined to a lattice. Row three is an IDENTITY and
not a finding: is_out and predicted_out are the same predicate, one of
them divided by (r1-r0), so agreement is the only thing it can print.
It checks this implementation, the way the 400-of-400 line in
--selftest does, and it says nothing about the world. The row that
carries weight is row four, the size of the miss, which nothing forces.
The draws are off a lattice but they are not continuous: rates land on
a grid of ten-thousandths, volumes and the peak on a grid of
thousandths. Row four is measured against the band and not against the
delta on purpose: the delta goes through zero inside this draw range
and a ratio to it would be noise.
VERDICT: SURVIVES, computed from row two: 133575 of 200000 off-lattice
draws put the answer outside the band, and a zero there would have
printed KILLED. Claim (2) does not depend on the spacing of any grid.
Read row four, and not row two or row three. The ~66.78% is a property of the range I let the peak roam over, and it would be dishonest to hand it to you as "two thirds of reprices". Row three is worse than that: is_out and predicted_out are the same predicate with one of them divided by (r1-r0), so 200000 of 200000 is the only thing that line can print. It checks this implementation, the way the 400 of 400 line in --selftest does, and it says nothing about the world. I had it in the post as the load-bearing number, which was the same mistake in a new suit. The draws are also off a lattice rather than continuous: rates land on ten-thousandths, volumes and the peak on thousandths. The tool calls row four the one that carries weight. Reviewing this page I checked that too, and it does not: across all 133575 draws that landed outside, the miss over the band is exactly the distance from the peak to the endpoint interval over the gap between the endpoints, and (r1-r0) cancels out of it completely. Row four is a distribution of three uniform volume draws over a range I declared, and it says nothing about rates or reprices either. The worst case, ~182254.44x, comes from two endpoint volumes 0.009 Mtok apart, where the band itself is $0.0196 wide, which is the same near-zero denominator I said I was avoiding by not dividing by the delta. So all four rows of ATTACK C check my implementation and my sampling. The claim they were supposed to support is carried by the theorem and by the spike run, not by them.
Did I cherry-pick a spike? Yes, and the attack lands:
ATTACK D: 'you cherry-picked a spike. On a normal path the band holds'
[...]
monotone paths (peak inside the endpoints): 640, outside the band: 0
overshooting paths : 1040, outside the band: 1040
[...]
VERDICT: NARROWED, computed from the two counts: 0 escapes on
monotone paths, 1040 on overshooting ones. Claim (2) applies to
non-monotone volume paths, which includes every transient spike,
migration ramp-and-settle and failed rollback. Every path swept here
moves the rate EXACTLY ONCE. What happens when the rate moves more
than once is not characterised by this sweep and is not claimed.
On a plain ramp the band held on all 640 of them, because the rate effect is then a convex combination of the two ends. So inside this family, where the rate moves once, the failure mode needs a non-monotone volume path: a transient spike, a migration that ramps and settles, a rollback that did not hold. What a file whose rate moves several times does is not swept by any of those three, and I am not going to guess at it. The tool's own selftest checks the same statement on a slightly different waypoint list and reports 640 inside, 800 outside, 0 disagreements with the theorem.
Here is what I cannot tell you: how often real traffic overshoots both endpoint volumes during a reprice. I have no production data on that and I am not putting a percentage on it. What I can do is price the miss when it happens, and hand you a flag that fires on your own export.
Does the invoice question get better too? In this construction it got worse
This is the correction that mattered most, and it kills a sentence I would have written happily while drafting this: "look finer and the ambiguity goes away".
Finance rarely asks bucket-over-bucket. It asks invoice-over-invoice: this month against last month. Take the same July as sep_daily.csv, put a flat June next to it, and use the finer data the way a same-day-last-month comparison does, by pairing July day i against June day i.
PART 2: the invoice question moves the OTHER way
[...]
June : bill $15.00 volume 60 Mtok blended rate $0.2500/Mtok
July : bill $631.50 volume 588 Mtok blended rate $~1.0739/Mtok
delta $616.50
The two prices actually charged in July were $0.25 and $1.10. The blended
$~1.0739 is neither, and it is the number a monthly aggregate hands the decomposition.
MONTHLY AGGREGATE, two snapshots:
rate effect $~49.4387 .. $484.5000 band $~435.0612 = ~70.5% of the delta
DAILY PAIRS, same-day-last-month, 30 pairs (9 flat, 10 single-factor, 11 both-moved):
rate effect $35.7000 .. $484.5000 band $448.8000 = ~72.7% of the delta
The finer view is $~13.7387 WIDER, not narrower (~70.5% -> ~72.7%).
And the monthly band $~49.4387..$484.5000 does not contain the daily one:
the daily rate-first answer $35.7000 sits $~13.7387 below the bottom of it.
Why: 11 of the 30 day-pairs moved BOTH factors, because a step change
persists into every later day. Slicing separates EVENTS in time; it
does not separate a state that is different for the rest of the month.
[...]
Eleven of thirty day-pairs moved both factors, because a step change persists into every later day. Slicing separates events in time. It does not separate a state that stays different for the rest of the month. So in this construction the finer query makes the band wider, and the monthly band does not even contain the finer answer, which means an extra billing period is not the remedy here either. One construction is one construction: I built a flat June against a July that steps once, and I did not sweep the space of Junes and Julys. Reviewing this page, I was handed a counter-construction where the finer cut goes the other way, and it is easy to build. What survives is the weaker and more useful statement, that a finer query is not automatically the remedy for the invoice question.
Two questions, two different answers, and I had been treating them as one. If you take one operational thing from this post, take that: before you quote a rate effect, say out loud which delta you are decomposing. The SCOPE block prints in every run for exactly that reason.
Note also the blended $~1.0739 per Mtok. July charged $0.25 and $1.10, and never $1.0739. A monthly aggregate hands the decomposition a price nobody was billed, which is a second, quieter reason the invoice question is harder than it looks.
Two bugs the draft of this tool carried
Both were found by my own review, after the draft they were in had been written, and both are the same shape: a number that looks like a measurement and is actually an artifact of how I computed it, which is exactly what a 58% win-rate over zero closed trades was.
Bug one: the portfolio band was not any rollout's range. The old code built the portfolio band by summing per-line minima and maxima, which quietly lets every line choose its own rollout order. A real rollout applies one order to everything. So the portfolio answer takes two values, sum-of-Laspeyres and sum-of-Paasche, and the band is the absolute value of the sum of interactions, not the sum of the absolute interactions. With interactions of opposite sign those are wildly different:
PART 3: a portfolio band summed per line is not any rollout's range
[...]
both-moved transitions on the 5x4 grid: 234
two-line portfolios (unordered pairs) : 27261
where the per-line sum OVERSTATES the band: 13680 (~50.1%)
where it is exactly right (same sign) : 13581 (~49.8%)
where the executable band is $0.00 and the old code printed a non-zero one
and exited 1 on it : 244 (~0.8%)
worst overstatement: $5841.00
line 1 $0.25 x 2M -> $15.00 x 200M interaction $2920.50
line 2 $0.25 x 200M -> $15.00 x 2M interaction $-2920.50
executable band $0.00, per-line sum $5841.00
FORCED BY CONSTRUCTION, and I am not reporting the ~50.1% as a frequency in
anyone's bill: this grid is symmetric in direction, so close to half of all
unordered pairs must have interactions of opposite sign. The number that is
not forced is the SIZE of the overstatement and the fact that it changes the
exit code, which is what a CI job acts on.
The ~50.1% is forced by the construction and I am labelling it rather than quoting it: the grid is symmetric in direction, so roughly half of all unordered pairs must have opposite-sign interactions. What is not forced is the size, $5841.00 at worst, and the 244 pairs where the old code printed a non-zero band and exited 1 on a portfolio whose answer is a single number. An exit code is what a CI job acts on, so that 244 is the part that would have woken somebody up for nothing.
The fix, and the reason my old regression test could never have caught it:
def totals(units, line_bucket):
"""Aggregate under the two orderings a single rollout can actually execute.
A portfolio-wide answer comes from ONE order applied to everything: either
every rate move lands at the old volume, or every one lands at the new
volume. The band is therefore |sum of interactions|, not the sum of the
absolute interactions. The draft of this tool summed per-unit min and max
independently, which lets every line pick its own order. Two lines with
interactions +$0.60 and -$0.60 came out as a $1.20 band and exit 1, when the
portfolio answer is a single number and no rollout on earth produces $1.20.
free_width below is that older number, kept and labelled for what it is.
"""
The old test used two identical lines. Identical lines have interactions of the same sign, and with the same sign the buggy sum and the correct one are equal, so the test passed on a tool that was wrong. The new fixture uses opposite signs on purpose:
$ python3 resolution_gate.py fixtures/opp_sign.csv
[...]
ENDPOINT VIEW first bucket against last, nothing in between
up BOTH_MOVED $1.00 -> $1.20 10 -> 13 Mtok rate-effect +$2.00 .. +$2.60
down BOTH_MOVED $1.00 -> $1.20 13 -> 10 Mtok rate-effect +$2.00 .. +$2.60
portfolio rate effect, one consistent order: +$4.60 .. +$4.60 band $0.00
per-line free order would print $1.20: that is an upper bound on
a rollout where each line takes a different order, not a range of one answer.
[...]
VERDICT: RESOLVED AT THE BUCKET WIDTH YOU ALREADY HAVE
rate effect +$4.60, volume effect +$0.00.
2 step-line pair(s) moved both factors, and their interactions cancel:
the PORTFOLIO answer is a single number under either consistent order. Each of
those lines on its own is still order-dependent, so do not quote them one at a
time out of this run.
$ echo $?
0
The old number is still printed, labelled as an upper bound for the case where each line really does take a different order. That is a legitimate quantity. It was just never a range of one answer.
Bug two: one of my three fail-closed layers was a tautology. I claimed three independent layers and one of them checked rate + volume + line == delta where the volume term had been defined as delta - line - rate. That expands to delta == delta. It could only ever catch a non-finite value, which the first two layers already catch.
The fix is that every term is now multiplied out on its own, and the delta it is compared against is recomputed from the raw bucket bills:
d["delta"] = r1 * q1 - r0 * q0
# Each of the four numbers below is its own multiplication, not derived by
# subtracting another effect from the delta. That is what makes the
# additivity check in report() a check and not a tautology.
d["rate_rf"] = (r1 - r0) * q0 # rate first (Laspeyres rate effect)
d["vol_rf"] = r1 * (q1 - q0) # volume after the rate moved
d["rate_vf"] = (r1 - r0) * q1 # volume first (Paasche rate effect)
d["vol_vf"] = r0 * (q1 - q0) # volume before the rate moved
d["inter"] = (r1 - r0) * (q1 - q0)
And a layer is only worth naming if it catches something the others let through, so --mutants breaks one thing at a time, including two internals that the old tautological layer waved past:
$ python3 resolution_gate.py --mutants
resolution_gate 1.0 --mutants
Each row is one deliberately broken input or one deliberately
broken internal, and the layer that has to catch it. A layer that
only ever fires when another layer already fired is decoration.
layer 0 unbroken input -> exit 0 (baseline, must be 0)
layer 1 date 10/07/2026 instead of ISO -> exit 2 m1.csv row 2: date='10/07/2026' is not ISO YYYY-MM-DD or YYY
layer 2 71-digit rate, no NaN, valid pattern -> exit 2 Inexact
layer 4 cost_usd 54.99 vs rate x volume 55 -> exit 2 m4.csv row 3: rate 1.10 x volume 50 = $55.00 but cost_usd=$5
layer 3 internal: chain drops its last step -> exit 2 internal: additivity broken. chained rate-first +$1.70 + vol
layer 3 internal: rate effect doubled -> exit 2 internal: additivity broken. chained rate-first +$3.40 + vol
The draft version of layer 3 was the identity delta == delta and
printed a verdict for both of these internals.
five broken inputs, exits [2, 2, 2, 2, 2], distinct layers responsible: 1, 2, 3, 4, 3
I am not proud of either bug. I am mildly proud that the fixture for the first one is now in the selftest with opposite signs, which is the version that can fail.
Is a single-factor bucket ever real, though?
Fair question, and it is the strongest attack on the $0.00 above, because in my separated fixture the volume sits exactly flat while the rate moves, and exactly flat is a constant of the fixture rather than a fact about traffic. So I added a drift to every bucket, so that no step has an unchanged volume:
ATTACK E: 'the $0.00 in the separated regime is a constant of your
fixture: real traffic is never flat, so no bucket is ever
genuinely single-factor'
Same separated sequence, with a volume drift added to EVERY bucket so
that no step has an exactly unchanged volume.
drift per bucket chained band two-snapshot band chained / two-snapshot
0.000 Mtok $0.0000 $40.8000 0.000%
0.001 Mtok $~0.0008 $~40.8246 ~0.002%
0.010 Mtok $0.0085 $41.0465 ~0.020%
0.050 Mtok $0.0425 $42.0325 ~0.101%
0.200 Mtok $0.1700 $45.7300 ~0.371%
1.000 Mtok $0.8500 $65.4500 ~1.298%
Drift leaks into the band only through the ONE bucket where the rate
moved, so the leak is that bucket's volume change times the rate step,
$0.85. That much is an identity. At a drift of 0.2 Mtok per bucket the
band is $0.17, which is 0.371% of the two-snapshot band, and at 1 Mtok
per bucket, a 50% swing on a line running 2 Mtok, it reaches 1.298%.
Not zero. What --tolerance exists for.
The table above holds a second constant of the fixture, and it is the
one that moves the answer: WHERE the reprice landed. Above, the rate
moves on bucket 10 while the line still runs 2 Mtok, and the ramp
comes after it. Put the ramp first and the same reprice lands on a
line already running 50 Mtok. Sweep both orders, with the wobble read
as a percentage of the level the line is running rather than as an
absolute: a deterministic zigzag, level*(1+w) on odd buckets and
level*(1-w) on even ones, so no step has an unchanged volume and
nothing is drawn at random.
wobble reprice lands on chained band two-snapshot band chained / two-snapshot
5% 2 Mtok $0.1700 $43.0100 ~0.395%
5% 50 Mtok $4.2500 $43.0100 ~9.881%
10% 2 Mtok $0.3400 $45.2200 ~0.751%
10% 50 Mtok $8.5000 $45.2200 ~18.796%
20% 2 Mtok $0.6800 $49.6400 ~1.369%
20% 50 Mtok $17.0000 $49.6400 ~34.246%
30% 2 Mtok $1.0200 $54.0600 ~1.886%
30% 50 Mtok $25.5000 $54.0600 ~47.169%
Same percentage wobble, same two events, same width: 25 times the
dollars, because the bucket that repriced was running 25 times the
volume. So the separated regime does not hand you $0.00. It hands you
a residual whose size is set by the volume in the bucket where the
rate moved, and that is a number you have to price on your own export.
VERDICT: NARROWED, computed from the largest share above, ~47.16%. A residual
that reached the two-snapshot band would print KILLED; a residual that
stayed at exactly $0.00 through every row would print SURVIVES.
The first table is the one I had. It is honest and it is not the interesting one, because it holds a second constant fixed: where the reprice landed. In the separated fixture the price moves on the 10th, while the line is still running 2 Mtok, and the ramp arrives ten days later. Put the ramp first and the identical reprice lands on a line already running 50 Mtok. Same two events, same width, same wobble in percent, and the residual goes from ~0.395% of the two-snapshot band to ~9.881%. Twenty five times the dollars in that construction, because the repricing bucket was running twenty five times the volume. What the ratio is on your export depends on how far apart your two volume levels are, and I have swept two levels, not a range of them.
So "re-export finer and the band goes to $0.00" was a promise the tool had no business making, and it was printing it in the WHAT TO DO block. It now says what is actually left: the wobble in the repricing bucket times the rate step, which is $0.00 only when that bucket happened to be flat. On the busy end of my own sweep that residual reaches ~47.169% of the two-snapshot band, which is not a rounding artifact and not something you want to find out after quoting a number.
On the co-drift fixture, where both factors move a little in all 31 buckets, the gate returns exit 1 on a chained band of $0.30 against a two-snapshot band of $9.00, and it will keep returning exit 1 until you say what you are willing to live with:
$ python3 resolution_gate.py fixtures/drift.csv --tolerance 0.50
[...]
30 step-line pairs moved, of which 30 moved BOTH factors inside one bucket
chained rate effect : +$34.35 .. +$34.65 band $0.30
[...]
VERDICT: RESOLVED AT THE BUCKET WIDTH YOU ALREADY HAVE
rate effect +$34.35 .. +$34.65, band $0.30, inside the
tolerance you declared (+$0.50). You accepted the ambiguity; the tool
did not remove it.
The two-snapshot view of this same file calls $9.00 of it disputed.
That difference, $8.70, is a property of the bucket width,
not of your bill.
$ echo $?
0
That last sentence in the verdict is deliberate. A tolerance is you accepting an ambiguity, not the tool removing one. Same for --tolerance 50 on the one-deploy fixture: exit 0, and a verdict that says in as many words that you accepted a $40.80 band.
The claim I kept from the earlier draft, and just lost
In the earlier draft of this work I measured how often the named culprit flips when you flip the assumed order, swept the spacing of the grid levels, and reported that the flips survive at ordinary drift spacing: 28 of 212 at 1.02x. I had already retracted a bigger number in that draft, and I kept this one as the survivor. It does not survive.
Both ladders in that sweep were built with the same multiplier k, so the rate ratio and the volume ratio came from one identical set, which manufactures exact ties. At that spacing every flip I counted sat on a tie, and once the two ladders stop sharing a multiplier the ties stop happening: 0 of them in every independent row below. Sweep the two spacings independently:
PART 5: the claim I kept after my first self-correction, and lost
[...]
rate k volume k cases flips on an exact tie median gap min gap
1.02x 1.02x 212 28 28 (100.0%) ~0.9% ~0.9%
1.02x 1.021x 240 2 0 (0.0%) ~0.6% ~0.6%
1.02x 1.019x 240 2 0 (0.0%) ~0.3% ~0.3%
1.05x 1.05x 212 28 28 (100.0%) ~2.4% ~2.4%
1.05x 1.053x 240 10 0 (0.0%) ~2.1% ~2.1%
2x 2x 212 72 28 (~38.8%) ~33.3% ~6.6%
2x 2.1x 240 72 0 (0.0%) ~31.2% ~5.7%
[...]
candidates that differ by about 1% of the delta: nobody names a
culprit on a 1% gap. That draft's 5x4 grid is a separate matter:
108 flips in 374 changed-bill transitions, 6 of them on an exact tie,
median gap between the two candidates ~60.5% of the delta, min ~27.1%.
That one is not a lattice artifact. The 2%-spacing row was.
[...]
Move the volume multiplier by one tenth of a percentage point, which changes nothing anybody would call a different regime, and the flips go from 28 to 2. All 28 of the survivors sat on an exact tie I had built by hand, and they were deciding between two candidates about 0.9% of the delta apart. I would not name a culprit on a 0.9% gap, and I doubt you would either.
The headline count from that draft, 108 flips in 374 transitions on the 5x4 step-change grid, holds up: 6 of the 108 are ties, the median gap between the two candidates is ~60.5% of the delta, and the minimum is ~27.1%. Those are real disagreements about real money. It was the narrow, careful, drift-spacing claim that was an artifact, which is the opposite of the way I expected to be wrong.
All counts in that table are exhaustive enumerations of a declared grid, so n is the whole population at each row, no sampling is involved and no standard error applies. The only randomised probe in this project is ATTACK C, which prints its SE.
Run it on your own export
Three commands, no dependencies, nothing to configure:
$ python3 make_fixtures.py
$ python3 resolution_gate.py fixtures/sep_daily.csv ; echo $?
$ python3 resolution_gate.py your_export.csv --tolerance 25
Your export needs four columns, and a fifth if you want the cost cross-check:
date,line,rate_usd_per_mtok,volume_mtok,cost_usd
2026-07-01,worker,0.25,2,0.50
One row per bucket per line. Dates are ISO, either YYYY-MM-DD or YYYY-MM-DDTHH:MM, one form per file, so lexicographic order is chronological order. JSON works too: {"rows": [...]} or a bare list, parsed with parse_float=str so no number round-trips through binary float.
Exit codes are the contract: 0 resolved at the bucket width you have, 1 irreducible at that width, 2 bad input. Wire it as a CI job on a monthly export and it will tell you, before anybody writes a number into a slide, whether the number is a measurement or a choice. Refusals are loud rather than quiet:
$ python3 resolution_gate.py fixtures/one_bucket.csv
INPUT ERROR: fixtures/one_bucket.csv: needs at least 2 distinct dates, found 1. With one bucket there is no delta; with exactly two you are back to a two-snapshot view, which is the thing this tool exists to check.
$ echo $?
2
And the guards get counted rather than asserted:
$ python3 resolution_gate.py --selftest
resolution_gate 1.0 --selftest
input guards: 17 of 17 malformed exports refused with exit 2
additivity in both orderings, exact, on 400 of 400 ordered pairs (identity: it
cannot come out otherwise, so this checks the implementation, not the world)
opposite-sign interactions (+$0.60 and -$0.60): consistent-order band $0.00 yes, exit 0, per-line free bound $1.20 reported separately yes
containment, on paths where the rate moves once: chained rate effect inside
the endpoint band while the volume waypoint is between the endpoint volumes.
640 inside, 800 outside, 0 disagreements with the theorem
zero-volume encodings (absent / 0 tokens / 0 tokens at rate 0): exits [0, 0, 0], BOTH_MOVED anywhere False (one event, one verdict)
selftest: 5 of 5 checks passed
The 400 of 400 line is labelled an identity in the output on purpose. It cannot come out any other way, so it tests my implementation and tells you nothing about the world. Keeping identities out of the findings column is most of the discipline here.
The zero-volume line closes a hole that used to change the verdict from formatting alone. A model that was not called in a bucket shows up in real exports three ways: the row is absent, or it carries 0 tokens, or it carries 0/0 for cost over tokens. The old tool read the last one as a rate moving from 0 and a volume moving from 0, handed the whole delta to the interaction term, and returned its strongest verdict. Now all three encodings of a line that starts billing are one event with one verdict. A line that stops billing in the middle of the period and comes back is a different case: I checked it after writing this, and the three encodings there give exits 0, 1 and 0, so that half of the hole is still open.
def present(cell):
"""A line billed 0 tokens at rate 0 is not a line, it is an empty cell.
Real exports write cost/tokens = 0/0 for a model that was not called. The
two-snapshot version of this tool read that as rate 0 -> rate r and volume
0 -> volume q, i.e. BOTH factors moved, and handed the whole delta to the
interaction term. Same event, three encodings, three verdicts. So: absent.
"""
return not (cell[0] == 0 and cell[1] == 0)
What this measures, and what it does not
The fixtures are synthetic and I want that stated in the body rather than buried. The rate levels are plausible public list-price levels used as levels to sweep; they are not a measurement of anyone's billing and they support no claim about any vendor. The 5x4 grid counts and the bucket-width tables are exhaustive enumerations over populations I declared, which makes them arithmetic over grids I chose rather than statistics about the world. The one randomised probe, ATTACK C, prints its own standard error and names, in its own output, which of its rows carries weight and which one is an identity that could not have printed anything else.
Five things this tool will not do for you, in the order I expect them to bite.
It knows nothing about mix. If your rate column is cost divided by tokens, it is a blended average, and it moves when nobody repriced anything: a prompt cache segment breaks, an output:input ratio shifts, a routing weight moves between two models that both already exist. I measured a hit rate falling from 100% to 40% off one injected timestamp in the cache-break detector, and that shows up in this tool as a rate move. The blended $~1.0739 in PART 2 is the same problem in miniature. The tool says so in its own docstring and keeps going, because that is your export's problem and not arithmetic's.
It assumes your rate is not a function of your volume, and a lot of real pricing is. Tiered rates, committed-use amortisation, batch discounts and cached-input pricing all make the blended rate move because the volume moved, and then cost = rate x volume has stopped being a decomposition of two independent things. I ran a two-row export through the gate to watch it fail. Pricing with a step at 100 Mtok, everything up to the step at $3.00 per Mtok and the excess at $2.00: 100 Mtok bills $300.00 at a blended $3.000, and 160 Mtok bills $420.00 at a blended $2.625.
date,line,rate_usd_per_mtok,volume_mtok,cost_usd
2026-07-01,worker,3.000,100,300.00
2026-07-02,worker,2.625,160,420.00
The gate reads that as BOTH_MOVED, prints a rate effect of -$60.00 .. -$37.50 and a band of $22.50, returns IRREDUCIBLE AT THIS BUCKET WIDTH at exit 1, and tells you to split the deploy. There was no deploy. Nobody repriced anything and nobody shipped anything; the traffic crossed a pricing step. In this regime "both factors moved inside one bucket" does not mean "they went out together", so the instruction this tool gives most firmly is the wrong one here, and nothing in its output tells you which of the two cases you are looking at.
The mix problem has a sharper version in the same place. Hold the volume exactly flat at 100 Mtok and move only the input:output split, at $3 per Mtok in and $15 per Mtok out, from 75/25 to 50/50. The bill goes from $600.00 to $900.00 and the blended rate goes from $6.00 to $9.00.
date,line,rate_usd_per_mtok,volume_mtok,cost_usd
2026-07-01,worker,6.00,100,600.00
2026-07-02,worker,9.00,100,900.00
Now the gate prints RATE_ONLY, a band of $0.00, a rate effect of +$300.00 and exit 0. That is its strongest verdict, on a rate effect nobody was charged. Neither of those two runs is a bug in the arithmetic and neither is in the runner: they are two-row exports I ran by hand, and you can paste them into a file and reproduce both in under a minute. What they show is the arithmetic being handed a rate column that is not a price.
It cannot tell you how often the band excludes the answer in your traffic, and it does not characterise every file. The containment result covers paths whose rate moves exactly once, checked against 200000 off-lattice draws and against every endpoints-plus-waypoint combination of the declared grid: there the band bounds the answer while the bucket that repriced ran a volume between the two endpoint volumes. A file whose rate moves more than once prices each of its rate steps at a different volume, and none of the containment sweeps covered that; for those the flag in the output is the only thing that speaks. How often either case happens to you is a fact about your rollouts that I have no data on.
It rejects credits and refunds by design. Negative costs fail the input pattern loudly rather than getting quietly attributed. That means it reads a net-positive usage summary and not a full invoice, and the sign arguments in this post rest on that rejection.
It validates shape, not billing meaning. Nothing in the four layers looks at what your rows mean: a pattern, an ISO date, a duplicate key, an additivity sum and a cost product. So cumulative month-to-date rows, mixed units, or gross where you meant net will pass all of it and get a verdict. The layers stop malformed numbers, not a wrong query.
Where does that leave the practice? Roughly here. Do not decompose two aggregates when a dated export is one query away. Say which delta you are decomposing before you say the number, because in the construction I measured the invoice question and the bucket question came out with different answers, and I did not sweep the constructions. Price the residual instead of assuming it went to zero: after the finer cut, what is left is the volume wobble in the repricing bucket times the rate step, and on the busy end of my own sweep that was ~47.169% of the two-snapshot band rather than nothing. Check the containment flag before you quote a band, because a band that excludes the answer is worse than a wide one. And if the finest width you can get still shows both factors moving inside one bucket, first check whether your rate column is a blended cost over tokens, and whether your rate is a function of your volume; if it is neither, then the two moves went out together, no waiting will separate them, and the thing to split next time is the deploy rather than the period. That last part is the only place where the old advice was right, and it is a much smaller place than I gave it.
The line I keep coming back to is that attribution is not a property of a report. It is a property of how the change was rolled out and at what resolution the change was recorded. Everything I have written about gating before execution rather than logging after it says the same thing in a different register, and the spend cap that stopped counting is the same lesson with a worse ending. The counterpart to this post is the 0.35% per day drift that produced zero alerts in 60 days: slow drift makes the disputed money small and the trend invisible, while a step change makes the disputed money enormous and the trend obvious. Also worth knowing before you argue about a rate: a single agent loop re-bills its whole transcript, which I measured as a 42.8x re-bill multiplier on one debugging session, so your volume can move a long way with nobody deciding anything.
Here is the question I have not answered, and it is the one I would ask a stranger with a bigger bill than mine. When the two moves genuinely shipped in one deploy, and no resolution can separate them, is a fixed slice of traffic held at the old configuration for one bucket a real remedy or a fantasy? It turns a bill into an experiment, it needs a routing layer most stacks do not have, and the two slices have to be comparable enough that the comparison means something. If you have shipped anything like it, I want to know how you kept the slices comparable, because that is where I expect it to fall apart, and it is the only remedy left in the one regime this post cannot fix.
Follow along if you want the next measurement rather than the next opinion: this is a FinOps blog for people who run agents, and every post here comes out of a tool I ran first. And tell me the worst bill delta you have had to explain from two aggregates, especially one where you later found out the real path between them.
Top comments (0)