Files
mpm-ad-roi-cap-table/SKILL.md
T

270 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: mpm-ad-roi-cap-table
description: Build a 60-month ROI / cap-table projection for MPM-financed digital signage hardware paid back out of advertising revenue. Use this whenever the user pairs an advertising revenue proposal (Passent Media / Galli Media, or any DOOH ad pro-forma) with the Odoo sales order for that same deployment and wants to know when the deal pays for itself — triggers include "ROI projection", "cap table", "payback model", "when does this pay off", "ad-financed hardware", "revenue share model", "how long to recover COGS", "free hardware paid by ads", "Passent proposal", "model this deal against ad revenue", or any request pairing an S##### quote with ad revenue numbers. Also use when the user asks what the ROI would be at MSRP versus at the discounted sale price, when they want the partner and end-user revenue split modeled (MPM / Passent / reseller / end user), when they want MPM-financed compared against customer-financed acquisition, or when they ask about the cost of money or interest on capital MPM fronts. One proposal models exactly one sales order — the skill models only the order the user names and never reuses one deal's ad revenue on another. Prefer this skill over generic spreadsheet work whenever MPM financing and advertising revenue appear together, even if the user does not say "ROI" or "cap table" explicitly.
---
# MPM Advertising ROI Cap Table
## What this models
A signage deployment generates advertising revenue, and that revenue is split among MPM,
Passent, the reseller, and the end user. Who fronted the capital determines both the split
and what "payback" even means, so the skill produces three distinct models from one set of
inputs.
| Model | Split | Carries | Audience |
|---|---|---|---|
| **2A** Internal — MPM Financed, ROI & Payback | MPM 30 / Passent 30 / Reseller 10 / End User 30 | COGS, sale price and MSRP milestones against MPM's share, plus interest on the financed capital | Internal — contains cost and margin |
| **2B** Internal — MPM Financed, Revenue Distribution | same 30/30/10/30 | Revenue only. No cost basis, no milestones, no margin | Internal, but safe to share the numbers with partners |
| **2C** Customer-Financed Acquisition | MPM 5 / Reseller 2.5 / Passent 22.5 / End User 70 | MSRP milestone only, measured against the **End User's** 70% share | Customer-facing — no COGS or sale price |
Each is written as its own workbook. That separation is deliberate: 2A contains cost and
margin data that must not reach a reseller or a customer, and the cheapest way to guarantee
that is for the customer-facing file to never contain it. Do not merge them into one file
to save an attachment.
The three payoff milestones, where a model carries them:
| Milestone | Meaning | Fill |
|---|---|---|
| COGS | Hard cost is recovered — the deal stops losing money | Yellow |
| Project Sale Price | The discounted contract value is recovered — MPM has earned what it would have booked on the sale | Orange |
| Total MSRP | Full undiscounted list value is recovered — the deal beat a full-price sale | Green |
In 2A these measure MPM's recovery of capital it put at risk. In 2C nothing is at risk for
MPM, so the single MSRP milestone answers the customer's question instead: having bought at
list, when does their 70% share pay them back? Keep that distinction sharp — the same green
cell means different things in the two files, and the Summary sheet names the party it
belongs to.
## Cost of money
Capital fronted is capital not earning elsewhere, and management wants that visible rather
than assumed away. In 2A the principal is MPM's **one-time COGS** — the actual cash out the
door, not the price it would have sold for, because you cannot borrow your own margin.
Interest accrues monthly on the declining balance at 12% APR by default, and MPM's monthly
share services interest first, then principal.
A milestone therefore clears only when cumulative net recovers the target **plus the
interest accrued to that month**. That definition makes the COGS milestone and the loan
balance hitting zero the same event, which is a useful internal check: if the Summary
reports a different month for "balance retired" and for the COGS milestone, something is
wrong in the config.
Watch for negative amortization. During the ramp, MPM's share can be smaller than the
interest accruing, so the balance grows for the first months before it falls. The script
detects this and flags it in red on the Summary. It is not necessarily fatal, but it is
exactly the thing a lender or a CFO will ask about, so never let it pass silently.
2B carries no interest because it is a revenue projection, not a payback model. 2C carries
none because the customer bought the system outright and MPM has no capital at risk.
## One proposal, one sales order
This is the constraint that governs everything else, so establish it before doing any math.
An advertising proposal is not a market rate card. Passent prices a *specific* deployment —
these vehicles, this ridership, this journey length, this slot count, this fill assumption.
The $22,138/month in the John Wayne proposal is a property of eleven screens on an airport
shuttle loop carrying 64,000 boardings a month. It says nothing about what a wayside
deployment in Annapolis or a fleet retrofit in Columbus would earn, and using it there —
or scaling it, or borrowing "a ballpark from a similar market" — produces a number that
looks rigorous and is fiction.
So: **model only the sales order the user names, using only the proposal written for that
same deployment.** Concretely:
- One proposal pairs with one sales order. Do not fan a single proposal across several
quotes, and do not roll multiple quotes into one projection.
- If the user names a sales order and there is no advertising proposal for that project,
stop and say so. Ask for the proposal. Do not substitute figures from another deal,
another agency, a rate card, or your own estimate of what that market might support —
and do not proceed on a verbal ballpark unless the user is clearly quoting Passent's
numbers *for this deployment* and confirms that is what they mean.
- If the user asks you to reuse an earlier model for a new order, treat that as a request
for a new proposal, not a copy of the old one.
### Verify the pairing before you build
Even when both documents are in hand, confirm they describe the same deployment. Check:
- **Customer and project reference** — the proposal's PROJECT/REF and the quote's customer
should name the same agency and site.
- **Screen count** — the proposal's slot and impression math assumes a screen count. It
should reconcile with the display quantity on the quote. Eleven M27V units on S01027
against a proposal built on an eleven-screen shuttle loop is a match.
- **Deployment type** — mobile/onboard versus wayside versus LED. A proposal built on
onboard journey time cannot price a wayside deployment, where dwell time and impression
math work completely differently.
When something doesn't reconcile, raise it rather than modeling through it. A mismatch
usually means the proposal predates a scope change, and the model would quietly inherit
the stale assumption.
## Workflow
### 1. Gather the ad revenue proposal
Read the Passent Media / Galli Media proposal (usually a one-page PDF). Extract:
- Monthly on-boardings, average journey time, ad slot count, total monthly impressions
- Each revenue line: source name, impressions/K, CPM, % fill, monthly gross, annual gross
- The TOTALS row — this is the **gross ad revenue** figure the whole model runs on
- Any event surge block (e.g. "2028 LA 28 Games 2.5 Months"). Capture it but leave it
**disabled by default** — surges are upside, not a payback assumption.
Note the proposal's own caveat about fill rates being lower early. That is why the model
ramps (step 4).
### 2. Pull the sales order from Odoo
Pull the one order the user named — not sibling quotes for the same customer, not a
superseded revision. If several revisions exist, ask which one is live rather than
guessing. The margin fields are what make this work; do not try to reconstruct cost from
product records.
```
mcp__plugin_odoo-mpm_odoo-mpm__search_sales_orders(query="S01027")
mcp__plugin_odoo-mpm_odoo-mpm__odoo_search_read(
model="sale.order.line",
domain=[["order_id","=",<id>]],
fields=["name","product_id","product_uom_qty","price_unit","discount",
"price_subtotal","margin","margin_percent","purchase_price",
"display_type","sequence"],
limit=60)
```
Per line:
- **MSRP extended** = `price_unit × product_uom_qty` (Odoo's `price_unit` is the MSRP shown on the quote; the discount is applied separately)
- **Net / sale price** = `price_subtotal`
- **COGS** = `price_subtotal margin` (Andy's shorthand; equals `purchase_price × qty`)
Skip rows where `display_type` is `line_section` or `line_note` — they are headers, not money.
Do not silently trust a `margin` of 0 on a line with real dollars. `[DBE-Dispatch]` and
`[PS-Travel]` are genuine pass-throughs at 100% cost, but a zero margin on a hardware line
usually means cost data is missing in Odoo. Flag those lines to the user rather than
modeling them as free money.
### 3. Classify one-time vs recurring — and confirm it
This is the single most consequential judgment call in the model, so surface it rather than
deciding silently. An annual license booked once on the quote is a cost MPM pays *every
year for five years*. Treating it as one-time understates the drag by roughly 4× over the
horizon.
Recurring signals — product code prefixes `LIC-`, `SUP-`, `API`, or names containing
*license, licensing, support agreement, hosting, subscription, SaaS, annual, maintenance,
extended warranty*. Everything else (hardware, cabling, mounting kits, DBE installation,
provisioning, PM/design/onsite labor, travel) is one-time.
Present the classification as a short table and ask the user to confirm before building.
Note the effective monthly cost so it is easy to sanity-check — e.g. MPTV Professional at
$199/unit/year across 11 units is $182.42/month, and Andy's mental benchmark is "roughly
$20 per controller per month."
Recurring lines are excluded from the payoff *targets* (which represent capital MPM fronts)
and instead run as a monthly expense against cash flow for the full horizon. This avoids
double-counting year one and keeps the ongoing-cost drag visible.
### 4. Set the assumptions
Defaults, all overridable — state them back to the user:
- **Horizon**: 60 months
- **Ramp**: linear from 25% of steady-state in month 1 to 100% by month 12, reflecting the
proposal's own fill-rate caveat
- **Splits**: the 2A/2B and 2C presets above. They are built into the script; override them
in the config only when a specific deal was negotiated differently, and say so if you do.
- **Cost of money**: 12% APR, declining balance, principal = one-time COGS (2A only)
- **Recurring licensing**: borne by MPM at vendor cost in 2A and 2B; borne by the End User
at invoiced price in 2C, since a customer who bought the system is paying its renewals
- **Client contribution**: $0 by default. If the deal is a hybrid where the client pays a
deposit, enter those as dated contributions.
- **Event surges**: off
Every share set must sum to 100% of gross. The script refuses to build otherwise, because a
split that doesn't allocate every dollar means someone's cut was dropped or double-counted.
### 5. Build the config and run the script
Write one JSON config and run the bundled builder once — it emits all three workbooks from
the same inputs, which is what keeps them consistent with each other.
```bash
python3 scripts/build_cap_table.py config.json --model all --outdir ./out
```
Use `--model 2A` with `-o` when the user wants a single variant rebuilt. The console recap
prints each model's split, 60-month distribution, interest, and milestone months, so you can
check the numbers before opening anything.
`references/config_schema.md` documents every field, and `references/worked_example.md`
walks through the John Wayne Airport deal end to end with the numbers it should produce —
read it if you are unsure how a field maps to the source documents.
### 6. Sanity-check before delivering
Fast checks that catch the errors that actually happen:
- The workbook names one sales order and one ad proposal, and they are the pair the user
asked about. Screen count reconciles between them.
- Sum of line MSRP ≥ sum of net ≥ sum of COGS. If not, a discount or margin was misread.
- Net subtotal across all lines equals the quote's printed Total.
- Each model's party shares sum to 100%, and the party columns in any month add up to that
month's gross.
- In 2A, the month the loan balance reaches zero equals the COGS milestone month. A
discrepancy means the principal basis or the interest config is off.
- Month-60 cumulative for a party is roughly `share × monthly_gross × ~57` (the ramp costs
about three months of revenue), less that party's recurring charge.
- **2B and 2C contain no COGS, margin, or sale-price figures anywhere.** Open them and
confirm. This is the check that matters most, because the cost of getting it wrong is
showing a reseller or a customer MPM's margin.
Then present the workbooks with `present_files` and lead with the answer: for 2A the payoff
months and interest paid, for 2C the customer's payback month. Andy reads the summary first
and opens the monthly grid only when a number looks off.
## Workbook structure
**2A** — Summary, Monthly Projection, Cost Basis, Assumptions.
**2B** — Summary, Monthly Projection, Assumptions.
**2C** — Summary, Monthly Projection, Assumptions.
1. **Summary** — deal identity, the pairing check, the distribution table (each party's
share, steady-state monthly, year one, 60-month total), then for models that carry them:
the financing block (principal, rate, total interest, month the balance retires), the
milestone table, and the ROI table.
2. **Monthly Projection** — the 60-row grid. Month, date, ramp %, gross, then a column per
party and a cumulative column per party. Models with a milestone party add that party's
recurring charge, net, interest and balance where applicable, and a cumulative net column
where milestone crossings are filled yellow/orange/green.
3. **Cost Basis** (2A only) — every Odoo line with qty, MSRP unit, extended MSRP, discount %,
net, margin, COGS, and the recurring flag. This is the audit trail; when someone
challenges a number, this is the sheet you open. Lines whose margin equals their full net
are flagged red — Odoo has no standard cost for them, so the COGS target is optimistic.
4. **Assumptions** — ad revenue inputs, the split, ramp parameters, financing terms, who
bears recurring licensing, and what was excluded and why.
## Reading the result honestly
A model like this is persuasive precisely because it is specific, so keep the uncertainty
visible rather than burying it. Ad revenue is a forecast built on an assumed 65% fill rate
that the proposal itself warns may not hold early. Report payoff as "month 13 on a 30% share
with a 12-month ramp and 12% money," not "month 13." When a milestone isn't reached inside
60 months, say so plainly and show how far short it lands — that is a more useful answer
than extending the horizon until it looks good.
The comparison management will actually want is between 2A and 2C, and it is not simply
"which nets MPM more." Financing (2A) puts real cash at risk for a real interest cost and
returns a 30% share for five years; a customer-financed sale (2C) books the hardware
immediately at 5% of ad revenue thereafter. Frame it as a capital allocation question —
what MPM gives up by tying up the money versus what the larger share is worth — and give
the interest figure and the negative-amortization window as explicit costs rather than
letting the larger 60-month number speak for itself.
When presenting 2C to a customer, the honest framing is that their payback depends on a
forecast they do not control. Say what the model assumes and what happens if fill comes in
low, rather than handing over a payback month as though it were a term of sale.