This appendix demonstrates how touring deal calculations (GBOR/NBOR/taxes/split points/versus/plus) are represented using our current Deal Foundry + Deal Engine method:
- The deal model is the canonical definition of inputs, clause structure, and computation intent (not executable math).
- The model ships with fixtures (inputs → expected outputs) and a contract template.
- During authoring, Deal Foundry can generate and run a sandboxed TypeScript evaluator (LLM-generated) in a Fly.io Sprite to verify desk-calculation math against fixtures.
- Production deal programs can be implemented later (any language) and must pass the same fixture pack.
Key point: we do not require calling systems to compute anything. They send raw deal data (ticket tiers, fees, taxes, expense lines), and the deal program computes results.
1) Touring formula catalog (source material)
These are the formulas we need to support for touring-style deals.
1.1 Revenue formulas
Gross Box Office Receipts (GBOR)
GBOR = Σ (tickets_sold_per_tier × tier_price)Net Box Office Receipts (NBOR) (simple form)
NBOR = GBOR − (Sales Tax + Facility Fees)
1.2 Tax calculation logic
Divider method (preferred when tax is included in the ticket price)
TaxAmount = Gross − Gross/(1 + TaxRate)Multiplier method (rare; when tax is applied on top)
TaxAmount = Gross × TaxRate
1.3 Split point formulas
Plus deal split point
SplitPoint = TotalExpenses + ArtistGuaranteeStandard split point (plus backend / promoter profit)
SplitPoint = (TotalExpenses + ArtistGuarantee) × (1 + PromoterProfitPct)Versus deal split point (as shown in source)
SplitPoint = TotalExpenses + (ArtistGuarantee / ArtistOveragePct)
(note: requiresArtistOveragePctinput.)
1.4 Payout formulas
Plus deal
Payout = Guarantee + [(NBOR − SplitPoint) × ArtistPct]Versus (Gross)
Payout = max(Guarantee, GBOR × ArtistPct)Versus (Net)
Payout = max(Guarantee, (NBOR − Expenses) × ArtistPct)
2) Deal type model (DSL)
This is a deal type, not an instance. Nothing negotiated (90%, $50k, etc.) is hardcoded. Those are inputs.
2.1 DealModel DSL (structural + computation intent)
model { deal_type: "touring_calcs_v1", version: 1.0.0, primitive_catalog_version: 1.0.0 }
workflow {
allowed { DRAFT; OFFER_OUT; HOLD; BOOKED; CANCELLED; }
mapping {
DRAFT -> draft;
OFFER_OUT -> negotiating;
HOLD -> negotiating;
BOOKED -> closed cp_ready: true;
CANCELLED -> cancelled;
}
}
event {
name: show_settled
description: "Settlement confirmed"
source: "internal_milestones"
condition: "evidence('show.settled') == true"
}
inputs {
schema: """
{
"type":"object",
"required":[
"currency",
"workflowState",
"dealVariant",
"ticketing",
"deductions",
"expenses",
"terms"
],
"properties":{
"currency": {"type":"string"},
"workflowState": {"type":"string"},
"dealVariant": {
"type":"string",
"enum":[
"versus_gross",
"versus_net",
"plus_deal",
"standard_split_point"
]
},
"ticketing":{
"type":"object",
"required":["tiers"],
"properties":{
"tiers":{
"type":"array",
"items":{
"type":"object",
"required":["name","quantity","compsKills","price"],
"properties":{
"name":{"type":"string"},
"quantity":{"type":"integer","minimum":0},
"compsKills":{"type":"integer","minimum":0,"default":0},
"price":{"type":"number","minimum":0}
}
}
}
}
},
"deductions":{
"type":"object",
"required":["taxMethod","taxRate","facilityFees"],
"properties":{
"taxMethod":{"type":"string","enum":["divider","multiplier"]},
"taxRate":{"type":"number","minimum":0,"maximum":1},
"facilityFees":{"$ref":"de://schema-library/lib/common@1.0.0#/$defs/Money"}
}
},
"expenses":{
"type":"object",
"required":["totalExpenses"],
"properties":{
"totalExpenses":{"$ref":"de://schema-library/lib/common@1.0.0#/$defs/Money"}
}
},
"terms":{
"type":"object",
"required":["guarantee","artistPct"],
"properties":{
"guarantee":{"$ref":"de://schema-library/lib/common@1.0.0#/$defs/Money"},
"artistPct":{"type":"number","minimum":0,"maximum":1},
"promoterProfitPct":{"type":"number","minimum":0,"maximum":1,"default":0.0},
"artistOveragePct":{"type":["number","null"],"minimum":0,"maximum":1},
"bonusPerPaidTicket":{"$ref":"de://schema-library/lib/common@1.0.0#/$defs/Money"},
"bonusCapTickets":{"type":"integer","minimum":0},
"commissionPct":{"type":"number","minimum":0,"maximum":1,"default":0.0}
}
}
}
}
"""
}
computations {
metric gbor {
type: money
display: "Gross Box Office Receipts (GBOR)"
description: "GBOR = Σ(tier.quantity × tier.price) across ticket tiers."
depends_on_inputs: ["ticketing.tiers.quantity", "ticketing.tiers.price"]
invariants: ["gbor >= 0"]
}
metric salesTax {
type: money
display: "Sales Tax"
description: "Divider: Gross - Gross/(1+taxRate); Multiplier: Gross*taxRate"
depends_on_inputs: ["deductions.taxMethod", "deductions.taxRate", "ticketing.tiers.quantity", "ticketing.tiers.price"]
}
metric nbor {
type: money
display: "Net Box Office Receipts (NBOR)"
description: "NBOR = GBOR − (SalesTax + FacilityFees)"
depends_on_inputs: ["deductions.facilityFees", "deductions.taxRate", "deductions.taxMethod", "ticketing.tiers.quantity", "ticketing.tiers.price"]
}
metric splitPoint {
type: money
display: "Split Point"
description: "Depends on dealVariant: plus, standard split point, or versus split point."
depends_on_inputs: [
"dealVariant",
"expenses.totalExpenses",
"terms.guarantee",
"terms.promoterProfitPct",
"terms.artistOveragePct"
]
}
metric payoutBase {
type: money
display: "Payout Base"
description: "Variant-dependent payout formula (plus / versus gross / versus net)."
depends_on_inputs: [
"dealVariant",
"terms.guarantee",
"terms.artistPct",
"expenses.totalExpenses",
"deductions.facilityFees",
"deductions.taxRate",
"deductions.taxMethod",
"ticketing.tiers.quantity",
"ticketing.tiers.price"
]
depends_on_metrics: ["gbor","salesTax","nbor","splitPoint"]
}
metric ticketBonus {
type: money
display: "Ticket Bonus"
description: "Bonus = bonusPerPaidTicket × min(paidTickets, bonusCapTickets). paidTickets = Σ max(quantity - compsKills, 0)"
depends_on_inputs: ["terms.bonusPerPaidTicket","terms.bonusCapTickets","ticketing.tiers.quantity","ticketing.tiers.compsKills"]
}
output walkout {
type: money
display: "Walkout"
description: "Walkout = (payoutBase + ticketBonus) − commissionPct × (payoutBase + ticketBonus)"
depends_on_inputs: ["terms.commissionPct"]
depends_on_metrics: ["payoutBase","ticketBonus"]
}
}
clause {
name: touring_compensation
kind: guarantee
description: "Touring compensation payout"
template: """
Artist shall receive a GUARANTEE of ${{terms.guarantee.amount.amount}} {{currency}}
under a {{dealVariant}} structure with artist percentage {{terms.artistPct}}.
Facility fees and sales tax are deducted per the deductions section.
Ticket bonus: ${{terms.bonusPerPaidTicket.amount.amount}} per paid ticket up to {{terms.bonusCapTickets}} tickets.
Commission: {{terms.commissionPct}}.
"""
payment_terms {
term {
name: settlement_balance
payment_type: installment
amount: calculated "clause_amount()"
due_date: relative { trigger: "show_settled == true", offset_days: "0" }
payment_method: ach
payment_destination_input_ref: "PAYMENT_PROFILE_INPUT"
}
}
}
type { name: touring_calcs_v1 compose: touring_compensation }3) Example instance payload (your touring terms)
{
"currency": "USD",
"workflowState": "BOOKED",
"dealVariant": "versus_net",
"ticketing": {
"tiers": [
{ "name": "Reserved 1", "quantity": 558, "compsKills": 20, "price": 59.50 },
{ "name": "Reserved 2", "quantity": 1988, "compsKills": 30, "price": 49.50 },
{ "name": "Reserved 3", "quantity": 262, "compsKills": 0, "price": 39.50 },
{ "name": "Reserved 3 (low)", "quantity": 12, "compsKills": 0, "price": 29.50 },
{ "name": "VIP 1", "quantity": 31, "compsKills": 0, "price": 159.50 },
{ "name": "VIP 2", "quantity": 31, "compsKills": 0, "price": 119.50 }
]
},
"deductions": {
"taxMethod": "divider",
"taxRate": 0.06,
"facilityFees": { "amount": "0.00", "currency": "USD" }
},
"expenses": {
"totalExpenses": { "amount": "59279.44", "currency": "USD" }
},
"terms": {
"guarantee": { "amount": "50000.00", "currency": "USD" },
"artistPct": 0.90,
"bonusPerPaidTicket": { "amount": "1.00", "currency": "USD" },
"bonusCapTickets": 2758,
"commissionPct": 0.0725,
"promoterProfitPct": 0.0,
"artistOveragePct": null
}
}4) Authoring-time verification via Sprite (hypothetical Bun/TS implementation)
Deal Foundry generates evaluator code (via LLM), runs it in a sprite, and compares results to expected fixture outputs.
4.1 Evaluator module (generated TypeScript)
// evaluator.ts (generated, authoring-only)
type Money = { amount: string; currency: string };
function money(amount: number, currency: string): Money {
return { amount: amount.toFixed(2), currency };
}
function addMoney(a: Money, b: Money): Money {
if (a.currency !== b.currency) throw new Error("currency_mismatch");
return money(parseFloat(a.amount) + parseFloat(b.amount), a.currency);
}
function subMoney(a: Money, b: Money): Money {
if (a.currency !== b.currency) throw new Error("currency_mismatch");
return money(parseFloat(a.amount) - parseFloat(b.amount), a.currency);
}
function mulMoney(a: Money, pct: number): Money {
return money(parseFloat(a.amount) * pct, a.currency);
}
function gbor(inputs: any): Money {
const ccy = inputs.currency;
const gross = inputs.ticketing.tiers.reduce((sum: number, t: any) => sum + (t.quantity * t.price), 0);
return money(gross, ccy);
}
function paidTickets(inputs: any): number {
return inputs.ticketing.tiers.reduce((sum: number, t: any) => sum + Math.max((t.quantity - (t.compsKills || 0)), 0), 0);
}
function salesTax(inputs: any): Money {
const ccy = inputs.currency;
const gross = parseFloat(gbor(inputs).amount);
const r = inputs.deductions.taxRate;
const taxAmt = inputs.deductions.taxMethod === "divider"
? (gross - (gross / (1 + r)))
: (gross * r);
return money(taxAmt, ccy);
}
function nbor(inputs: any): Money {
const gross = gbor(inputs);
const tax = salesTax(inputs);
const facility = inputs.deductions.facilityFees;
return subMoney(subMoney(gross, tax), facility);
}
// Versus(Net): max(Guarantee, (NBOR - Expenses) * ArtistPct)
function payoutBase(inputs: any): Money {
const guarantee = inputs.terms.guarantee;
const base = subMoney(nbor(inputs), inputs.expenses.totalExpenses);
const pctPay = mulMoney(base, inputs.terms.artistPct);
return (parseFloat(pctPay.amount) > parseFloat(guarantee.amount)) ? pctPay : guarantee;
}
function ticketBonus(inputs: any): Money {
const ccy = inputs.currency;
const paid = paidTickets(inputs);
const cap = inputs.terms.bonusCapTickets;
const per = parseFloat(inputs.terms.bonusPerPaidTicket.amount);
return money(per * Math.min(paid, cap), ccy);
}
export function computeModel(ctx: { inputs: any }) {
const inputs = ctx.inputs;
const base = payoutBase(inputs);
const bonus = ticketBonus(inputs);
const gross = addMoney(base, bonus);
const commission = mulMoney(gross, inputs.terms.commissionPct || 0);
const walkout = subMoney(gross, commission);
return { outputs: { gbor: gbor(inputs), salesTax: salesTax(inputs), nbor: nbor(inputs), payoutBase: base, ticketBonus: bonus, walkout } };
}4.2 Bun runner (inside a sprite)
// runner.ts
import { readFileSync, writeFileSync } from "node:fs";
import { computeModel } from "./evaluator";
const job = JSON.parse(readFileSync(process.argv[2], "utf-8"));
const result = computeModel({ inputs: job.payload });
writeFileSync(process.argv[3], JSON.stringify(result, null, 2));Command executed by the Evaluation Worker:
bun run /work/evaluator/runner.ts /work/job.json /work/out/results.json4.4 How Deal Foundry actually runs this code (API wiring)
The TypeScript shown above runs inside a sandbox (local sandbox or Fly.io Sprite).
The Deal Foundry Authoring Service does not call the Sprite directly. Instead:
- UI calls the Authoring Service
- Authoring Service creates an evaluation job
- An Evaluation Worker (Bun service) pulls the job, runs the Sprite, posts results back
- UI polls the Authoring Service for job status/results
4.4.1 External APIs (UI → Authoring Service)
Create an evaluation job (scenario or fixtures):
POST /authoring/drafts/{draftId}/eval-jobs
Content-Type: application/json
{
"type": "scenario",
"payload": { /* touring instance payload */ },
"evidenceOverrides": { "show.settled": true },
"asOf": "2026-01-15"
}Response:
{ "jobId": "ej_01J..." }UI polling:
GET /authoring/eval-jobs/ej_01J...
GET /authoring/eval-jobs/ej_01J.../results4.4.2 Internal APIs (Evaluation Worker ↔ Authoring Service)
Two equivalent patterns are acceptable:
A) Worker polls (simplest)
GET /internal/eval-jobs/next?limit=1B) Authoring Service pushes
POST http://evaluation-worker.internal/jobs/run
{ "jobId": "ej_01J..." }Worker retrieves the job definition (artifacts + payload pointers):
GET /internal/eval-jobs/ej_01J...Worker posts results back:
POST /internal/eval-jobs/ej_01J.../results
Content-Type: application/json
{
"status": "succeeded",
"durationMs": 842,
"outputs": { "walkout": { "amount":"67163.00", "currency":"USD" } },
"diffs": [],
"warnings": [],
"traceRef": "trace_..."
}4.4.3 Sprite execution (Worker → Sprite)
Inside the Sprite, the worker writes:
/work/job.json(payload + evidence)/work/evaluator/evaluator.ts/work/evaluator/runner.ts
Then executes:
bun run /work/evaluator/runner.ts /work/job.json /work/out/results.jsonWorker reads /work/out/results.json and posts the results back to the Authoring Service.
4.4.4 Why this matters
- The browser never talks to Sprites.
- AuthZ and auditing are centralized in the Authoring Service.
- Cleanup is job-scoped (sprites are killed by TTL / reaper).
- The authoring UI experience is deterministic and debuggable.
5) Notes on alternatives
Alternative scenarios are instance-level:
- change
taxMethod/taxRate - change
facilityFees - change
dealVariant - change
totalExpenses - change
artistPct, guarantee, cap, etc.
Add fixtures for each alternative to lock expected behavior.