Costs as of the sale, corrected without rewriting history.

Every cost is a dated row in an append-only ledger, by item, product, offer or workspace. A correction recosts the past orders it touches, and the as-recorded view still shows what each order was booked with.

Measure: know what each option earnedIncludes parts not in production yet

Cost kinds and grains

In production

Shopify keeps one unit cost per variant and no history. Offer Suite keeps the cost of goods beside every cost Shopify has no field for (3PL fees, labels, inserts, packaging, freight, payment processing), each under a kind with a grain. The grain stops a cost being counted twice: a per-parcel kind on a three-pack is counted once, a per-unit kind three times.

Until you add your own amount for a kind, the profile's default applies, in forecast and realized profit alike. Freight and shipping boxes have no estimate yet: they count only once your agent posts an order's actual cost.

The dtc-us cost profile, read from the engine
Cost kindKeyCountedDefaultRules
Cost of goodsunit_costPer unitNoneEvery order
3PL base3pl_basePer order$1.18Every order
Extra pick3pl_extra_pickPer unit$0.20Each unit after the first in an order
Shipping label3pl_labelPer order$0.10Every order
Insert card3pl_insertPer order$0.05First orders only
FreightfreightPer parcelNoneNo estimate yet: counted only from actual costs your agent posts
Shipping boxpackaging_boxPer parcelNoneNo estimate yet: counted only from actual costs your agent postsNot charged when the unit cost already includes packaging
Poly bagpackaging_plasticPer unitNoneNot charged when the unit cost already includes packaging
Payment processingpayment_processingPercent of revenue with shipping, after discounts, before tax2.9%Replaced by the order's actual Shopify Payments fee once Shopify reports it (partly available)
Processing per orderpayment_processing_fixedPer order$0.30Replaced by the order's actual Shopify Payments fee once Shopify reports it (partly available)
The dtc-us cost profile and its defaults as the margin engine exports them, named with the dashboard's labels. Real product data, not a specimen; today it is the only cost profile.

Dated rows, voids, lookup

In production

A cost is a row with an amount and the date it takes effect, which can be in the past. Rows are appended, never edited: a correction is a new row for the same date, and the ledger keeps both, each with when it was recorded.

TargetsA row costs one item (a SKU), a product, an offer or the whole workspace.
LookupAn order line's cost falls back from its item, to the item's kit components, to the product, to the workspace.
Offer rowsAn offer-level row is per order, per parcel or a percent, never per unit.
VoidsA void is final. A read as of an earlier time still sees the voided row, because it stood then.
As ofThe API and MCP read the ledger and the effective costs as they were recorded at any past time (asOf) and as they applied on any date (at).
CSV importAll or nothing: one bad row refuses the file, unless the import is sent with partial=1.
Scope and baseThrough the API and MCP, a kind can be limited to every, first, renewal, one-time or subscription orders, and a percent kind can take revenue with shipping, product revenue or unit cost as its base.

The dashboard's Costs page adds rows, voids them, imports a CSV and seeds from Shopify.

Restatements and two views

In production

Every write that moves a past cost (a row, a void, a CSV import, a Shopify seed, a kind change or a variant re-mapped to another item) records a restatement and recosts the affected past orders in the current view. The as-recorded view keeps the costs each order was booked with. Every realized read in the API, MCP and Sidekick takes the view to use.

Cost ledger

Amounts by kind and product over valid time; corrections and backdated rows marked

CorrectedBackdatedVoided
Cost of goodsSingle Bag
$12.00
Cost of goodsStarter Kit
$17.50
Cost of goodsRitual Set
$20.00
PackagingStarter Kit
$0.50
Gift fulfilmentall SKUs
$0.90
3PL baseall SKUs
$1.18
A cost ledger over valid time, drawn with the dashboard's timeline component on specimen data: what applied when, which rows were corrected or backdated, which were voided. The live dashboard lists ledger rows; this timeline view is not in it yet.

Each restatement reports what triggered it, its scope (the items, products, offers or kinds it touches, and the date it reaches back to), how many orders it recosts, its status and the change in the workspace's realized profit since that date. The change is one figure for the whole job, not a breakdown per order.

Restatement log, newest first · specimen data
RestatementFromOrders recostedStatusProfit change
Shipping boxShipping box made requiredKind change · recorded Sep 16All dates0 of 12,410QueuedPending
Cost of goodsCSV import · 42 rowsRecorded Sep 16Aug 11,900 of 3,120RunningPending
Cost of goodsRitual set raised · $20.00 → $22.50 from Sep 13Cost row · recorded Sep 14Sep 139Completed−$22.50
All kindsManual restatement from Jun 1Manual re-run · recorded Sep 10Jun 1300 of 8,900FailedClickHouse insert timed out on batch 4None
Cost of goodsSingle bag backdated · $12.00 from May 20; was missingCost row · recorded Jul 10May 20700Completed−$8,400.00
PackagingStarter kit voided · $0.60 entered on the wrong SKU; $0.50 from Jun 1Void · recorded Jun 3Jun 140Completed+$4.00
Cost of goodsStarter kit corrected · $17.00 → $17.50 from May 20Cost row · recorded Jun 2May 20260Completed−$130.00
The restatement log as the API serves it and the dashboard's Costs page lists it. Specimen data.

