Room·Balance
Run a pilot

Hotel room allocation & oversold revenue

Optimal room allocation, with a certificate.

RoomBalance answers the three hard questions in room allocation exactly: the fewest rooms that cover your booking calendar, how to match every booking to a room type under your upgrade rules, and — when you're oversold — which bookings to accept to keep the most revenue. Every answer is provably optimal and carries a certificate you can check.

~6 s
Exact minimum rooms for a full 10,000,000-booking year, with an oversold certificate.
39–59%
All a shipped PMS greedy captures of the attainable revenue when you're oversold. RoomBalance returns the provable maximum.
< 1 µs
Room-type matching at 100,000,000 bookings — cost independent of how many bookings you have.

What it does

Three allocation problems, each solved to the proven optimum.

RoomBalance is an allocation engine you call over a one-line protocol. It covers the three room problems a property actually faces — and for each, it returns the provably optimal answer together with a certificate you can check.

STAYOPTIMAL · MIN ROOMS

Cover the calendar

Given every booking as an arrival–departure interval, find the fewest rooms that serve them all — one physical room reused across non-overlapping stays — and how far a fixed inventory is oversold.

  • Provably minimum rooms (the peak concurrent stays).
  • Oversold certificate against any inventory.
  • Exact by interval coloring, O(N log N).
ROOMMATCH · TYPE MATCHING

Match to room types

Bookings group into request classes by which room types can serve them — a suite can satisfy a deluxe request. RoomMatch tells you whether everyone fits one room each under your upgrade rules, and exactly where you're short.

  • Exact match on the classes × types quotient.
  • Cost independent of the booking count.
  • Per-type load so you see the shortfall.
OCCUPANCYMAX · OVERSOLD

Keep the most revenue

When demand exceeds your rooms, decide which bookings to accept so the accepted stays still fit and total revenue is as high as possible — the choice a greedy gets wrong.

  • Provable maximum-revenue accept/reject split.
  • Exact min-cost flow, verified vs a brute-force oracle.
  • Shows the revenue a greedy leaves on the table.

The certificate

A number you can put in front of an owner.

Every answer carries its own proof. RoomBalance cannot overclaim — when a fixed inventory can't cover the calendar it reports exactly how oversold you are, and it never reports a revenue figure it can't prove is the maximum.

# what each answer certifies
capabilitycertificate fieldwhat it proves
StayOptimalrooms_used = peak_overlapthe fewest rooms that can cover the calendar — the interval-coloring optimum, plus oversold_by against your inventory
RoomMatchexact=1, min_per_roomevery booking housed one-per-room under the upgrade rules, or exactly where you fall short
OccupancyMaxtotal_value ≤ offered_valuethe maximum revenue any accept/reject split can reach on your rooms — not a greedy's guess

Each figure is computed by the engine and returned in the same line as the answer — check it without re-solving.

Measured, reproducible

The numbers — scale, exactness, and recovered revenue.

Wall-clock milliseconds, single-threaded, every figure reproducible from source. Below are the engine's own scale timings, the revenue an oversold property recovers over the greedy a property-management system ships, and head-to-head races against OR-Tools CP-SAT and CBC on byte-identical instances.

