From e3503402ad37a7c28a636c5de6676401b471610d Mon Sep 17 00:00:00 2001 From: Andy Gilliom Date: Fri, 11 Sep 2026 14:23:59 -0500 Subject: [PATCH] feat: add SKILL.md (v1.0.0) --- SKILL.md | 269 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 SKILL.md diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..9fd3c7b --- /dev/null +++ b/SKILL.md @@ -0,0 +1,269 @@ +--- +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","=",]], + 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.