Cost engine¶
All cost arithmetic lives in src/cost/. It follows the AWS billing-and-cost-management skill: never reason about money, run code; public on-demand rates only; 730 hours per month; never invent a price.
| File | Role |
|---|---|
pricefile.mjs |
Ingests Price List bulk files into compact rate rows; scanLarge is a byte-level scanner for files too big for JSON.parse (EC2 is ~460 MB). |
pricebook.mjs |
loadPriceBook(region); dim(), tryDim(), tiered(), round4(), money(), HOURS_PER_MONTH, PricingError. |
pricers.mjs |
One pricer per service; SERVICE_PRICER (catalog id → pricer), NO_CHARGE, Bedrock model matching, AgentCore component detection. |
estimate.mjs |
estimateCost(diagram, options): runs pricers, totals, sensitivity, what-ifs, provenance. |
Price book (data/prices/<region>.json)¶
{ "region": "us-east-1", "retrievedAt": "…",
"services": { "AWSLambda": { "publicationDate": "…", "rows": [ { "sku": "…", "u": "Request", "unit": "Requests", "usd": "0.0000002", "b": 0, "e": null, "fam": "…", "d": "…", "a": { /* attributes */ } } ] } } }
u is the usage type with the region prefix stripped; b/e are tier begin/end. dim(code, predicate, what) returns the tier rows for one dimension and throws if the predicate matches SKUs with different rates (several SKUs with identical rates — a "legacy" and a "current" listing — are fine). A silently wrong SKU is worse than "not estimated".
A pricer¶
lambda: {
label: "AWS Lambda",
run({ pb, u, vol }) {
const req = vol("requestsPerMonth", 1e6), ms = u("avgDurationMs", 200), mem = u("memoryMb", 512);
const rate = pb.dim("AWSLambda", (x) => x.u === "Request", "Lambda requests");
return { lines: [ line("Requests", rate, req) ] };
},
}
u(key, default)readsnode.usage[key], records it as spec or default, and returns it.vol(key, default)is the same for volumes that scale with traffic (sensitivity multiplies these; fixed capacity such as instance counts usesu).line(label, rows, qty, opts)prices through tiers and recordsqty,unit,rate,sku,usagetype.- Return
{ lines, notes?, notItemized? }; throwPricingErrorfor anything that cannot be priced without guessing.estimate.mjsturns that intostatus: "error"orneeds-input. - Optional
whatIf({ pb, usage, base })returns an alternative with a delta (see Lambda arm64, RDS Multi-AZ, Bedrock batch, S3-IA).
Adding a service¶
- Fetch its rates: add the service code and a narrow filter to
SERVICESinscripts/fetch-prices.mjs(keep the snapshot small), thennode scripts/fetch-prices.mjs --services <Code>. - Write the pricer in
pricers.mjsand map the catalog id inSERVICE_PRICER; add aCATEGORYentry inestimate.mjs. - Add its usage keys to the user guide table (
docs/user-guide/cost.md) and to theusagedescription inschemas.mjsif keys are validated. - Add a test that reads the rate from the price book itself (never hard-codes a price) and checks the formula — see the Lambda test.
- Verify one number by hand against the AWS pricing page.
Bedrock model matching¶
findBedrockModel(pb, text) normalises names (case, punctuation, "(Amazon Bedrock Edition)", embeddings → embedding) and requires every query token to be present. An exact match wins; several partial matches are an error that lists candidates. Titan rows have no model attribute, so bedrockModelName(row) derives one from the usage type (TitanEmbeddingV2-Text-input-tokens → Titan Embedding V2 Text). Embedding models have no output rate, so the output line is omitted.
estimateCost result¶
{ schema_version, asOf, region, currency, scale, basis,
totals: { monthlyUsd, annualUsd, byCategory },
coverage: { nodes, estimated, override, noCharge, notBillable, notItemized, needsInput, notEstimated, error },
confidence: "indicative" | "stated", defaultedAssumptions: [ { node, key, value } ],
nodes: [ { id, label, service, status, pricer, category, monthlyUsd, lines, assumptions, notes } ],
sensitivity: [ { scale, monthlyUsd } ], whatIfs: [ … ], priceBook: { region, retrievedAt, publications } }
Totals include only estimated and override nodes. Pass asOf: new Date(…) in tests for reproducibility.
Gotchas learnt the hard way¶
- Service codes differ from marketing names (
AmazonES= OpenSearch,AmazonStates= Step Functions,AWSQueueService= SQS). - Guardrails usage types need the
…UnitsConsumedsuffix; PII usesPaidUnitsConsumed. - A
Set.add(...codes)only adds the first argument — loop. - The "scaled by traffic" flag must be set on the assumption entry even when the key already exists.