Reproduce a valuation¶
This page is the walkthrough for someone who has to put a conclusion in a report: an auditor, a diligence analyst, or anyone checking a published pWatt NAV. It assumes a command line and nothing else — no Python knowledge, no account with Suno, no credential.
What you need¶
- The snapshot. Every valuation Suno publishes carries the complete set of inputs it consumed, as a single JSON document. That document is the valuation: it contains the plant, the assumptions, the macro series, the PPA curve, the risk scores, the replacement events, the metered generation and the invoiced O&M. The engine neither knows nor cares how you obtained it.
- The engine commit. Each published valuation records the version and the commit SHA of the engine that computed it. That commit is what you install.
You do not have to unwrap anything first. Suno's public API returns the snapshot nested inside a response envelope, alongside the numbers the valuation was published with:
curl -s https://<host>/api/v1/public/valuation/projects/42/versions/7/snapshot \
> valuation.json
suno-valuation (from Step 3 onward) reads either shape, detected from the
document itself: a bare snapshot, or that envelope. Point it straight at
valuation.json — no jq '.inputs', no hand-copying outputs into
--expect-nav / --expect-npv.
Step 1 — Install the exact engine¶
Use a clean virtual environment, so that what you install is all there is.
python3 -m venv verify-env
source verify-env/bin/activate
pip install "suno-valuation-engine @ git+https://gitlab.com/suno-finance/suno-valuation-engine.git@<commit-sha>"
Install by commit SHA, not by tag. A tag is a label that a repository owner can move; a commit SHA is the content itself. If the valuation you are checking names a commit, pin that commit — then the code you ran is provably the code that was published, and not merely code with the same name.
Confirm that nothing else came along:
pip list
You should see suno-valuation-engine and pip's own packaging tooling, and
nothing else. The package declares no runtime dependencies, which is a
deliberate part of the design: nothing installed alongside the engine can
change a result, and there is no third-party code in the path between the
inputs and the number.
Step 2 — Look at the snapshot before running anything¶
Open the JSON. It is meant to be read.
python3 -m json.tool snapshot.json | less
The document declares its format in snapshot_schema, and carries, among
others:
| Block | What it holds |
|---|---|
project |
installed capacity, design production, initial efficiency, degradation, commissioning date |
assumptions |
lifetime, fee, REC price, indexation choices, withholding rate, investment amount and date |
macro |
one row per year: FX, local CPI, PPI, US risk-free rate, US inflation |
ppa |
the price curve, point by point |
risk |
each scored risk dimension feeding the discount premium |
equipment_replacements |
each replacement event with its cost build-up, probability and downtime |
opex, hist_opex_monthly |
the projected monthly O&M and the invoiced history |
hist_prod_monthly |
the metered monthly production |
capital_curve |
the capital repayment schedule behind the withholding calculation |
This is the point at which the real audit work happens. The steps below check the arithmetic; only this step checks the facts. See what reproduction proves for what each input should be cross-checked against.
Step 3 — Run the model¶
suno-valuation run snapshot.json
NPV (pre-tax, USD): 1,464,124.62
NPV (after tax, USD): ...
NAV per pWatt (USD): 1.0841352254
Currency: USD
Warnings: none
For full float precision, or to feed the result into your own tooling:
suno-valuation run snapshot.json --json
which prints one JSON object with npv_usd, npv_after_tax_usd,
pwatt_nav_usd, currency and warnings at full precision — the same bits
the engine produced, not the rounded display above.
Read the warnings. A valuation can complete and still warn that the FX rate was held flat past the forecast, that a replacement event falls in the evaluation year, or that a repayment tranche was scheduled past the end of the asset's life. A warning is not a failure, but it marks a place where the model substituted a convention for data, and each one deserves a manual look.
Step 4 — Verify against the published number¶
If your file is the backend's response envelope — the shape curl gets you
in "What you need" above, with the snapshot nested under inputs and the
published numbers under outputs — you do not need to pass anything:
suno-valuation verify valuation.json
OK: valuation.json reproduces the expected numbers
verify compares against outputs.npv_usd and outputs.pwatt_nav_usd from
the envelope itself: the exact numbers that version was published with, with
no copying required. Pass --expect-nav / --expect-npv explicitly whenever
you want to check against a different number than the one the envelope
carries — a figure from a report, say — and it always wins over the
envelope's own outputs:
suno-valuation verify snapshot.json --expect-nav 1.0841352254
OK: snapshot.json reproduces the expected numbers
(This also works on a bare snapshot, which carries no outputs of its own —
in that case, at least one --expect-* flag is required.)
You can check either headline number, or both:
suno-valuation verify snapshot.json \
--expect-npv 1464124.6218754272 \
--expect-nav 1.0841352253798053
The comparison is exact by default: the tolerance is zero, so the engine's output must equal the expected value bit for bit. That is the standard Suno holds itself to, and it is how the reproduction run reported on the overview page was scored.
If you pass a rounded figure — the ten decimal places a published report shows, rather than the full double — give the comparison a tolerance that matches the precision you typed:
suno-valuation verify snapshot.json --expect-nav 1.0841352254 --tolerance 1e-9
A mismatch prints both numbers and their difference, so a discrepancy can be sized immediately rather than merely noted.
The exit codes, and what each means for your report¶
verify is designed so that a conclusion can be scripted rather than read.
| Code | Meaning | What it supports in a report |
|---|---|---|
0 |
Reproduced. The engine's output matched the expected numbers within the tolerance you set. | The published figure follows from the published inputs under the published model. It does not attest to the inputs. |
1 |
Mismatch. The snapshot loaded and the model ran, but the result differs. | A finding. The difference is printed; the arithmetic, the inputs and the expected value are all available to locate it. Raise it. |
2 |
Cannot be evaluated. The file is missing, is not JSON, is malformed, declares an unsupported schema, carries inputs the engine's validation refuses, carries no published numbers to compare against with no --expect-* flag given, or is an envelope the backend itself marked reproducible: false. |
Not a pass and not a discrepancy — no comparison took place. The message states which of those it was, including the backend's own not_reproducible_reason when that is why. |
In a shell:
suno-valuation verify snapshot.json --expect-nav "$NAV"
case $? in
0) echo "reproduced" ;;
1) echo "MISMATCH - investigate" ;;
2) echo "could not be evaluated - see message" ;;
esac
Snapshots that cannot be reproduced¶
The loader reads snapshot schemas 14 and 15 and refuses everything else by
number, with exit code 2.
This is deliberate, and it costs Suno three of its own published valuations.
Those three belong to one project, were published in July 2026 under schemas 3
and 5, and predate this package. Reading them with today's model would mean
supplying values for wht_rate, opex_real_escalation and ppa_base_year,
which those snapshots do not carry, and interpreting each macro year's sofr
field as if it were US inflation, which it is not. The output would be a
number that reproduces on demand and that was never published. Suno would
rather label three valuations non-reproducible than publish a fabricated
agreement. All three are superseded by later versions of the same project,
which do reproduce exactly.
If you are checking one of those three, exit code 2 is the correct and
expected outcome, and the honest line in a report is that the version is not
reproducible from its snapshot — not that it failed verification.
Doing it from Python instead¶
The CLI is a thin shell over two public functions, and an analyst who wants the full year-by-year table rather than the headline numbers should use them directly:
import json
from suno_valuation import load_inputs_snapshot, run_valuation
with open("snapshot.json") as fh:
snapshot = json.load(fh)
result = run_valuation(load_inputs_snapshot(snapshot))
print(result.npv_usd, result.pwatt_nav_usd)
for row in result.yearly_table:
print(row.year, row.generation_kwh, row.net_cashflow_usd, row.discounted_cashflow_usd)
Every headline number is reconcilable from that table by hand: npv_usd is the
sum of discounted_cashflow_usd, npv_after_tax_usd the sum of
discounted_net_investor_cashflow_usd, and pwatt_nav_usd the latter divided
by kwp × 1,000. See the API reference for the full surface and the
methodology for what each column means.
If the numbers do not match¶
Report it. A discrepancy in a published valuation is the most valuable thing this repository can receive, and the process does not require you to identify the cause: open an issue with the Calculation defect template at https://gitlab.com/suno-finance/suno-valuation-engine/-/issues, attaching the snapshot, the expected number, the number you obtained, and the engine commit you installed.
Before doing so, two checks worth a minute:
- confirm you installed the commit the valuation names, not the latest code — the model changes over time, and an old valuation is only reproducible with the engine that computed it;
- confirm the expected value you passed is the full published figure and not a
rounded copy, or set
--toleranceaccordingly.