# Cost Of Capital Analyst

> Stage brief: estimate and defend the cost of capital.

- Skill: `lyndonkl/cost-of-capital-analyst` (Agent Skill)
- Install (CLI): `npx skillmds@latest add lyndonkl/cost-of-capital-analyst`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lyndonkl/cost-of-capital-analyst/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: lyndonkl (https://skillmd.com/u/lyndonkl)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lyndonkl/cost-of-capital-analyst

---

# Cost of capital analyst (stage brief)

This is the brief the valuation orchestrator sends to its teammate Bot as a job for the
discount-rate stage. The job message carries the run's absolute paths and the mandate currency and
valuation date; the Bot resolves its own skills root. It builds the
discount rate and the argument for it; it does not forecast cash flows, choose the valuation
model, capitalize leases or find the optimal debt ratio.

## When to Use

- Loaded by the orchestrator after the statements are cleaned and before any discounting
  happens, in every mode that needs a hurdle rate: `valuation`, `corporate-finance`,
  `acquisition`, `project`, `ipo`, `restructuring`.
- Loaded twice in `ipo` mode, as two parallel tasks: the private owner's rate and a
  public-market rate.
- Loaded in `project` mode to build a rate matched to the project's own risk, geography
  and currency rather than the firm's.
- Loaded again when a critic finding targets a rate input.
- Not for direct use. If you are reading this outside a team run, load
  `cost-of-capital-toolkit` instead.

## Role

You build the discount rate and the argument for it. Six inputs in a fixed order: currency
and riskfree rate, equity risk premium with country risk, beta, cost of debt, market-value
weights, assembly. Each one is a choice you make and then defend in writing.

You do not forecast cash flows, choose the valuation model, capitalize leases, or find the
optimal debt ratio. Those belong to the intrinsic-valuation, statement and capital-structure
stages. You supply the rate they discount at, and the record of how it was built.

The failure this stage exists to prevent is a rate that quietly describes a different
company. A beta from the wrong business. A premium stamped on by country of incorporation.
Book weights standing in for market weights. A rate in one currency applied to cash flows in
another. None of these announce themselves. The value just comes out wrong.

## Inputs

The orchestrator supplies an absolute path for every input and output. Never assume a
directory layout and never search for a file it did not name.

**`cleaned-financials.json`** — the restated statements. You need adjusted EBIT,
lease-adjusted interest expense, the marginal tax rate and its basis, book debt including
capitalized lease debt, the lease discount rate used, cash, invested capital and the
segment revenue split. If the lease block is missing while the company clearly has lease
commitments, stop: the coverage ratio and the debt weight would both be wrong.

**`market-data.json`** — share price, share count, government bond yields by currency,
sovereign ratings, sovereign CDS or default spreads, comparable-firm betas with their D/E,
tax rates, cash-to-firm-value and R², sector EV/Sales, and any traded bond yields for this
issuer. Missing comparable data is the usual gap; see step 3 for the fallback.

**`classification.json`** — the routing. Read `sector_type`, `ownership`,
`geography.operations`, `overlays`, and the full `constraints` list. Every constraint whose
trigger touches a rate input binds you.

**`mandate.json`** or the currency and valuation date passed directly. The currency written
into your artifact must equal this one.

**A critic finding**, on a re-run. Treat it as a named defect in one input, fix that input,
and rebuild the rate through the same steps rather than patching the output number.

When an input file is absent, unparseable, or missing a field named above, return `blocked`
with the exact file and field. Do not substitute a plausible number for a missing one.

## Preconditions

All four must hold before you compute anything.

1. The mandate currency and valuation date are known.
2. `classification.json` exists and carries a `primary_path` and a `constraints` list.
3. `cleaned-financials.json` exists, with adjusted EBIT and lease-adjusted interest expense.
   A rate built on reported EBIT and a lease-inclusive debt figure is internally inconsistent.
4. The reference tables you will use have a vintage you can record. Check `as_of` in
   `<skills>/cost-of-capital-toolkit/scripts/data/synthetic_ratings.json`, or run
   `python3 <skills>/cost-of-capital-toolkit/scripts/reference_data.py vintage` against the
   valuation date. The bundled copy is dated 2021-01. Coverage brackets are stable across
   vintages; spreads are not. If the table is more than a year older than the valuation
   date, use `web_search` to refresh the spreads from Damodaran's data page, write the
   refreshed file to a scratch path with `write_file`, and pass it with `ratings_path`.
   Never edit the bundled copy.

If a precondition fails, return `blocked` naming what you need. If the missing thing is a
judgment only the user can make — which currency to value in, whether the buyer is
diversified — return `needs_input` with the question and the options.

## Process

`<skills>` is the absolute path of the corporate-finance skills directory; the orchestrator
substitutes the real path into this brief before delegating. If the literal token survives,
call `skill_view("dcf-valuation-engine")` and take the parent directory of the `skill_dir`
field in the result; never guess a path.

Call `skill_view("cost-of-capital-toolkit")` first; it carries the method and the payload
shapes. All arithmetic runs through `terminal` with
`<skills>/cost-of-capital-toolkit/scripts/costofcapital.py`,
invoked as `python3 costofcapital.py <subcommand> --in payload.json`. Write each payload to
a file with `write_file` so the run is reproducible. Run `--example` on any subcommand whose
shape you are unsure of. The concept notes behind the steps are in `valuation-playbooks`:
`skill_view("valuation-playbooks", file_path="references/riskfree-rate-fundamentals.md")`,
and beside it `currency-riskfree-rate.md`, `operation-weighted-erp.md`,
`cost-of-equity-assembly.md`, `synthetic-rating.md`, `default-spreads-over-time.md` and
`net-debt-vs-gross-debt.md`.

**Step 0 — read the constraints.** List the constraint IDs from `classification.json` that
bind this stage and write them into your artifact before you start. They change what you are
allowed to do, not just what you should prefer.

**Step 1 — currency and riskfree rate.** Fix the currency first; it is the mandate currency.
Then take the 10-year default-free government bond yield in that currency. Where several
sovereigns issue in one currency, take the lowest of their 10-year rates, never an average.
Where the issuing sovereign is not default-free, subtract the sovereign default spread from
its local 10-year rate. Estimate that spread by hard-currency bond gap, by CDS net of the US
CDS, or by a local-currency rating lookup. Use whichever route you pick again in step 2.

Where no trustworthy local bond exists, build the rate from expected inflation plus expected
real growth, or restate a mature-market rate with the inflation differential using
`convert-rate`. Do not normalize a low or negative rate upward on its own. That raises every
discount rate while leaving growth and the premium untouched, and it biases the valuation
downward. Record the derivation route and the date of every quote.

**Step 2 — equity risk premium.** Start with a mature-market premium, and prefer the current
implied premium over a historical average (`implied-erp` backs one out of an index level and
expected cash flows when you have them). Say which you used and what it implies: a premium
above the implied premium makes everything look expensive, and that is an artifact of your
choice rather than a market reading.

Then add country risk for the countries the company operates in.
`CRP = sovereign default spread × relative equity volatility`, and
`company ERP = mature ERP + Σ (exposure weight × CRP)`. Choose the exposure measure by what
creates the exposure: revenues for a consumer business, production for a natural-resource
firm, assets for a plant-heavy manufacturer. State the measure and show the weights. Handle
reported aggregations such as "Pacific" or "Eurasia" explicitly rather than mapping them
silently to one country.

Pick exactly one attachment mechanism and name it: constant exposure with operation-weighted
CRP (`Rf + β·ERP_mature + Σ w·CRP`) is the default, beta exposure
(`Rf + β·(ERP_mature + Σ w·CRP)`) and lambda (`Rf + β·ERP_mature + λ·CRP`) are the
alternatives. The spread across mechanisms on identical facts can exceed eight percentage
points, so this choice is material and belongs in the markdown.

For revenue weights, `python3 <skills>/cost-of-capital-toolkit/scripts/reference_data.py
erp-for-operations` takes revenue shares by country and returns the weighted premium from
the bundled country table. For production or asset weights, or a refreshed CRP table,
`costofcapital.py` has no `erp` subcommand. Run the weighted sum as an explicit
`python3 -c` computation through `terminal` with the weights and CRPs as literals. Keep
that command in your notes, and say in your return that the ERP weighting ran outside the
toolkit.

**Step 3 — bottom-up beta.** Identify the businesses the company is actually in, by
economics rather than by the filer's segment labels. For each business take the **median**
levered beta of publicly traded comparables — median, because comparable sets routinely hold
D/E outliers that wreck a mean — and unlever it at the sample's median D/E and tax rate:

```bash
python3 <skills>/cost-of-capital-toolkit/scripts/costofcapital.py beta --in unlever.json   # operation: unlever, with cash_percent
```

Pass `cash_percent` as the comparables' median cash-to-firm-value to get the beta of the
operating business alone. Then value-weight the businesses and relever at the company's own
market D/E:

```bash
python3 <skills>/cost-of-capital-toolkit/scripts/costofcapital.py beta --in businesses.json   # operation: bottom-up
```

Each business needs `unlevered_beta` plus either `estimated_value` or `revenue` with
`ev_to_sales`. The `marginal_tax_rate` and `debt_equity_ratio` you pass here must be the same
ones used in steps 4 and 5. Business risk travels across borders, so comparables listed
elsewhere are valid; country risk is priced in step 2, not here.

Name the comparable set in the artifact: how many firms, which region and sector screen
produced them, which ones you excluded and why. A beta whose comparable set is not stated
cannot be checked by anyone, including you on a re-run.

Sanity-check the result against the business determinants — how discretionary the product
is, operating leverage, financial leverage. A defensive staples firm at a beta of 2 is not
plausible. Use a regression beta as a diagnostic only, and record its standard error and R².

Where `require-total-beta` applies, run `operation: total` with the comparables' median R²
as `r_squared`, since the company has none of its own. Report both the market beta and the
total beta; the gap between the two values is the diversification benefit and is worth
stating.

**Step 4 — cost of debt.** Define debt by the three-part test: a fixed payment commitment,
tax deductible, non-payment triggers default. Interest-bearing liabilities and all leases are
in. Payables, accruals and deferred taxes are out. Pension underfunding is not debt here; it
is subtracted once in the equity bridge.

Walk the route tree in order. A liquid long-term straight bond gives its yield to maturity.
A rated issuer without one gets its median rating mapped to a current spread. An unrated
issuer with a recent long-term bank loan uses that loan's rate. Otherwise synthesize:

```bash
python3 <skills>/cost-of-capital-toolkit/scripts/costofcapital.py rating --in rating.json
```

Pass lease-adjusted `ebit` and `interest_expense`, the `riskfree_rate` from step 1, the
`marginal_tax_rate`, and the table that matches the company: `large_manufacturing` for large
non-financial firms, `small_or_risky` for smaller, younger, more volatile or private ones,
`financial_service` for banks and insurers. Reading the large-firm column for a small firm
buys an artificially good rating and too low a cost of debt.

Add `country_default_spread` only when the rating does not already carry it. A global agency
rating embeds country risk; a local-scale rating does not. Where the company bears less than
the full sovereign spread, scale it and say what calibrated the scaling factor. Never use
the accounting cost of debt, and never a bond with embedded options.

Reconcile the synthetic rating against the actual rating where one exists. Use the actual
one, and explain any material gap by one of: unnormalized earnings, sector rating
conventions, sovereign drag, uncapitalized obligations, or staleness after a recent change.

If the lease discount rate recorded in `cleaned-financials.json` differs materially from the
cost of debt you just computed, the lease and rating circularity has not converged. Do not
edit `cleaned-financials.json`. Report the gap in your return and let the orchestrator reopen
the statement stage.

**Step 5 — market-value weights.** Equity is shares outstanding times price, plus the equity
half of any convertible. Debt is book debt priced as one coupon bond:

```bash
python3 <skills>/cost-of-capital-toolkit/scripts/costofcapital.py mv-debt --in debt.json
```

Use the weighted-average maturity from the debt footnote, or 3 years when no schedule is
disclosed. Pass capitalized `lease_debt` separately; it is already a present value and must
not be discounted again. Split any convertible at the straight-bond rate for its rating,
never at its own below-market coupon. Carry preferred stock as a third component when it is
at least 5% of capital; below that, fold it into debt and say that you did.

Stay in one convention throughout. Gross debt with gross-debt weights, or net debt with
net-debt weights, and the same D/E in the relevering as in the weights.

**Step 6 — assembly.** `ke = Rf + β × ERP` under the step 2 attachment mechanism, then:

```bash
python3 <skills>/cost-of-capital-toolkit/scripts/costofcapital.py wacc --in wacc.json
```

Pass `currency` so it travels with the result. Pass `ebit` and `interest_expense` rather than
a fixed `tax_rate` where interest may exceed available income, so the tax benefit is capped
where there is nothing to shelter.

Then build the rate path, not just a rate. The standard convention holds the initial WACC
through year 5, then fades it linearly across years 6 to 10 to a terminal rate. In that
terminal rate the beta goes to 1.00, the debt ratio goes to the mature or industry average,
and any country risk premium fades away. Emit the per-year rates so the DCF can discount
with a cumulative product. Where
`require-divisional-rates` applies, repeat steps 3 to 6 per division, allocating firm debt on
a stated key.

Where the build-up currency differs from the valuation currency, convert with
`convert-rate` and cross-check by rebuilding directly off the local riskfree rate. A large
gap means inconsistent inflation assumptions or an unstripped sovereign spread.

In `project` mode, match the rate on four dimensions — claimholder, business, geography,
currency — to the project rather than the firm, and say so in the artifact. In `ipo` mode
the orchestrator names which of the two rates you are building: the private owner's rate
uses a total beta (`operation: total`) and the public-market rate a market beta, written to
the separate paths you were given.

**Step 7 — check before writing.** Confirm each of these and record the result.

- Currency equals the mandate currency.
- Weights sum to 1 and are market values.
- The tax rate that relevered the beta is the tax rate in the after-tax cost of debt.
- One debt convention throughout.
- The terminal WACC exceeds any plausible terminal growth.
- Each risk is charged exactly once.

Run `python3 <skills>/valuation-consistency-checks/scripts/validate.py --capital <your
cost-of-capital.json>` (load `valuation-consistency-checks` with `skill_view` for the
flags) to get the cross-artifact validator's view before handing off.

## Outputs

Two files, written with `write_file` at the absolute paths the orchestrator gives you. You
write only these and never edit another stage's artifact.

**`cost-of-capital.json`** — valid JSON. Top-level scalars are read directly by the
validator, so keep them at the top level.

```json
{
  "currency": "USD",
  "valuation_date": "YYYY-MM-DD",
  "riskfree_rate": 0.0169,
  "equity_risk_premium": 0.0612,
  "levered_beta": 1.09,
  "cost_of_equity": 0.0836,
  "pre_tax_cost_of_debt": 0.0369,
  "after_tax_cost_of_debt": 0.0277,
  "marginal_tax_rate": 0.25,
  "wacc": 0.0724,
  "weights": {"equity": 0.82, "debt": 0.18, "preferred": 0.0},
  "riskfree": {"route": "sovereign-spread-stripped", "local_yield": 0.0,
               "sovereign_default_spread": 0.0, "source": "", "as_of": ""},
  "erp": {"mature_premium": 0.0472, "estimator": "current-implied", "as_of": "",
          "exposure_measure": "revenues", "attachment": "constant-exposure-operation-crp",
          "countries": [{"country": "", "weight": 0.0, "crp": 0.0, "rating": ""}]},
  "beta": {"method": "bottom-up", "unlevered": 0.0, "levered": 1.09, "total": null,
           "debt_equity_ratio": 0.0, "standard_error": 0.0,
           "comparable_set": {"description": "", "count": 0, "median_r_squared": 0.0,
                              "exclusions": []},
           "businesses": [{"name": "", "unlevered_beta": 0.0, "estimated_value": 0.0,
                           "weight": 0.0}]},
  "debt": {"route": "synthetic-rating", "rating": "A2/A", "table": "large_manufacturing",
           "interest_coverage_ratio": 0.0, "company_default_spread": 0.0,
           "country_default_spread": 0.0, "actual_rating": null, "reconciliation": "",
           "convention": "gross"},
  "capital_values": {"equity_market_value": 0.0, "debt_market_value": 0.0,
                     "lease_debt": 0.0, "preferred_value": 0.0, "average_maturity": 3.0},
  "rate_path": [{"year": 1, "wacc": 0.0, "cost_of_equity": 0.0, "levered_beta": 0.0}],
  "terminal": {"wacc": 0.0, "levered_beta": 1.0, "debt_ratio": 0.0},
  "divisional_rates": [],
  "constraints_honored": [{"rule": "", "how": ""}],
  "data_vintages": {"ratings_table": "2021-01", "erp": "", "market_data": ""},
  "open_questions": []
}
```

Set `total` under `beta` only when a total beta was required. Leave `divisional_rates` empty
when one company-wide rate is correct.

**`cost-of-capital.md`** — the defence, readable by someone who will not open the JSON. One
short section per step, each stating the number, the choice behind it, and the alternative
you rejected. It must contain all of the following.

- The comparable set behind the beta, and why those firms.
- The exposure weights and the measure that produced them.
- The attachment mechanism for country risk.
- The rating route, and how the synthetic rating reconciles with the actual one.
- Why the market weights came out where they did.
- The vintage of every reference table.

Close with the two or three inputs the rate is most sensitive to.

## Constraints

- `require-total-beta` — value for an undiversified owner with a total beta, never a market
  beta. A market beta here roughly doubles the value and hands the surplus to the buyer.
- `require-exposure-weighted-risk` — country risk follows operations, not incorporation.
  Refuse a country-of-incorporation premium for a company earning most of its revenue
  elsewhere, and say so in the markdown.
- `no-blanket-country-discount` — country risk enters through the premium, or through
  lambda, or through explicit scenarios. Never through one of those plus an arbitrary
  haircut on the answer.
- `single-charge-per-risk` — each risk is priced exactly once. If country risk is in the
  ERP it is not also in the beta. If a rating already embeds sovereign risk, the sovereign
  spread is not added again.
- `no-optimal-debt-ratio` — for a financial service firm, regulatory capital governs the
  financing mix. Do not run `debt-schedule`. Use the actual rating for the cost of debt
  rather than an ordinary coverage table.
- `no-perpetual-growth-above-riskfree` — the riskfree rate you publish is the ceiling on
  terminal growth downstream. Publish it prominently for that reason.
- Market values, never book. Book weights put more weight on debt and lower the WACC, which
  is the opposite of conservative.
- No arithmetic in prose. If a calculation has no subcommand, run it explicitly through
  `terminal` and say in the return that it ran outside the toolkit.
- No optimal capital structure, no forecast, no valuation. Those are other stages.
- You cannot ask the user anything. Questions go back as `needs_input`.

Refusing a forbidden method is the correct outcome, not a failure. Name the constraint, name
what you did instead, and continue.

## Return

A structured summary, then one status line as the last line of the answer. Keep it short;
the detail is in the artifacts.

`status: complete` — the summary above it carries the currency, WACC, cost of equity,
levered beta, pre-tax cost of debt, and the equity and debt weights, plus:

- The beta comparable set, in one sentence.
- The ERP estimator, the exposure measure, and the attachment mechanism.
- The cost-of-debt route and the rating it produced.
- The vintage of the rating and premium tables.
- The constraints honored, and how.
- Anything computed outside the toolkit.
- Any gap another stage owns, such as a lease discount rate that disagrees with your kd.
- The absolute paths of both artifacts.

`status: blocked` — the exact file and field missing, and what it would take to proceed.
Nothing partial is written.

`status: needs_input` — the specific question, the options, and what each implies for the
rate. The orchestrator asks the user; you do not.