Measured against OR-Tools CP-SAT and CBC, on byte-identical instances. Two of these three problems are polynomial: CP-SAT and CBC reach the same exact answer, so the win there is speed, the exactness certificate, and zero dependencies — not "only we can solve it." RoomMatch is the exception — the genuine polynomial-vs-NP cliff, where CP-SAT times out and returns a worse answer. Each race is a single run on a moderately loaded box (a competitor's time is its own model-build plus solve), so treat the ratios as order-of-magnitude, not lab-grade.

StayOptimal — exact minimum rooms, at chain scale

# minimum rooms over a 365-day horizon, 1–14 night stays · solve = wall-clock ms
bookingssolve_msrooms (= peak overlap)
10,0003.07238
100,00034.92,202
1,000,00037820,921
5,000,0002,159103,544
10,000,0005,884206,491

A full year of a ten-million-booking chain, exact minimum rooms, in about six seconds. Interval coloring is polynomial — StayOptimal is the optimal algorithm, not a heuristic that beats a weaker one. The win over an in-house naive O(N²) sweep is scale (it stops finishing around 50k); the sell is the exactness, the certificate, and the range.

# vs OR-Tools CP-SAT — identical instances · ours single-threaded, CP-SAT on all 8 cores · both prove the same minimum
bookingsRoomBalance ms (1 core)CP-SAT ms (8 cores)speeduprooms (both)
1,0000.2613.552×34
5,0001.4533.423×133
20,0006.7099.615×462
50,00017.623814×1,112
100,00036.949113×2,202
500,0001862,54114×10,509

Minimum interval coloring is polynomial, and CP-SAT's cumulative propagator solves it exactly — it does not blow up here. So this is not "the competitor can't do it": it is a steady 13–52× while single-threaded against CP-SAT's eight cores, agreeing on the minimum every time, then carrying on to 10M bookings where standing up the CP model is itself the problem. The sell is speed, the exact minimum plus oversold certificate, and zero dependencies — not that we alone are exact.

OccupancyMax — the revenue a greedy leaves behind

The heuristic a property-management system actually runs: sort bookings by value density, first-fit into any free room. Fast, and it quietly discards revenue. Heavily oversold inventory, identical instances:

# max-revenue accept/reject when oversold · ours = provable maximum
bookingsroomsRoomBalance valuegreedy valuegreedy captures
1,00020479,498187,19139.0%
5,000501,945,937847,44043.6%
20,0001005,506,4852,499,40745.4%
50,00020012,313,4566,534,60553.1%
100,00030021,118,29112,355,62058.5%

On heavily oversold inventory the shipped greedy captures only 39–59% of the attainable revenue; OccupancyMax returns the provable maximum every time. That gap is the sell — not speed (the greedy is faster and worse), but the revenue it silently discards.

# OccupancyMax scale — exact min-cost flow, same instances
bookingsroomssolve_ms
1,000201.4
5,0005010.4
20,00010060
50,000200477
100,000300999

100,000 oversold requests, exact max-revenue selection against 300 rooms, in about one second. OccupancyMax is a flow — heavier than StayOptimal's coloring — but it returns the guaranteed revenue optimum every time.

# vs the exact solvers — OR-Tools CP-SAT and CBC, identical instances · every proven value bit-identical to ours
bookingsroomsvalue (all agree)RoomBalance msCP-SAT msCBC msvs CP-SATvs CBC
100550,7940.108.686×
2508123,8750.3112.740×
50012248,1050.7430.241×
1,00020479,4981.8311252.761×29×
2,00030919,6064.3541796×
5,000501,945,93714.32,193580153×41×
20,0001005,506,48584.728,4052,889336×34×
50,00020012,313,456442DNF7,57717×
100,00030021,118,2911,594DNF14,466

This selection problem is polynomial (its clique matrix is totally unimodular), so an LP-based solver like CBC also reaches the exact optimum at every scale — we are not the only tool that can solve it, and we do not claim CBC can't. What is measured: CP-SAT is the wrong tool here — its search stops proving anything past ~20k (DNF = only the trivial value-0 answer after the time limit) and we are 40–336× faster where it finishes; CBC does scale, but we are still 9–41× faster end-to-end in a dependency-free binary. The margin over CBC narrows with size (29× → 9×) as our own flow cost grows — we state that rather than hide it. Every value CP-SAT and CBC did prove is bit-identical to ours: independent confirmation the engine is exact.

RoomMatch — matching cost independent of booking count

# room-type matching, 4 types × 4 classes upgrade ladder · quotient = wall-clock ms
bookingsquotient_msmin per roommatchable
10,0000.00341yes
100,0000.00121yes
1,000,0000.00081yes
10,000,0000.00091yes
100,000,0000.00081yes

100 million bookings matched to room types in under a microsecond — the engine flows the 4 types × 4 classes quotient and never materializes the bookings.

# vs OR-Tools CP-SAT — min-makespan, identical instances · ours single-threaded, CP-SAT on all 8 cores
requests → typesRoomBalance msCP-SATmakespan (ours / CP-SAT)speedup
1,000 → 1000.36327 ms10 / 10908×
10,000 → 1,0005.5754,891 ms10 / 10~9,850×
100,000 → 10,000339timeout (150 s)10 / 11exact where it times out
1,000,000 → 100,00011,032does not scale16 / —

This is the one place to lean hard. RoomMatch is polynomial via the transportation quotient; CP-SAT is search over an NP formulation, and it falls off a cliff. It is already 908× behind at a thousand requests and ~9,850× at ten thousand — same exact makespan. At a hundred thousand CP-SAT times out after 150 s and returns a worse answer (a busiest-type load of 11 versus our provable minimum of 10); at a million requests ours still returns the exact optimum (16) in about eleven seconds, while standing up a million-request CP model is not viable at all.

Measured on an 11th-Gen Intel Core i5-1135G7 @ 2.40 GHz, 32 GB, Debian 13, gcc -O2, our engine single-threaded, on a shared box under moderate load (≈ 1.6 for the scale runs, drifting to ~3.6 during the head-to-head races) — absolute ms run high vs a quiet box, so the ratios are order-of-magnitude, not lab-grade, and each is a single run per instance. Competitor times (CP-SAT, CBC) are each solver's own end-to-end wall — model build plus solve — with all 8 cores. Correctness: StayOptimal cross-checked on 2,000 random instances against an O(n²) oracle; OccupancyMax on 3,000 random instances against a 2ⁿ brute-force oracle — both exact on every instance, and CP-SAT and CBC independently confirm OccupancyMax's value is exact up to 100,000 bookings. objdump confirms zero floating-point instructions; the engine links no libm.

Live engine

Run it yourself.

These call the real engine over the local gateway — one request line in, one result line out. Demo sizes are kept small so every call is instant; the million-booking figures above are measured results served as data, not run live here. Start the engine with site_hotel/serve.sh, then open this page through the gateway.

# the entire integration surface — one line in, one line out $ printf 'ORBIT_OCCUPANCY stays=0:2,2:4,0:4 inv=1 values=2,2,3\n' | nc -U /tmp/roombalance.sock OK product=OccupancyMax stays=3 inventory=1 accepted=2 rejected=1 total_value=4 offered_value=7 rooms_used=1

1 · StayOptimal — fewest rooms to cover the calendar

Give each booking as arrival:depart (night intervals). Returns the provable minimum rooms — the peak concurrent stays — and, against a fixed inventory, exactly how oversold you are and on which day.

2 · RoomMatch — match bookings to room types

types = rooms per room-type (indexed 0…). requests = bookings per request-class (indexed 0…). eligibility = class:type pairs saying which types can serve each class (upgrade rules). Returns whether everyone fits one room each, and the load per type.

3 · OccupancyMax — keep the most revenue when oversold

You have more bookings than rooms. Give each stay as arrival:depart, an inventory of rooms, and optional per-booking values (revenue; defaults to nights). Returns the provable maximum-revenue subset that still fits, and the revenue offered vs kept.

Integration

Three endpoints, JSON in, JSON out.

# StayOptimal — fewest rooms to cover a booking calendar POST /v1/hotel {"stays": [[a,d], ...], "inv": R?} -> stays feasible rooms_used peak_overlap oversold_by peak_day # RoomMatch — match bookings to room types under upgrade eligibility POST /v1/rooms {"types": [inv0,...], "requests": [c0,...], "elig": [[class,type],...]} -> types requests total_bookings total_rooms matchable fits_inventory min_per_room exact load # OccupancyMax — max-revenue subset when oversold POST /v1/occupancy {"stays": [[a,d], ...], "inv": R, "values": [v0,...]?} -> stays inventory accepted rejected total_value offered_value rooms_used

The browser never sends a raw command line — it posts validated JSON and the gateway builds the engine line itself, so no command injection is possible. Legacy /api/<name> aliases remain for compatibility.

Advisory · Tier-2

Separate, uncertified endpoints — an estimate, never a certificate.

The allocation core above returns certificates. This is a different layer: it estimates the numbers that core takes as inputs — a no-show rate, an overbooking level, a demand forecast — from your own history. Every reply carries "advisory": 1, "uncertified": 1 and an interval; none of them ever returns a certificate, and none is ever mixed into a /v1/hotel|rooms|occupancy answer. A recommended overbooking C re-enters the certified core only as an explicit input (inv = R + C).

Advisory · Tier-2 · uncertified estimate

These call the same daemon, but they estimate — every reply is advisory=1 uncertified=1, carries an interval where one applies, and never a certificate. Nothing here is mixed into a certified /v1/hotel|rooms|occupancy answer; a recommended value re-enters the core only if you pass it as a plain input.

Measured — backtested on real history (Antonio, 119,390 bookings)

No-show rate — Beta-Binomial shrinkage

From your own arrivals and no-shows, a posterior mean shrunk toward a prior, plus a 90% interval. Estimate, not a certificate — the raw empirical rate is shown alongside.

Overbooking level — newsvendor argmax

The level C that maximizes expected net revenue given a no-show rate, a room value and a walk cost. It returns C as a suggestion; you choose whether to call /v1/occupancy with inv = R + C. It is never auto-applied.

Demand forecast — additive pickup

With otb bookings on the books and an expected remaining pickup, the final = on-the-books + remaining, with an interval from the residual scale sd. An estimate that informs the room count R — never a certificate.

Drift / regime — Fisher-Rao vs a noise floor

Split your no-show history into an older and a recent window. The geodesic distance between the two rates is compared to a 1/√N sampling-noise floor: drift means weight the recent window; otherwise pool all history. A read, not a certificate.

Decision — optimizer exact given a supplied curve; no measured revenue lift

Pricing — revenue argmax over your demand curve

Give parallel lists of candidate prices and the demand you expect at each, plus rooms. It returns the exact revenue-max price capped at capacity. The optimizer is exact given the curve you supply; the elasticity itself is not measured here, so there is no revenue-lift claim.

LOS / CTA — shortest stay that clears the bids

From per-night bid prices and a nightly rate, the shortest length of stay whose rate clears the sum of the first L bids — else close-to-arrival. A proposed restriction, not a certificate.

Group — revenue vs the transient it displaces

A take/pass read: a group's revenue against the transient revenue it would displace, with exact min() accounting over a per-night transient forecast. A recommendation, not a certificate.

# No-show rate — Beta-Binomial shrinkage of your history toward a prior (bp = /10000) POST /v1/advisory/noshow {"arrivals": A, "noshows": N, "prior_bp": p?, "k": k?} -> advisory uncertified rate lo hi raw # Overbooking level — newsvendor argmax of expected net revenue POST /v1/advisory/overbook {"rooms": R, "rate_bp": p, "value": v, "walk": w, "cmax": c?} -> advisory uncertified recommend_C accept_up_to exp_net gain critical_fractile # Drift — has your no-show behaviour changed? Fisher-Rao geodesic vs a noise floor POST /v1/advisory/regime {"old_arrivals": Ao, "old_noshows": No, "recent_arrivals": Ar, "recent_noshows": Nr} -> advisory uncertified old_rate recent_rate fisher_distance ratio drift recommend # Demand — additive pickup: forecast final bookings from what's on the books now POST /v1/advisory/demand {"otb": B, "remaining": r, "sd": s?} -> advisory uncertified forecast lo hi # Pricing — revenue argmax over your demand curve, capped at capacity POST /v1/advisory/price {"prices": [p0,...], "demand": [d0,...], "rooms": R} -> advisory uncertified best_price sold revenue # LOS/CTA — shortest min-stay that clears the per-night bid prices; else close-to-arrival POST /v1/advisory/los {"bid": [b0,...], "rate": u, "maxL": L?} -> advisory uncertified min_los cta margin # Group — exact displacement accounting: group revenue vs transient revenue given up POST /v1/advisory/group {"size": g, "grate": u, "fc": [d0,...], "avail": R, "trate": u} -> advisory uncertified net_gain accept group_revenue displaced_revenue

Cold-start seeds come from a public dataset for a brand-new property with no history; the moment you supply your own bookings they take over. A public-data average is never presented as a prediction about your guests. Every advisory number is measured before it ships — see the backtest in the repo (tier2/TIER2_BACKTEST.md).

Why it matters

Sell the room you have, and prove the decision.

Provision the rooms you actually need

The peak concurrent stays is the room count you must have. StayOptimal makes it provably minimal and tells you exactly how far a fixed inventory is oversold — a number you can hand to operations before you promise a room you don't have.

Recover the revenue a greedy discards

When you're oversold, which bookings you accept decides the night's revenue. On heavily oversold instances a shipped greedy captured only 39–59% of the attainable revenue; OccupancyMax keeps the provable maximum, and shows you the gap.

Honour your upgrade rules, at any volume

RoomMatch checks that every booking can be housed one room each under your upgrade ladder — and its cost tracks the number of room types and request classes, not the number of bookings, so a hundred million reservations cost the same as ten thousand.

A guarantee you can write down

"Fewest rooms to cover the calendar," "everyone housed under the upgrade rules," "the maximum revenue this inventory can earn tonight" — each is a certificate a heuristic cannot give and RoomBalance can.

Trustworthy in production

Built to survive an audit, not just a demo.

Every property below is verified against the shipped binary — for the operator who has to put this in the path of a real booking system.

Zero dependencies

ldd shows libc only — no libm, nothing transitive to vendor, audit, or patch.

Air-gapped by construction

Opens an AF_UNIX socket at 0600 only — no network syscalls anywhere, and it writes no files. It cannot phone home.

Deterministic

Integer decision logic, zero floating point anywhere (objdump-verified). Same input, same output, on every box — cacheable and sign-off-able.

Flat tail latency

Pure C, no garbage collector, no JIT, no interpreter. No p99 spike from a GC pause or a cold import.

Bounded memory

RoomMatch never materializes the bookings — it flows the room-type quotient — so matching cost is independent of reservation volume.

Self-certifying output

Every response carries its own certificate — minimum rooms, oversold amount, or provable-maximum revenue. Trust the result without re-solving it.

Stateless, one line in / out

No session, no auth handshake, no schema. printf … | nc -U from any language.

Cross-checked test suites

StayOptimal on 2,000 random calendars vs an O(n²) oracle; OccupancyMax on 3,000 random instances vs a 2ⁿ brute-force oracle — exact on every one.

Honest scope

What it proves, and what it doesn't.

Overclaiming here loses the operator, so we state it plainly. RoomBalance is exact allocation given your bookings and your rates. It is not a yield or pricing system, and it does not pretend to be.

Provably optimal

  • Fewest rooms to cover a booking calendar (interval coloring), with an oversold certificate against any inventory.
  • Room-type matching under upgrade eligibility — exact on the classes × types quotient.
  • Max-revenue accept/reject when oversold — exact min-cost flow, verified against a brute-force oracle.

Out of scope — and we say so

  • Dynamic pricing / rate optimization is stochastic and out of lane — you supply the rates, the certified core allocates.
  • Demand and no-show forecasting are not part of the certified core — it takes your bookings and rates as given and answers exactly for them. The separate Tier-2 advisory layer will estimate these from your own history, but every such reply is explicitly uncertified, carries an interval, and never shares a response with a certificate.
  • Overbooking, cancellation and no-show policy are business decisions the core never makes for you. The advisory layer proposes a level; whether you use it — and it re-enters the core only as a plain inv = R + C input — stays your call.

vs alternatives

Straight answers to the obvious objections.

Our PMS already assigns rooms — why add this?
A PMS assigns rooms greedily and proves nothing. It can't tell you the fewest rooms that cover your calendar, and when you're oversold its accept/reject captured only 39–59% of the attainable revenue in our tests. RoomBalance returns the proven optimum for each, with a certificate — it complements the PMS rather than replacing it.
Isn't this just yield management / revenue management?
No. We don't forecast demand and we don't set prices. RoomBalance solves the exact allocation given the bookings and rates you already have: minimum rooms, room-type matching, and — when oversold — the max-revenue subset. It sits underneath a yield system, not in place of one.
We use an ILP / CP-SAT solver for this.
We race it, on byte-identical instances. On room-type matching RoomBalance returns the same exact makespan hundreds to thousands of times faster — 908× at a thousand requests, ~9,850× at ten thousand — and at a hundred thousand CP-SAT times out and returns a worse answer. Minimum rooms and max-revenue selection are polynomial, so CP-SAT reaches the same optimum there too: we're 13–52× faster on the room count and 40–336× faster on the revenue selection (where CP-SAT stops finishing past ~20k), single-threaded against its eight cores — with no solver stack to license or vendor.
We just eyeball it in a spreadsheet.
Fine at a handful of bookings; it doesn't scale to a year of a chain and it gives you no certificate. RoomBalance does a ten-million-booking calendar in about six seconds and hands back a number you can defend.
How do I know the answer is right, and how hard is it to integrate?
It's dependency-free C that can't touch the network, returns the same answer every time, and every answer carries its own certificate — cross-checked against brute-force oracles on thousands of random instances. Integration is one request line in, one result line out.

Run a pilot

Send us a week of your real calendar — it costs you nothing.

Export a week of bookings — arrivals, departures, room types, rates. We'll show you the exact minimum rooms, whether everyone fits under your upgrade rules, and — if you were oversold — the revenue a greedy left on the table. No accounts, no checkout: a conversation and a measurement.

Start a pilot conversation

Nous Research Centre · erick.llamas@gmail.com