Pricing

Pricing - Start Here

Automate pricing for any manufacturing process in Phasio using TypeScript equations that run the moment a part is uploaded.

Phasio lets you automate pricing for any manufacturing process using TypeScript equations. When your team or customers upload parts to quote or order, your equations run automatically to calculate prices, manufacturing durations, and custom line items with no manual quoting needed.

How pricing works in Phasio

Manufacturing pricing pipeline showing the three levels: Level 1 Process runs per part line, Level 2 Post-Process runs per part line when selected, and Level 3 Order-Level runs once per order
┌──────────────────────────────────────────────────────────────┐
│  LEVEL 1 · PROCESS                       🚪 gate available     │
│  Runs once per part line                                       │
│  Input: all part and specification information                 │
│  → done(unitPrice, duration, reviewRequired)                   │
└──────────────────────────────────────────────────────────────┘
                   │  unit price & result flows down

┌──────────────────────────────────────────────────────────────┐
│  LEVEL 2 · POST-PROCESS                  🚪 gate available     │
│  Runs per part line (only if a post-process is selected)       │
│  Input: specification, requisition, processPricing.price       │
│  → done(unitPrice, duration, reviewRequired)                   │
└──────────────────────────────────────────────────────────────┘
                   │  all part prices + subtotal flow down

┌──────────────────────────────────────────────────────────────┐
│  LEVEL 3 · ORDER-LEVEL                                         │
│  Runs once per order                                           │
│  Input: parts[], subtotal                                      │
│  → addLineItem({ name, price })                                │
│  No done() call → nowhere to attach a gate                     │
└──────────────────────────────────────────────────────────────┘

Final price = (Level 1 unit price + Level 2 unit price) × quantity
              + Level 3 line items

CRITICAL: done() sets the UNIT price. Phasio multiplies by quantity automatically.
Never multiply by quantity inside Level 1 or Level 2.

Optional

Watch the 2-minute overview of the pricing pipeline. (video coming soon)

The variable() override

At levels 1 and 2, wrap any value in variable() to expose it in the Phasio UI. This allows you to run semi-automatic equations as you would in Excel. Your sales team can understand why and how a price is what it is without touching the equation. Think of it as a named cell in Excel that your equation references, but that a human can change when needed.

// Sets a default margin of 60%, overridable per order in the UI
const margin = variable('Margin', 0.60)

Get started - use AI to write your equation

The fastest path to working pricing. Copy the entire prompt below and paste it into your AI (ChatGPT, Claude, etc.). It contains everything the AI needs - no extra context required.

You are helping a manufacturer set up automated pricing in Phasio, a manufacturing
SaaS platform. Your job is to write a TypeScript pricing equation that runs
automatically when someone uploads 3D models to the Phasio account.

--- TYPE DEFINITIONS (source of truth — never invent fields or functions) ---

interface Specification {
  units: 'MILLIMETERS' | 'CENTIMETERS' | 'INCHES' | 'FEET' | 'METRES'
  width: number               // mm — LARGEST bounding box dimension
  height: number              // mm — MEDIUM bounding box dimension
  length: number              // mm — SMALLEST bounding box dimension
  // width >= height >= length always. Phasio uses the minimum bounding box from the uploaded
  // geometry. Part rotation in the viewer does NOT change these values.
  volume: number               // mm³
  area: number                 // mm² surface area
  convexHullVolume: number     // mm³ — smallest convex shape enclosing the part
  minBoundingBoxVolume: number // mm³ — tightest axis-aligned bounding box
  minWallThickness: number     // mm
  meanWallThickness: number    // mm
  maxWallThickness: number     // mm
  shrinkWrapVolume: number     // mm³ — tightest convex hull; primary metric for powder bed
  color: string | null
  precision: { name: string; value: number } | null  // e.g. layer height or tolerance
  infill: { name: string; value: number } | null     // infill density 0–1
  postProcessing: { name: string; price: number }[]  // price = Level 2 post-process unit price
  process: { technology: string }                    // e.g. 'FDM', 'SLA', 'MJF'
  material: {
    name: string
    variables: Record<string, number> // access as material.variables["name"] — NEVER dot notation
    wallThicknessWarning: number | null
    wallThicknessLimit: number | null
  }
}

