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
┌──────────────────────────────────────────────────────────────┐
│ 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
| Term | What it means |
|---|---|
specification | Geometry, material, process, colour, post-processing for a part |
requisition | Quantity and lead time for a part line |
parts | All part lines in an order (order level only) |
subtotal | Sum 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 |
processPricing | Available in post-process equations - holds price and variables from Level 1 |
revision.watertight | 1 if the mesh is manifold/watertight, 0 if not - useful for review gates |
| material variable | A named numeric value on the material in Phasio. Access via material.variables["name"]. Must be created before the equation can be saved. |
| gate | reviewRequired passed to done() - sends a part to manual review. Levels 1 & 2 only. |
| shrink-wrap volume | Tightest convex hull around the part. Used for powder bed processes (MJF, SLS). |
Last updated on