A restatement that fails stays listed with its error and can be run again. The realized mirror is revisioned against the orders it copies, and a sweep every 5 minutes re-mirrors any order left behind and resends any restatement that was not sent.

Items, kits and catalog health

In production
  • Costs attach to an item, the physical SKU, so one row covers every variant that maps to it. A variant can be re-pointed to another item through the API or MCP, and its past orders are restated.
  • A kit is an item with a dated bill of materials, nesting up to 8 levels and refusing cycles. For a per-unit kind it has no row of its own for, a kit costs the sum of its components at the order's date; a recipe set from a date never recosts orders placed before that date.
  • Shopify fixed-bundle orders are matched to a kit only on an exact match: the same parts, a whole multiple of one kit. Anything else counts as its parts, never a guess.
  • GET /items/health runs eight checks, each listing its offenders, among them variants with no SKU, SKUs whose variants had different Shopify costs, reused SKUs, items or kit parts with no cost, unmapped order lines, orders missing a required cost kind and recent mapping changes.
  • The same report gives the share of recent realized revenue on fully costed orders. The dashboard's Costs page shows that share and the uncosted items.

From Shopify

In production
  • One call seeds a cost-of-goods row per item from Shopify's unit costs, only where the amount changed, and can backdate them. Variants with no cost are skipped, not zeroed. When variants sharing a SKU carry different costs, nothing is written for that item and the conflict is reported, never guessed.
  • Shopify overwrites unit costs and keeps no history, so each change it reports (a unit cost, a SKU, a new unmapped variant) becomes a dated proposal in an inbox you accept or ignore through the API, MCP or the dashboard's Costs page. Accepting appends a ledger row and restates.
  • Auto-accept takes plain unit-cost changes only: never a cleared cost, a cost that conflicts with another variant of the item, or a SKU or new-variant proposal.

Actual costs replace estimates

In production

Your agent, or a script relaying your 3PL's or carrier's invoices, posts each order's actual shipping and fulfilment costs to POST /orders/actual-costs (the MCP tool record_actual_costs), up to 500 rows a call. Each actual replaces that kind's estimate in realized profit, in both views.

  • Rows are append-only and idempotent by sourceRef: a credit is a negative row under its own reference, and a conflicting resend is refused.
  • Per-parcel kinds are charged once per non-cancelled Shopify fulfilment, at least once per order.
  • GET /orders/costs (get_order_costs) returns one order's estimate, actual and variance by kind, as the API computes them.

Payment processing

Partly available

The actual-fee read has not yet run on a live Shopify Payments order.

Payment processing is a cost in both forecast and realized profit. The estimate is 2.9% of revenue as charged (lines after discounts, plus shipping, before tax) plus $0.30 per order, and either amount can be changed through the costs API.

On Shopify Payments orders that arrive by webhook, Offer Suite reads the actual fee from Shopify for up to about 12 hours; once it is known, it replaces the estimate on that order in realized profit. Other gateways, imported orders and orders whose fee never appears keep the estimate, and the forecast always uses it.

Lines added after the sale

On the dev API, not in production yet

Once an order is paid, a line that a post-purchase upsell or any other order edit adds is stored as its own line of the order. It is costed from the ledger at the order's date, counted in that order's offer and option contribution in both views, and its refunds net against it.

A quantity change to a line the order was placed with stays an uncosted adjustment, shown apart from contribution. Upsell profit is not yet reported apart from the front-end sale.

Settings

In production
defaultCostProfileThe cost profile for orders no offer claims. Today dtc-us is the only one; null leaves those orders uncosted.
autoAcceptShopifyCostsAccepts Shopify unit-cost proposals automatically, but only while Shopify is the unit cost's canonical source.
unitCostIncludesWhat the Shopify unit cost already includes (packaging, inbound freight), so those kinds are not charged twice. Changing it restates the current view.

Set through the API (GET and PATCH /tenant) or the MCP tools get_tenant and update_tenant.

What it does not do yet

  • No built-in 3PL or carrier connector: actual costs arrive from your agent or a script.
  • The dashboard has no as-of view and no controls for a kind's order scope, percent base or unit-cost inclusions; those are API and MCP only.
  • The dashboard's Shopify seed shows only counts, not the conflicts it refused, and the dashboard and App Home have no settings page.
  • Variance between estimated and actual costs is shown per order, not summed across offers.

Questions

What happens to last month's profit when I fix a cost?

The correction is a new dated row. Offer Suite records a restatement, recosts every order it reaches in the current view and reports how much realized profit moved. The as-recorded view still shows each order with the costs it was booked with, so both figures stay available.

How do I know a profit figure is fully costed?

The items health report lists items, kit parts and orders without a required cost, and gives the share of recent realized revenue on fully costed orders. The dashboard's Costs page shows the same share. Fully costed counts the profile's default amounts (3PL fees, label, insert, payment processing) for any kind you have not set yourself, so set your own to make the figure yours.

Can it read my Shopify unit costs?

Yes. A seed writes one cost-of-goods row per item from Shopify's unit costs, and every later change Shopify reports arrives as a dated proposal you accept or ignore. Conflicting costs on one SKU are reported, never averaged.

Are payment fees an estimate?

On Shopify Payments orders that arrive by webhook, the actual fee replaces the estimate once Shopify reports it. Otherwise the fee is estimated at 2.9% plus $0.30 per order, which you can change. The actual-fee read has not yet run on a live Shopify Payments order.