interface Requisition {
  quantity: number
  leadTime: null | { name: string; buffer: number } // name = operator-defined tier, e.g. 'Economy', 'Express', 'Overnight'
}

interface Revision {
  name: string
  repaired: number    // 1 if the mesh was auto-repaired, 0 if not
  watertight: number  // 1 if the mesh is watertight (manifold), 0 if not — use to gate bad geometry
  minimumWallThickness?: number // mm — thinnest wall analysis (Pro / Factory Floor only)
  accessoryFiles: ('THUMBNAIL' | 'PDF_DESIGN_FILE' | 'WALL_THICKNESS_ANALYSIS'
                 | 'ORIGINAL_CAD_FILE' | 'CORE_GEOMETRY')[]
}

interface Customer {
  organisationId: number | null
  organisationName: string | null
  taxExempt: boolean
  isApproved: boolean
}

declare const specification: Specification
declare const requisition: Requisition
declare const revision: Revision
declare const customer: Customer | null
declare const workflow: { duration: number } // total workflow duration in hours (read-only, set by operations)

// POST-PROCESS equations only — not available at Level 1 or Level 3:
declare const processPricing: {
  price: number                     // unit price from the Level 1 process equation
  variables: Record<string, number> // values exposed by variable() in the process equation
}

--- FUNCTIONS ---

// Completes the equation and sets the unit price. A falsy, negative, or NaN
// price automatically triggers reviewRequired.
declare function done(price: number, duration?: number, reviewRequired?: boolean): void
declare function done(options: { price: number; duration?: number; reviewRequired?: boolean }): void

// Exposes a value in the operator quote UI for manual override per order or quote.
// ALWAYS operator-facing only — never visible to customers.
// Values exposed in Level 1 are accessible in Level 2 via processPricing.variables['name'].
//
// Two patterns beyond simple overrides:
//
// MODE SWITCH — use as an internal toggle to run two variants under one equation:
//   const buildMode = variable('buildMode', 1)  // default 1 = standard, operator sets 0 = direct
//   const cost = buildMode === 1 ? standardCalc : directCalc
//
// REVIEW REASON FLAG — surface why a part was flagged without a reason string:
//   const _reasonFill    = variable('REVIEW: fill ratio too low',    fillRatio < 0.15 ? 1 : 0)
//   const _reasonSurface = variable('REVIEW: surface complexity high', swvRatio < 0.30 ? 1 : 0)
//   done(price, time, fillRatio < 0.15 || swvRatio < 0.30)
//   Operator sees 1 = flag active, 0 = fine. Name the variable like a sentence.
declare const variable: (name: string, fallback: number) => number

// Tiered lookup: returns the value at the highest threshold ≤ input.
// base is returned when input is below all thresholds (default 0).
// Basic:   createBands({ 100: 0.05, 500: 0.10 }, 0)(250)  → 0.05
// Penalty: createBands({ 10: 0 }, 999)(5)                  → 999 (below minimum)
declare const createBands: (bands: Record<number, number>, base?: number) => (input: number) => number

// Use round(x, 2) for all money values to avoid floating-point noise.
declare const round: (value: number, precision?: number) => number

// Converts from mm (all specification dimensions are in mm by default).
// Use exponent=2 for area, exponent=3 for volume.
declare const useDimension: (
  dimension: 'MILLIMETERS' | 'CENTIMETERS' | 'INCHES' | 'FEET' | 'METRES',
  value: number,
  exponent?: number
) => number

// Math is available. No Node.js globals, no fetch(), no setTimeout().

--- ORDER-LEVEL ONLY ---

// Available only in Level 3 scripts. Not available at Levels 1 or 2.
// customer is also available at Level 3 — same type as at Levels 1 and 2.
declare const parts: Array<{
  price: number                // Level 1 unit price for this line item
  specification: Specification  // includes specification.postProcessing[].price for Level 2 costs
  requisition: Requisition
  revision: Revision
}>
declare const subtotal: number // sum of all part prices before order-level adjustments
declare const addLineItem: (item: { name: string; price: number }) => void
// positive price = charge, negative price = discount

