Skip to content

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 + ArtistGuarantee

  • Standard split point (plus backend / promoter profit)
    SplitPoint = (TotalExpenses + ArtistGuarantee) × (1 + PromoterProfitPct)

  • Versus deal split point (as shown in source)
    SplitPoint = TotalExpenses + (ArtistGuarantee / ArtistOveragePct)
    (note: requires ArtistOveragePct input.)

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)

g4
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)

json
{
  "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)

ts
// 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)

ts
// 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:

bash
bun run /work/evaluator/runner.ts /work/job.json /work/out/results.json

4.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):

http
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:

json
{ "jobId": "ej_01J..." }

UI polling:

http
GET /authoring/eval-jobs/ej_01J...
GET /authoring/eval-jobs/ej_01J.../results

4.4.2 Internal APIs (Evaluation Worker ↔ Authoring Service)

Two equivalent patterns are acceptable:

A) Worker polls (simplest)

http
GET /internal/eval-jobs/next?limit=1

B) Authoring Service pushes

http
POST http://evaluation-worker.internal/jobs/run
{ "jobId": "ej_01J..." }

Worker retrieves the job definition (artifacts + payload pointers):

http
GET /internal/eval-jobs/ej_01J...

Worker posts results back:

http
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:

bash
bun run /work/evaluator/runner.ts /work/job.json /work/out/results.json

Worker 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.

Confidential. For internal use only.