Alongside the result, the Ptally engine returns a debug block: every input it read, every lookup, every formula with the values it used, every rule it checked and how long each step took. Like the models, it is plain JSON that an AI agent can trace and a person can read. This page walks through a real one.
People and AI agents read the same debug block. When a price looks wrong, both see the same steps, values and warnings.
Priced against the pet insurance model. It comes back valid at £621.47 but referred, because £7,000 cover on a high-risk breed needs an underwriter. The debug block shows why.
{
"species": "dog",
"breed": "french_bulldog",
"pet_age": 2,
"region": "south_east",
"vet_fee_limit": 7000,
"excess": 100,
"neutered": true
}{
"risk_premium": 554.88,
"ipt": 66.59,
"monthly_premium": 51.79,
"base_premium": 298,
"refer": true,
"result": 621.47,
"valid": true,
"debug": {
"meta": "{ … }",
"timer": "[ 14 entries ]",
"errors": "[]",
"exclusions": "[]",
"refers": "[ 1 entry ]",
"excesses": "[]",
"endorsements": "[]",
"indicators": "{}",
"steps": "{ 15 steps }"
}
}{
"risk_premium": 554.88,
"ipt": 66.59,
"monthly_premium": 51.79,
"base_premium": 298,
"refer": true,
"result": 621.47,
"valid": true,
"debug": {
"meta": {
"name": "UK Pet Insurance: Cats and Dogs",
"description": "Lifetime vet-fee cover for a single cat or dog, rated by species, breed, age, region, vet-fee limit and excess.",
"summary": "Rates annual lifetime vet-fee cover for one cat or dog in the UK. Base rates by species and annual vet-fee limit are adjusted for age band, breed, region, voluntary excess and neutering, then 12% Insurance Premium Tax is added. Older pets and breed/species mismatches are declined; high-limit cover on high-risk breeds is referred."
},
"timer": [
{
"time": 0,
"elapsed": 0.88,
"key": "input"
},
{
"time": 0.698,
"elapsed": 0.698,
"key": "base_rates"
},
{
"time": 0.864,
"elapsed": 0.166,
"key": "age_bands"
},
{
"time": 0.986,
"elapsed": 0.122,
"key": "breed_factors"
},
{
"time": 1.019,
"elapsed": 0.033,
"key": "region_factors"
},
{
"time": 1.247,
"elapsed": 0.228,
"key": "excess_factors"
},
{
"time": 1.285,
"elapsed": 0.038,
"key": "neuter_factors"
},
{
"time": 2.672,
"elapsed": 1.387,
"key": "risk_premium"
},
{
"time": 3.06,
"elapsed": 0.388,
"key": "ipt"
},
{
"time": 3.248,
"elapsed": 0.187,
"key": "total_premium"
},
{
"time": 3.372,
"elapsed": 0.125,
"key": "monthly_premium"
},
{
"time": 3.549,
"elapsed": 0.177,
"key": "pet_exclusions"
},
{
"time": 3.62,
"elapsed": 0.071,
"key": "pet_refers"
},
{
"time": 3.927,
"elapsed": 0.307,
"key": "output"
}
],
"errors": [],
"exclusions": [],
"refers": [
{
"step": "pet_refers",
"name": "£7,000 limit on a high-risk breed",
"vet_fee_limit": 7000,
"breed": "french_bulldog",
"exp": "{{vet_fee_limit}} == 7000 && ({{breed}} == 'french_bulldog' || {{breed}} == 'persian')",
"result": true
}
],
"excesses": [],
"endorsements": [],
"indicators": {},
"steps": {
"statics": {},
"input": {
"errors": [],
"items": [
{
"step": "input",
"name": "Species",
"species": "dog",
"exp": "{{species}}",
"result": "dog"
},
{
"step": "input",
"name": "Breed",
"breed": "french_bulldog",
"exp": "{{breed}}",
"result": "french_bulldog"
},
{
"step": "input",
"name": "Pet age (years)",
"pet_age": 2,
"exp": "{{pet_age}}",
"result": 2
},
{
"step": "input",
"name": "Region",
"region": "south_east",
"exp": "{{region}}",
"result": "south_east"
},
{
"step": "input",
"name": "Annual vet-fee limit (£)",
"vet_fee_limit": 7000,
"exp": "{{vet_fee_limit}}",
"result": 7000
},
{
"step": "input",
"name": "Excess (£)",
"excess": 100,
"exp": "{{excess}}",
"result": 100
},
{
"step": "input",
"name": "Neutered",
"neutered": true,
"exp": "{{neutered}}",
"result": true
}
],
"output": {
"species": "dog",
"breed": "french_bulldog",
"pet_age": 2,
"region": "south_east",
"vet_fee_limit": 7000,
"excess": 100,
"neutered": true
}
},
"base_rates": {
"errors": [],
"items": [],
"output": {
"base_premium": 298
}
},
"age_bands": {
"errors": [],
"items": [],
"output": {
"pet_age_factor": 1
}
},
"breed_factors": {
"errors": [],
"items": [],
"output": {
"breed_factor": 1.75,
"breed_matches": 1
}
},
"region_factors": {
"errors": [],
"items": [],
"output": {
"region_factor": 1.12
}
},
"excess_factors": {
"errors": [],
"items": [],
"output": {
"excess_factor": 1
}
},
"neuter_factors": {
"errors": [],
"items": [],
"output": {
"neuter_factor": 0.95
}
},
"risk_premium": {
"errors": [],
"items": [
{
"formula": "round({{base_premium}} * {{pet_age_factor}} * {{breed_factor}} * {{region_factor}} * {{excess_factor}} * {{neuter_factor}}, 2)",
"result": 554.88,
"base_premium": 298,
"pet_age_factor": 1,
"breed_factor": 1.75,
"region_factor": 1.12,
"excess_factor": 1,
"neuter_factor": 0.95
}
],
"output": {
"risk_premium": 554.88
}
},
"ipt": {
"errors": [],
"items": [
{
"formula": "round({{risk_premium}} * 0.12, 2)",
"result": 66.59,
"risk_premium": 554.88
}
],
"output": {
"ipt": 66.59
}
},
"total_premium": {
"errors": [],
"items": [
{
"formula": "round({{risk_premium}} + {{ipt}}, 2)",
"result": 621.47,
"risk_premium": 554.88,
"ipt": 66.59
}
],
"output": {
"total_premium": 621.47
}
},
"monthly_premium": {
"errors": [],
"items": [
{
"formula": "round({{total_premium}} / 12, 2)",
"result": 51.79,
"total_premium": 621.47
}
],
"output": {
"monthly_premium": 51.79
}
},
"pet_exclusions": {
"errors": [],
"items": [
{
"step": "pet_exclusions",
"name": "Pet older than 14 at inception",
"pet_age": 2,
"exp": "{{pet_age}} > 14",
"result": false
},
{
"step": "pet_exclusions",
"name": "Breed does not match species",
"breed_matches": 1,
"exp": "{{breed_matches}} == 0",
"result": false
}
],
"output": {
"pet_exclusions": false
}
},
"pet_refers": {
"errors": [],
"items": [
{
"step": "pet_refers",
"name": "£7,000 limit on a high-risk breed",
"vet_fee_limit": 7000,
"breed": "french_bulldog",
"exp": "{{vet_fee_limit}} == 7000 && ({{breed}} == 'french_bulldog' || {{breed}} == 'persian')",
"result": true
}
],
"output": {
"pet_refers": true
}
},
"output": {
"errors": [],
"items": [
{
"step": "output",
"name": "Risk premium",
"risk_premium": 554.88,
"exp": "{{risk_premium}}",
"result": 554.88
},
{
"step": "output",
"name": "IPT",
"ipt": 66.59,
"exp": "{{ipt}}",
"result": 66.59
},
{
"step": "output",
"name": "Monthly premium",
"monthly_premium": 51.79,
"exp": "{{monthly_premium}}",
"result": 51.79
},
{
"step": "output",
"name": "Base premium",
"base_premium": 298,
"exp": "{{base_premium}}",
"result": 298
},
{
"step": "output",
"name": "Refer to underwriter",
"pet_refers": true,
"exp": "{{pet_refers}}",
"result": true
},
{
"formula": "round({{total_premium}}, 2)",
"result": 621.47,
"total_premium": 621.47
},
{
"step": "result",
"pet_exclusions": false,
"exp": "{{pet_exclusions}} == false",
"result": true
}
],
"output": {
"risk_premium": 554.88,
"ipt": 66.59,
"monthly_premium": 51.79,
"base_premium": 298,
"refer": true,
"result": 621.47,
"valid": true
}
}
}
}
}result · validThe price and whether it can be bound, plus every field the model exposes.debug.metaWhich model priced the quote.debug.timerHow long each step took, in milliseconds.debug.stepsThe full trace: every input, lookup, formula and rule with the values it used.debug.exclusions · refersOnly the rules that fired, and the values that fired them.debug.errorsWarnings where the engine fell back to a default. Empty is what you want.excesses · endorsements · indicatorsExcesses and wording that apply, and fields indexed for search.result is the price from the model's result formula. Here that is the annual premium including IPT. valid is false if any exclusion fired; a declined quote still carries a price, but it must not be bound.
Everything else is the model's own output fields: the premium split, the monthly figure and refer, which is true here. Those are the numbers to show a customer or put on a schedule.
{
"risk_premium": 554.88,
"ipt": 66.59,
"monthly_premium": 51.79,
"base_premium": 298,
"refer": true,
"result": 621.47,
"valid": true
}The model's name, description and summary at the moment it priced the quote. Combined with the version on the quote response, it ties every price to an exact model, so you can always answer “what was live when this was quoted?”
{
"name": "UK Pet Insurance: Cats and Dogs",
"description": "Lifetime vet-fee cover for a single cat or dog, rated by species, breed, age, region, vet-fee limit and excess.",
"summary": "Rates annual lifetime vet-fee cover for one cat or dog in the UK. Base rates by species and annual vet-fee limit are adjusted for age band, breed, region, voluntary excess and neutering, then 12% Insurance Premium Tax is added. Older pets and breed/species mismatches are declined; high-limit cover on high-risk breeds is referred."
}One entry per step, in run order. time is when the step started and elapsed how long it took, both in milliseconds from the start of the run. This whole quote took 4.2 ms.
Use it to find a slow step, usually a very large table or an external call, before it shows up as latency at the aggregators.
inputbase_ratesage_bandsbreed_factorsregion_factorsexcess_factorsneuter_factorsrisk_premiumipttotal_premiummonthly_premiumpet_exclusionspet_refersoutputOne entry per step, keyed by step id, in run order. Each has output (the values it added), plus items showing how it got there and errors for anything it couldn't work out. Values flow down: every step sees the outputs of the steps above it.
| Step | Type | Output |
|---|---|---|
input | input | species: "dog" · breed: "french_bulldog" · pet_age: 2 · region: "south_east" · vet_fee_limit: 7000 · excess: 100 · neutered: true |
base_rates | collection | base_premium: 298 |
age_bands | collection | pet_age_factor: 1 |
breed_factors | collection | breed_factor: 1.75 · breed_matches: 1 |
region_factors | collection | region_factor: 1.12 |
excess_factors | collection | excess_factor: 1 |
neuter_factors | collection | neuter_factor: 0.95 |
risk_premium | calculation | risk_premium: 554.88 |
ipt | calculation | ipt: 66.59 |
total_premium | calculation | total_premium: 621.47 |
monthly_premium | calculation | monthly_premium: 51.79 |
pet_exclusions | exclusion | pet_exclusions: false |
pet_refers | refer | pet_refers: true |
output | output | risk_premium: 554.88 · ipt: 66.59 · monthly_premium: 51.79 · base_premium: 298 · refer: true · result: 621.47 · valid: true |
One item per input field: the raw value received, the expression that read it and the result the model used. If a field is missing or misspelled in the request, this is where you'll see the default being used instead.
{
"input": {
"items": [
{
"step": "input",
"name": "Species",
"species": "dog",
"exp": "{{species}}",
"result": "dog"
},
{
"step": "input",
"name": "Breed",
"breed": "french_bulldog",
"exp": "{{breed}}",
"result": "french_bulldog"
}
],
"output": {
"species": "dog",
"breed": "french_bulldog",
"pet_age": 2,
"region": "south_east",
"vet_fee_limit": 7000,
"excess": 100,
"neutered": true
}
}
}Rate-table steps report the values they found in output. Here the French Bulldog row gave a breed factor of 1.75 and the South East a region factor of 1.12. If a lookup finds no matching row, it falls back to its default and says so in errors.
{
"breed_factors": {
"errors": [],
"items": [],
"output": {
"breed_factor": 1.75,
"breed_matches": 1
}
},
"region_factors": {
"errors": [],
"items": [],
"output": {
"region_factor": 1.12
}
}
}Each calculation item shows the formula, the value of every variable it referenced and the result. You can redo the arithmetic by hand: 298 × 1 × 1.75 × 1.12 × 1 × 0.95 = 554.88. This is the fastest way to find which factor is off.
{
"risk_premium": {
"errors": [],
"items": [
{
"formula": "round({{base_premium}} * {{pet_age_factor}} * {{breed_factor}} * {{region_factor}} * {{excess_factor}} * {{neuter_factor}}, 2)",
"result": 554.88,
"base_premium": 298,
"pet_age_factor": 1,
"breed_factor": 1.75,
"region_factor": 1.12,
"excess_factor": 1,
"neuter_factor": 0.95
}
],
"output": {
"risk_premium": 554.88
}
}
}Every rule in an exclusion or refer step is listed with the values its expression saw and its result. The step's output is true if any rule fired. Here the £7,000 limit and the breed together fired the refer.
{
"pet_refers": {
"errors": [],
"items": [
{
"step": "pet_refers",
"name": "£7,000 limit on a high-risk breed",
"vet_fee_limit": 7000,
"breed": "french_bulldog",
"exp": "{{vet_fee_limit}} == 7000 && ({{breed}} == 'french_bulldog' || {{breed}} == 'persian')",
"result": true
}
],
"output": {
"pet_refers": true
}
}
}These lists show only the rules that returned true appear here, collected across every rule step in the model. An empty exclusions list means the quote is valid; anything in refers needs an underwriter before binding.
Show these to an underwriter as the reasons for a referral or decline. Each entry carries the rule's name and the values that triggered it.
{
"exclusions": [],
"refers": [
{
"step": "pet_refers",
"name": "£7,000 limit on a high-risk breed",
"vet_fee_limit": 7000,
"breed": "french_bulldog",
"exp": "{{vet_fee_limit}} == 7000 && ({{breed}} == 'french_bulldog' || {{breed}} == 'persian')",
"result": true
}
]
}Empty in our example, which is what you want. When something can't be worked out, the engine doesn't stop: it uses the default and records a warning. Each one names the step, the expression or formula, the inputs it saw and the default_to value it used instead.
The same model, quoted for a cat with a Labrador's breed. No row matches, so the breed factor fell back to 1. The model expects this because a separate exclusion declines the quote, but in most models a low warning means a missing row in a rate table.
{
"step": "breed_factors",
"level": "low",
"message": "breed_factor returned empty result for: [{\"query\":\"species\",\"value\":\"cat\"},{\"query\":\"breed\",\"value\":\"labrador\"}]",
"default_to": 1,
"expression": "collection.filter(species=={{species}}).filter(breed=={{breed}}).map(breed_rel).first()",
"inputs": [
{
"key": "species",
"value": "cat"
},
{
"key": "breed",
"value": "labrador"
}
]
}And the exclusion that declined it, with valid: false on the result:
[
{
"step": "pet_exclusions",
"name": "Breed does not match species",
"breed_matches": 0,
"exp": "{{breed_matches}} == 0",
"result": true
}
]From an early draft of this model, where a calculation tested a true/false field directly. The formula couldn't be evaluated, so the neutered factor silently became 1 and every neutered pet missed its 5% discount. The warning was the only sign. The fix was to move the discount into a lookup.
{
"step": "neuter_factor",
"level": "medium",
"message": "neutered is not a number",
"default_to": 1,
"formula": "{{neutered}} == true ? 0.95 : 1",
"inputs": [
{
"key": "neutered",
"value": null
}
]
}lowmediumdefault_toThe pet model doesn't use these, so they come back empty. They are always present, so your integration can rely on the shape.
index in the model, stored on the quote so you can search your quote history by them.These steps work the same whether you, an underwriter or an AI agent is debugging.