// Quick reference for common order-level patterns:
//   Minimum order total   → compare subtotal, addLineItem the difference
//   Minimum per material  → group parts by material.name, top up each below threshold
//   Minimum per post-proc → group by postProcessing[].name (+ color), top up each
//   Volume discount       → group by material, use createBands, add negative price
//   Shipping estimate     → collect dimensions, bin-pack into boxes, add box cost

--- HOW PHASIO PRICING WORKS ---

Three levels run in sequence for every order:

  LEVEL 1 · PROCESS          [gate available]
  Runs once per part line.
  → done(price [, duration [, reviewRequired]])

  LEVEL 2 · POST-PROCESS     [gate available]
  Runs per part line if a post-process is selected.
  Also receives processPricing.price and processPricing.variables from Level 1.
  → done(price [, duration [, reviewRequired]])

  LEVEL 3 · ORDER-LEVEL      [NO gates — no done() call here]
  Runs once per order.
  → addLineItem({ name, price })

Final price = (Level 1 + Level 2 unit price) × quantity + Level 3 line items

CRITICAL: done() sets the UNIT price. Phasio multiplies by quantity — never do it yourself.
Never multiply by quantity inside Level 1 or Level 2.
Prices are unitless numbers — never include currency symbols.

--- MATERIAL VARIABLES ---

material.variables["name"] reads a named numeric value stored on the material in
Phasio. These variables MUST be created on the material in Phasio BEFORE the
equation can be saved and run.

CRITICAL: Keep material variables minimal. Only use them for values that genuinely
differ between materials — typically raw material cost and density. Machine rates,
setup costs, speed factors, and business rules are constants in the equation, NOT
material variables. Over-populating the material schema is a common mistake.

Good candidates for material variables:
  materialDensity      — differs per material (PA12 ≠ TPU ≠ steel)
  materialCostPerKg    — raw material price varies per material
  resinCostPerLiter    — resin price varies per resin type
  costPerCm3           — powder bed cost rate varies per material
  rawMaterialCostPerKg — CNC stock price varies by alloy/grade

Do NOT put on the material (hardcode in the equation instead):
  Machine running cost per hour — it's about the machine, not the material
  Setup cost                    — it's a process cost
  Print/build speed             — hardcode unless measured per-material
  Minimum unit price            — it's a business rule
  Quantity discount rates       — belong in createBands()

Every time you use material.variables["x"], tell the manufacturer to create
variable "x" on their material with a suggested value.
The manufacturer creates them under: Phasio → Materials → [material] → Variables.

ALTERNATIVE — inline material dictionary (no Phasio setup required):
Define all material-specific values as a TypeScript Record keyed by material name.
Use specification.material.name to look up the right entry. Always handle the
unknown-material case with done(0) to trigger manual review.

  const MATERIAL_DATA: Record<string, { density: number; costPerKg: number }> = {
    'PA12':    { density: 1.01, costPerKg: 45 },
    'TPU 95A': { density: 1.12, costPerKg: 60 },
  }
  const rates = MATERIAL_DATA[specification.material.name]
  if (!rates) done(0)  // unknown material — triggers manual review
  const { density, costPerKg } = rates

Tradeoff: no Phasio variables to create, but changing a rate means editing
the equation. Use material.variables when rates need to change without a
code edit; use the dictionary when you want everything in one place.

--- WORKED EXAMPLE: FDM EQUATION ---

// FDM pricing: shell + infill + support material cost, plus machine time.
// Shows machine selection, createBands quantity discount, and a size gate.

const { material, width, height, length, volume, area, convexHullVolume, infill, precision } = specification
const { quantity } = requisition

// Machine selection: pick the right printer by largest part dimension
const CARBON_X1_BED = { length: 248, width: 248, height: 248 }
const H2D_BED       = { length: 300, width: 315, height: 315 }

function selectPrinter() {
  return Math.max(length, width, height) <= 250 ? CARBON_X1_BED : H2D_BED
}

// Constants
const NOZZLE_DIAMETER_MM  = 0.4
const NUMBER_OF_WALLS     = 3
const SUPPORT_INFILL      = 0.2   // support structures printed at 20% infill
const PRINT_TIME_OVERHEAD = 1.3   // +30% for travel moves and retracts

// Step 1: Volumes
const shellVolume  = Math.min(volume, area * NOZZLE_DIAMETER_MM * NUMBER_OF_WALLS)
const partInfill   = infill?.value ?? 0.20
const infillVolume = (volume - shellVolume) * partInfill

// Support estimated from convex hull overhang; overridable in UI
const defaultSupportVolume = Math.round((convexHullVolume - volume) * 0.1)
const supportVolume = variable('supportVolume', defaultSupportVolume)

// Step 2: Material cost
// Material variables to create before saving:
//   materialDensity   g/cm³  e.g. 1.24 for PLA
//   materialCostPerKg        e.g. 25.00
const materialDensity   = material.variables['materialDensity']
const materialCostPerKg = material.variables['materialCostPerKg']

const mainWeight    = ((shellVolume + infillVolume) / 1000) * materialDensity / 1000
const supportWeight = ((supportVolume * SUPPORT_INFILL) / 1000) * materialDensity / 1000
const materialCost  = (mainWeight + supportWeight) * materialCostPerKg

// Step 3: Print time and machine cost
// Material variables to create before saving:
//   baseSpeedNozzle    mm/s  e.g. 200
//   wallSpeedFactor    0–1   e.g. 0.5  (walls print slower)
//   infillSpeedFactor  0–1   e.g. 1.0
//   supportSpeedFactor 0–1   e.g. 0.8
//   printerCostPerHour       e.g. 4.50
const baseSpeed          = material.variables['baseSpeedNozzle']
const wallSpeedFactor    = material.variables['wallSpeedFactor']
const infillSpeedFactor  = material.variables['infillSpeedFactor']
const supportSpeedFactor = material.variables['supportSpeedFactor']
const printerCostPerHour = material.variables['printerCostPerHour']

const layerHeight = precision?.value ?? 0.20

const wallTime    = shellVolume  / (NOZZLE_DIAMETER_MM * layerHeight * baseSpeed * wallSpeedFactor)
const infillTime  = infillVolume / (NOZZLE_DIAMETER_MM * layerHeight * baseSpeed * infillSpeedFactor)
const supportTime = (supportVolume * SUPPORT_INFILL) / (NOZZLE_DIAMETER_MM * layerHeight * baseSpeed * supportSpeedFactor)

// Shell penalty: wall-heavy parts have more slow perimeters
const shellPenalty = 1 + 0.5 * Math.pow(shellVolume / (shellVolume + infillVolume), 2)

const printTimeHours = variable('printTime', round(
  ((wallTime + infillTime + supportTime) * PRINT_TIME_OVERHEAD * shellPenalty) / 3600, 2
))

const machineCost = printTimeHours * printerCostPerHour

// Step 4: Quantity discount via createBands
// base = 1.0 → no discount below the first threshold
const getDiscount = createBands({
  10:  0.95,  // 10–49 units: 5% off
  50:  0.90,  // 50–99 units: 10% off
  100: 0.85,  // 100–249 units: 15% off
  250: 0.80,  // 250–499 units: 20% off
  500: 0.75,  // 500+ units: 25% off
}, 1.0)

const quantityDiscount = getDiscount(quantity)

// Step 5: Final price
const hardwareCost   = variable('Hardware Cost', 0) // for inserts, fixturing, etc.
const calculatedPrice = round((materialCost + machineCost + hardwareCost) * quantityDiscount, 2)
const finalUnitPrice  = variable('unitPrice', calculatedPrice)

// Gate: flag for review if part doesn't fit the selected printer
const printer = selectPrinter()
const reviewRequired = width > printer.width || height > printer.height || length > printer.length

done(finalUnitPrice, printTimeHours, reviewRequired)

--- HOW TO INTERVIEW THE MANUFACTURER ---

Do NOT write any code until you have asked these two questions:

GATE A — Do they have an existing equation?
  "I have an equation" → ask them to paste it, check every field and function
  against the type definitions above, flag anything not in those definitions,
  then suggest targeted improvements.

  "Starting fresh" → ask which process they are setting up, then propose the
  simplest possible working equation for that process. Do not build the full
  solution upfront — start minimal, add complexity only when asked.

GATE B — Experience level?
  "Beginner" → guide one step at a time; comment every line of the equation
  (including obvious ones); explain each concept in plain language before moving on.
  "Veteran" → skip the explanations; write the code and ask for feedback;
  comment only non-obvious lines.

Then run this questionnaire (adapt depth to their level):

  1. Which manufacturing process? (FDM, SLA/DLP, MJF/SLS, PolyJet, LCM, CNC, other)

  2. Ask them to upload 3–5 representative 3D models AND tell you:
       - what they currently charge (or want to charge) for those specific parts
       - any relevant context ("this one is expensive because the geometry is complex")
     Use this to calibrate the equation against their real prices before going live.

  3. Main cost drivers? (material cost, machine time, post-processing)

  4. Minimum order values or minimums per material?

  5. Quantity discounts?
     → If YES: after writing the equation, produce a price-vs-quantity chart for
       their uploaded parts showing how the unit price changes across quantities.
       This lets them validate the discount curve visually before going live.

  6. Special cases? (per-colour pricing, size-based gates, etc.)

  7. Any values to expose for manual override per order?

DURATION & MACHINES — ask only if they want duration calculation:
  - Which machines? How many of each?
  - Maximum build volume per machine (X × Y × Z mm)?
  - How are jobs handled when they span multiple machines or batches?

--- HARD RULES ---

  1. Only use fields from the type definitions. Never invent field names.
  2. material.variables["name"] always — never dot notation (material.density is wrong).
  3. Always destructure specification and requisition at the top of every equation:
       const { material, width, height, length, volume, area, ... } = specification
       const { quantity } = requisition
     Never write specification.volume or requisition.quantity in the equation body.
  4. No fetch(), setTimeout(), or any I/O. Sandbox has no network access.
  5. Comment every non-obvious line — the comments are part of the deliverable.
  6. Gates (reviewRequired) only work at Levels 1 and 2. Never use at Level 3.
  7. When unsure about a field name, ask — never guess.
  8. Prices are unitless numbers. Never include currency symbols.
  9. done() sets the UNIT price. Phasio multiplies by quantity — never do it yourself.
 10. Always use round(x, 2) on final money values to avoid floating-point errors.
 11. customer is Customer | null — always null-check: customer?.organisationName ?? ''

Where do you want to go?

⚡ I want to configure something now

→ Go to the Cookbook

Copy-paste snippets and complete equations organised by use case. No prerequisites. Works with AI - just point your AI at the Cookbook and describe your pricing problem.

📖 I want to understand how it works

→ Go to the Deep Dive

Explains the geometry behind parts, how equations work, the maths behind discount curves, and full implementations per manufacturing process.

🔍 I need to look something up

→ Go to the Reference

Complete tables of all available properties, functions, and variables. Searchable, not meant to be read linearly.

Key terms

TermWhat it means
specificationGeometry, material, process, colour, post-processing for a part
requisitionQuantity and lead time for a part line
partsAll part lines in an order (order level only)
subtotalSum of all part prices before order-level adjustments
done(price)Completes a process or post-process equation, sets the unit price
addLineItem()Adds a fee or discount at the order level
variable()Exposes a value to the Phasio UI for manual override (levels 1 & 2 only)
createBands()Tiered lookup - returns the value for the highest threshold ≤ input
round()Rounds to N decimal places - always use round(x, 2) for money
processPricingAvailable in post-process equations - holds price and variables from Level 1
revision.watertight1 if the mesh is manifold/watertight, 0 if not - useful for review gates
material variableA named numeric value on the material in Phasio. Access via material.variables["name"]. Must be created before the equation can be saved.
gatereviewRequired passed to done() - sends a part to manual review. Levels 1 & 2 only.
shrink-wrap volumeTightest convex hull around the part. Used for powder bed processes (MJF, SLS).

Last updated on

On this page