Spec 0.1-draftTattoo 0.1.0 · draftEdit on git.cafe ↗
On this page

Specification · 0.1-draft

One contract, from questions to quote.

UQP gives businesses and agents a shared JSON vocabulary for services without a fixed shelf price. The core defines the objects; industry packs define the questions and pricing math. This draft is not stable.

Start here

  1. Read the objects: discover questions with a manifest, collect answers, then evaluate the business model.
  2. Try the sample JSON: follow the synthetic Tattoo pack examples below. Validate them against the raw schemas.
  3. Pick a transport: REST and MCP have local reference bindings. They are not production services.

For a local reference implementation, clone the repository, install dependencies and run:

npm ci
npm test
npm run typecheck

The pure Tattoo evaluator accepts answers plus explicit consent, a pricing model and a caller-supplied timestamp. See Tattoo pack implementation notes before using its unvalidated reference settings.

Core objects

All schemas use JSON Schema draft 2020-12. Object roots reject unknown properties. Tables below are generated from the checked-in schemas, including nested fields and shared definitions.

These examples use Tattoo pack 0.1.0 and a fixed evaluation time. The model constants are unvalidated; the seven-day expiry is an illustrative business choice, not a protocol default. Example data, not real prices.

QuoteManifest

A QuoteManifest declares the intake questions, model reference, review mode and validity settings offered by a business.

QuoteManifest fields. Nested required fields are required when their parent is present.
FieldTypeRequiredDescription
pack_idstringYesIndustry pack identifier. Minimum length: 1.
pack_versionstringYesSelected pack version; must agree across related objects. Minimum length: 1.
enabled_questionsarray of stringYesQuestion IDs enabled by the business model. Items must be unique. Item constraints: Minimum length: 1.
pricing_model_refstringYesReference to the business-owned model; consumers resolve it. Minimum length: 1.
artist_review_modeenumYesBusiness review preference; both modes return a non-binding Estimate before a binding Quote. Allowed: pre_price, post_price.
expiry_daysintegerYesBusiness-configured validity period in days; no protocol default. Minimum: 1.
currenciesarray of stringYesSupported three-letter uppercase currency codes. Minimum items: 1. Items must be unique. Item constraints: Pattern: ^[A-Z]{3}$.
QuoteManifest · Tattoo pack 0.1.0. Example data, not real prices.
QuoteManifest · Tattoo pack 0.1.0
{
  "pack_id": "tattoo",
  "pack_version": "0.1.0",
  "enabled_questions": [
    "description",
    "placement",
    "size",
    "width",
    "height",
    "complexity",
    "style",
    "budget_min",
    "budget_max",
    "first_tattoo"
  ],
  "pricing_model_ref": "urn:uqp:tattoo:pricing:example",
  "artist_review_mode": "post_price",
  "expiry_days": 7,
  "currencies": [
    "USD"
  ]
}

Raw QuoteManifest schema · Browse all schemas

QuoteRequest

A QuoteRequest carries raw answers and explicit consent for a selected industry pack and version.

QuoteRequest fields. Nested required fields are required when their parent is present.
FieldTypeRequiredDescription
pack_idstringYesIndustry pack identifier. Minimum length: 1.
pack_versionstringYesSelected pack version; must agree across related objects. Minimum length: 1.
answersobjectYesExact raw question answers. Keep contact details in a separate channel; free text still needs privacy checks. Known contact-information keys are rejected. Map values may be any JSON value.
consentobjectYesExplicit research-sharing and anonymization preferences, with capture time. Unknown properties are rejected.
consent.share_for_pricing_researchbooleanYesWhether sharing for pricing research was authorized.
consent.anonymizebooleanYesRequested anonymization preference, not proof that the artifact is anonymous.
consent.captured_atstringYesWhen consent was recorded. Format: date-time.
QuoteRequest · Tattoo pack 0.1.0. Example data, not real prices.
QuoteRequest · Tattoo pack 0.1.0
{
  "pack_id": "tattoo",
  "pack_version": "0.1.0",
  "answers": {
    "description": "A small geometric mountain design with clean outlines.",
    "size": "sm",
    "placement": [
      "Lower Arm"
    ],
    "complexity": "simple",
    "style": "blackgrey"
  },
  "consent": {
    "share_for_pricing_research": false,
    "anonymize": true,
    "captured_at": "2026-11-05T12:00:00Z"
  }
}

Raw QuoteRequest schema · Browse all schemas

PricingModel

A PricingModel records the business-owned rates, factor switches, rounding and deposit rules used to calculate a non-binding Estimate.

PricingModel fields. Nested required fields are required when their parent is present.
FieldTypeRequiredDescription
pack_idstringYesIndustry pack identifier. Minimum length: 1.
pack_versionstringYesSelected pack version; must agree across related objects. Minimum length: 1.
idstringNoIdentifier for this model or factor. Minimum length: 1.
artist_review_modeenumNoBusiness review preference; both modes return a non-binding Estimate before a binding Quote. Allowed: pre_price, post_price.
factorsarray of objectYesBusiness-configurable pricing switches and parameters. Item constraints: Unknown properties are rejected.
factors[].idstringYesIdentifier for this model or factor. Minimum length: 1.
factors[].enabledbooleanYesWhether this factor participates in pricing.
factors[].weightnumberNoOptional factor weight, interpreted by the pack.
factors[].valueany JSONNoPack-defined value; any JSON value is permitted.
factors[].modestringNoPack-defined calculation mode, except the fixed rounding modes. Minimum length: 1.
factors[].provenancestringNoSource of settings or price, including who stated the price and when. Minimum length: 1.
ratesobjectYesPack-defined rate map, including currency. Map values may be any JSON value.
minimumsobjectYesPack-defined business minimums. Map values may be any JSON value.
roundingobjectYesIncrement and direction used to round prices. Unknown properties are rejected.
rounding.incrementnumberYesPositive rounding increment. Greater than 0.
rounding.modeenumYesPack-defined calculation mode, except the fixed rounding modes. Allowed: nearest, up, down.
deposit_policyobjectYesPack-defined deposit calculation policy. Unknown properties are rejected.
deposit_policy.modestringYesPack-defined calculation mode, except the fixed rounding modes. Minimum length: 1.
deposit_policy.amountnumberNoNonnegative deposit amount. Minimum: 0.
deposit_policy.percentagenumberNoFraction from zero to one, not a whole-number percent. Minimum: 0. Maximum: 1.
deposit_policy.basisstringNoPricing basis: hourly, day_rate, from or fixed; deposit basis is pack-defined. Minimum length: 1.
deposit_policy.rounding_incrementnumber or nullNoPack-defined field. Greater than 0.
deposit_policy.policiesarray of stringNoDeposit policies the client must explicitly approve before payment handoff. Minimum items: 1. Item constraints: Minimum length: 1.
expiry_daysintegerYesBusiness-configured validity period in days; no protocol default. Minimum: 1.
provenancestringNoSource of settings or price, including who stated the price and when. Minimum length: 1.
keyword_matchenumNoPack-defined field. Allowed: substring, whole_word.
PricingModel · Tattoo pack 0.1.0. Example data, not real prices.
PricingModel · Tattoo pack 0.1.0
{
  "pack_id": "tattoo",
  "pack_version": "0.1.0",
  "provenance": "tattoo.co current constant, unvalidated",
  "id": "tattoo-formula-a-example",
  "artist_review_mode": "post_price",
  "factors": [
    {
      "id": "size",
      "enabled": true,
      "provenance": "tattoo.co current constant, unvalidated",
      "value": {
        "dimensions": {
          "xs": 1.5,
          "sm": 3,
          "md": 5,
          "lg": 8,
          "xl": 12,
          "unsure": 5
        },
        "area_per_hour": 12,
        "minimum_hours": 1,
        "unknown_base_hours": 4
      },
      "mode": "no_estimate"
    },
    {
      "id": "placement",
      "enabled": true,
      "provenance": "tattoo.co current constant, unvalidated",
      "mode": "additive_0_5h",
      "value": {
        "additive_hours": 0.5,
        "multiplier": 1.2,
        "tricky": [
          "Wrist",
          "Hand",
          "Neck",
          "Foot",
          "Lower Leg"
        ],
        "match": {
          "additive_0_5h": "substring",
          "multiplier_1_2": "substring_ci"
        }
      }
    },
    {
      "id": "complexity",
      "enabled": true,
      "provenance": "tattoo.co current constant, unvalidated",
      "mode": "asked",
      "value": {
        "simple": 1,
        "moderate": 1.15,
        "detailed": 1.35,
        "realism": 1.6
      }
    },
    {
      "id": "style",
      "enabled": true,
      "provenance": "tattoo.co current constant, unvalidated",
      "mode": "asked",
      "value": {
        "blackgrey": 1,
        "popcolor": 1.15,
        "color": 1.3
      }
    },
    {
      "id": "wraparound",
      "enabled": false,
      "provenance": "tattoo.co current constant, unvalidated",
      "mode": "wraparound_multiplier_1_3",
      "weight": 1.3
    },
    {
      "id": "tier",
      "enabled": true,
      "provenance": "tattoo.co current constant, unvalidated",
      "mode": "fallback_standard"
    },
    {
      "id": "deposit",
      "enabled": true,
      "provenance": "tattoo.co current constant, unvalidated",
      "mode": "pct_of_low_30",
      "value": {
        "low_percentage": 0.3,
        "total_percentage": 0.5,
        "low_increment": 25,
        "total_increment": 50,
        "piece_increment": 25
      }
    },
    {
      "id": "sessions",
      "enabled": false,
      "provenance": "tattoo.co current constant, unvalidated",
      "mode": "not_yet_modeled"
    },
    {
      "id": "cover_up",
      "enabled": false,
      "provenance": "tattoo.co current constant, unvalidated",
      "mode": "not_yet_modeled"
    },
    {
      "id": "multi_area",
      "enabled": false,
      "provenance": "tattoo.co current constant, unvalidated",
      "mode": "not_yet_modeled"
    },
    {
      "id": "city_rate",
      "enabled": false,
      "provenance": "tattoo.co current constant, unvalidated",
      "mode": "not_yet_modeled"
    },
    {
      "id": "non_usd",
      "enabled": false,
      "provenance": "tattoo.co current constant, unvalidated",
      "mode": "not_yet_modeled"
    }
  ],
  "rates": {
    "currency": "USD",
    "pricing_mode": "hourly",
    "tiers": {
      "apprentice": 120,
      "standard": 200,
      "senior": 300
    },
    "range_band": {
      "low": 0.85,
      "high": 1.15
    },
    "piece_high_multiplier": 1.5,
    "provenance": "tattoo.co current constant, unvalidated"
  },
  "minimums": {
    "piece_deposit": 100,
    "provenance": "tattoo.co current constant, unvalidated"
  },
  "rounding": {
    "increment": 50,
    "mode": "nearest"
  },
  "deposit_policy": {
    "mode": "pct_of_low_30",
    "percentage": 0.3
  },
  "expiry_days": 7,
  "keyword_match": "substring"
}

Raw PricingModel schema · Browse all schemas

Estimate

An Estimate is a non-binding price range computed from PricingModel and returned on PAYLOAD_SUBMITTED, even when artist review is required. It is UQP’s pre-quote layer, not an offer the client can accept.

Estimate fields. Nested required fields are required when their parent is present.
FieldTypeRequiredDescription
pack_idstringYesIndustry pack identifier. Minimum length: 1.
pack_versionstringYesSelected pack version; must agree across related objects. Minimum length: 1.
answersobjectYesExact raw question answers. Keep contact details in a separate channel; free text still needs privacy checks. Known contact-information keys are rejected. Map values may be any JSON value.
artist_review_modeenumYesBusiness review preference; both modes return a non-binding Estimate before a binding Quote. Allowed: pre_price, post_price.
price_rangeobjectYesRequired on every Estimate and Quote, including pending review. Unknown properties are rejected.
price_range.minnumberYesNonnegative lower bound; application must check min ≤ max. Minimum: 0.
price_range.maxnumberYesNonnegative upper bound. Minimum: 0.
price_range.currencystringYesThree-letter uppercase currency code. Pattern: ^[A-Z]{3}$.
price_range.basisenumYesPricing basis: hourly, day_rate, from or fixed; deposit basis is pack-defined. Allowed: hourly, day_rate, from, fixed.
price_range.methodenumYesPrice method: artist_stated, rule or logged_quotes; logged_quotes requires n ≥ 5. Allowed: artist_stated, rule, logged_quotes.
price_range.nintegerNoNumber of logged quotes supporting the price; at least five for logged_quotes. Minimum: 5.
price_range.provenanceobjectYesSource of settings or price, including who stated the price and when. Unknown properties are rejected.
price_range.provenance.whostringYesWho stated the price. Minimum length: 1.
price_range.provenance.whenstringYesWhen the price was stated. Format: date-time.
sessionsobjectYesSession estimate; provisional in the tattoo reference evaluator. Unknown properties are rejected.
sessions.countintegerYesNonnegative session count. Minimum: 0.
sessions.hours_per_sessionnumberYesNonnegative estimated hours per session. Minimum: 0.
depositobjectYesPack-defined field. Provider is merchant of record; UQP never holds funds. Only processor confirmation marks a deposit paid. Unknown properties are rejected.
deposit.amountnumberYesNonnegative deposit amount. Minimum: 0.
deposit.basisstringYesPricing basis: hourly, day_rate, from or fixed; deposit basis is pack-defined. Minimum length: 1.
deposit.policiesarray of stringYesDeposit policies the client must explicitly approve before payment handoff. Minimum items: 1. Item constraints: Minimum length: 1.
deposit.statusenumYesDeposit remains unpaid until processor confirmation. Allowed: unpaid, paid.
deposit.merchant_of_recordany JSONYesThe provider, never UQP.
deposit.confirmationobjectNoPayment processor confirmation; client self-report is insufficient. Unknown properties are rejected.
deposit.confirmation.sourceany JSONYesPayment confirmation must originate from the payment processor.
deposit.confirmation.referencestringYesProcessor confirmation reference. Minimum length: 1.
deposit.confirmation.confirmed_atstringYesWhen the payment processor confirmed payment. Format: date-time.
deposit.provenanceobjectYesSource of settings or price, including who stated the price and when. Unknown properties are rejected.
deposit.provenance.whostringYesWho stated the price. Minimum length: 1.
deposit.provenance.whenstringYesWhen the price was stated. Format: date-time.
assumptionsarray of stringYesAssumptions and limitations that accompany the estimate.
intermediatesarray of objectYesOrdered audit trail of every evaluation step, including disabled factors. Minimum items: 1. Item constraints: Unknown properties are rejected.
intermediates[].stepstringYesEvaluator step name. Minimum length: 1.
intermediates[].factor_idstringYesFactor responsible for this step. Minimum length: 1.
intermediates[].inputany JSONYesOriginal input for the calculation; any JSON value.
intermediates[].valueany JSONYesPack-defined value; any JSON value is permitted.
intermediates[].skippedany JSONNoPack-defined field.
intermediates[].reasonstringNoPack-defined field.
review_statusenumYesCurrent lifecycle state; allowed transitions must be enforced by the consumer. Allowed: VALIDATED_PENDING_REVIEW, MUTATION_REQUIRED.
consentobjectYesExplicit research-sharing and anonymization preferences, with capture time. Unknown properties are rejected.
consent.share_for_pricing_researchbooleanYesWhether sharing for pricing research was authorized.
consent.anonymizebooleanYesRequested anonymization preference, not proof that the artifact is anonymous.
consent.captured_atstringYesWhen consent was recorded. Format: date-time.
anonymizedbooleanYesWhether the delivered artifact actually is anonymized, independently of the requested preference.
kindany JSONYesDiscriminator: estimate or quote.
bindingany JSONYesFalse for Estimate; true only for an artist-reviewed Quote.
confidenceenumYesConfidence in this estimate or reviewed offer; not a guarantee. Allowed: low, medium, high.
Estimate · Tattoo pack 0.1.0. Example data, not real prices.
Estimate · Tattoo pack 0.1.0
{
  "kind": "estimate",
  "binding": false,
  "confidence": "low",
  "pack_id": "tattoo",
  "pack_version": "0.1.0",
  "answers": {
    "description": "A small geometric mountain design with clean outlines.",
    "size": "sm",
    "placement": [
      "Lower Arm"
    ],
    "complexity": "simple",
    "style": "blackgrey"
  },
  "artist_review_mode": "post_price",
  "price_range": {
    "min": 150,
    "max": 250,
    "currency": "USD",
    "basis": "hourly",
    "method": "rule",
    "provenance": {
      "who": "PricingModel tattoo@0.1.0 (example rules, unvalidated)",
      "when": "2026-11-05T12:00:00.000Z"
    }
  },
  "sessions": {
    "count": 1,
    "hours_per_session": 1
  },
  "deposit": {
    "amount": 50,
    "basis": "low",
    "provenance": {
      "who": "PricingModel tattoo@0.1.0 (example rules, unvalidated)",
      "when": "2026-11-05T12:00:00.000Z"
    },
    "policies": [
      "Example data, not real prices; provider must state final deposit policies before artist review."
    ],
    "status": "unpaid",
    "merchant_of_record": "provider"
  },
  "assumptions": [
    "tattoo.co current constant, unvalidated; supplied handoff attributed to f018b86, not independently revalidated.",
    "Pricing uses unrounded hours; display logs both Formula A Math.round(t*10)/10 and Formula B toFixed(1).",
    "Sessions are a provisional single block (zero for by_piece); session splitting and caps are not modeled.",
    "Multiple placements trigger any-tricky once; area summing is not modeled.",
    "Raw answers are preserved; anonymized=false because free text has not been deidentified."
  ],
  "intermediates": [
    {
      "step": "pricing_mode",
      "factor_id": "pricing",
      "input": "hourly",
      "value": "hourly"
    },
    {
      "step": "input_resolved",
      "factor_id": "complexity",
      "input": {
        "source": "asked",
        "rule_id": null,
        "keywords": []
      },
      "value": "simple"
    },
    {
      "step": "input_resolved",
      "factor_id": "style",
      "input": {
        "source": "asked",
        "rule_id": null,
        "keywords": []
      },
      "value": "blackgrey"
    },
    {
      "step": "input_resolved",
      "factor_id": "size",
      "input": {
        "source": "asked",
        "rule_id": null,
        "keywords": []
      },
      "value": "sm"
    },
    {
      "step": "input_resolved",
      "factor_id": "placement",
      "input": {
        "source": "asked",
        "rule_id": null,
        "keywords": []
      },
      "value": [
        "Lower Arm"
      ]
    },
    {
      "step": "input_resolved",
      "factor_id": "tier",
      "input": {
        "source": "configured",
        "rule_id": null,
        "keywords": []
      },
      "value": "fallback_standard"
    },
    {
      "step": "input_resolved",
      "factor_id": "deposit",
      "input": {
        "source": "configured",
        "rule_id": null,
        "keywords": []
      },
      "value": "pct_of_low_30"
    },
    {
      "step": "dimensions",
      "factor_id": "size",
      "input": {
        "raw_size": "sm",
        "width": null,
        "height": null
      },
      "value": {
        "width": 3,
        "height": 3
      }
    },
    {
      "step": "area",
      "factor_id": "size",
      "input": {
        "width": 3,
        "height": 3
      },
      "value": 9
    },
    {
      "step": "base_hours",
      "factor_id": "size",
      "input": {
        "area": 9,
        "area_per_hour": 12,
        "minimum_hours": 1
      },
      "value": 1
    },
    {
      "step": "placement",
      "factor_id": "placement",
      "input": {
        "hours": 1,
        "placements": [
          "Lower Arm"
        ],
        "mode": "additive_0_5h",
        "match": "substring",
        "tricky": false,
        "adjustment": 0
      },
      "value": 1
    },
    {
      "step": "complexity",
      "factor_id": "complexity",
      "input": {
        "source": "asked",
        "selected": "simple",
        "hours": 1
      },
      "value": 1
    },
    {
      "step": "complexity_hours",
      "factor_id": "complexity",
      "input": {
        "multiplier": 1
      },
      "value": 1
    },
    {
      "step": "skipped",
      "factor_id": "wraparound",
      "input": {
        "skipped": true,
        "reason": "disabled",
        "question_ids": []
      },
      "value": 1,
      "skipped": true,
      "reason": "disabled"
    },
    {
      "step": "hours_raw",
      "factor_id": "hours",
      "input": {
        "pricing_mode": "hourly"
      },
      "value": 1
    },
    {
      "step": "hours_estimate",
      "factor_id": "hours",
      "input": {
        "raw": 1,
        "decimal_places": 1,
        "formula_a": 1,
        "formula_b": "1.0"
      },
      "value": 1
    },
    {
      "step": "tier",
      "factor_id": "tier",
      "input": {
        "source": "fallback_standard",
        "complexity": "simple",
        "style": "blackgrey",
        "rule_id": null,
        "keywords": [],
        "used": true
      },
      "value": "standard"
    },
    {
      "step": "rate",
      "factor_id": "rate",
      "input": {
        "pricing_mode": "hourly"
      },
      "value": {
        "amount": 200,
        "source": "tier:standard"
      }
    },
    {
      "step": "style",
      "factor_id": "style",
      "input": {
        "source": "asked",
        "selected": "blackgrey"
      },
      "value": 1
    },
    {
      "step": "subtotal",
      "factor_id": "pricing",
      "input": {
        "hours": 1,
        "rate": 200,
        "style_multiplier": 1
      },
      "value": 200
    },
    {
      "step": "shop_minimum",
      "factor_id": "minimum",
      "input": {
        "subtotal": 200,
        "minimum": 200,
        "applied": false
      },
      "value": 200
    },
    {
      "step": "total",
      "factor_id": "pricing",
      "input": {
        "by_piece": false
      },
      "value": 200
    },
    {
      "step": "range_band",
      "factor_id": "range",
      "input": {
        "total": 200,
        "low_multiplier": 0.85,
        "high_multiplier": 1.15
      },
      "value": {
        "low": 170,
        "high": 229.99999999999997
      }
    },
    {
      "step": "range",
      "factor_id": "range",
      "input": {
        "low": 170,
        "high": 229.99999999999997,
        "rounding": {
          "increment": 50,
          "mode": "nearest"
        }
      },
      "value": {
        "low": 150,
        "high": 250
      }
    },
    {
      "step": "deposit_base",
      "factor_id": "deposit",
      "input": {
        "basis": "low",
        "base": 150,
        "percentage": 0.3
      },
      "value": 45
    },
    {
      "step": "deposit_raw",
      "factor_id": "deposit",
      "input": {
        "basis": "low"
      },
      "value": 45
    },
    {
      "step": "deposit",
      "factor_id": "deposit",
      "input": {
        "mode": "pct_of_low_30",
        "unrounded": 45,
        "increment": 25,
        "minimum": 0
      },
      "value": 50
    },
    {
      "step": "skipped",
      "factor_id": "sessions",
      "input": {
        "skipped": true,
        "reason": "disabled",
        "question_ids": []
      },
      "value": 0,
      "skipped": true,
      "reason": "disabled"
    },
    {
      "step": "skipped",
      "factor_id": "cover_up",
      "input": {
        "skipped": true,
        "reason": "disabled",
        "question_ids": []
      },
      "value": 0,
      "skipped": true,
      "reason": "disabled"
    },
    {
      "step": "skipped",
      "factor_id": "multi_area",
      "input": {
        "skipped": true,
        "reason": "disabled",
        "question_ids": []
      },
      "value": 0,
      "skipped": true,
      "reason": "disabled"
    },
    {
      "step": "skipped",
      "factor_id": "city_rate",
      "input": {
        "skipped": true,
        "reason": "disabled",
        "question_ids": []
      },
      "value": 0,
      "skipped": true,
      "reason": "disabled"
    },
    {
      "step": "skipped",
      "factor_id": "non_usd",
      "input": {
        "skipped": true,
        "reason": "disabled",
        "question_ids": []
      },
      "value": 0,
      "skipped": true,
      "reason": "disabled"
    },
    {
      "step": "sessions",
      "factor_id": "sessions",
      "input": {
        "hours": 1,
        "provisional": true
      },
      "value": {
        "count": 1,
        "hours_per_session": 1
      }
    },
    {
      "step": "expiry",
      "factor_id": "expiry",
      "input": {
        "now": "2026-11-05T12:00:00.000Z",
        "expiry_days": 7
      },
      "value": "2026-11-12T12:00:00.000Z"
    }
  ],
  "review_status": "VALIDATED_PENDING_REVIEW",
  "consent": {
    "share_for_pricing_research": false,
    "anonymize": true,
    "captured_at": "2026-11-05T12:00:00Z"
  },
  "anonymized": false
}

Raw Estimate schema · Browse all schemas

Quote

A Quote is a binding offer issued only after artist review at QUOTED. It records the price range, sessions, deposit amount and policies, valid_until, assumptions, confidence, price provenance and calculation trail.

Quote fields. Nested required fields are required when their parent is present.
FieldTypeRequiredDescription
pack_idstringYesIndustry pack identifier. Minimum length: 1.
pack_versionstringYesSelected pack version; must agree across related objects. Minimum length: 1.
answersobjectYesExact raw question answers. Keep contact details in a separate channel; free text still needs privacy checks. Known contact-information keys are rejected. Map values may be any JSON value.
artist_review_modeenumYesBusiness review preference; both modes return a non-binding Estimate before a binding Quote. Allowed: pre_price, post_price.
price_rangeobjectYesRequired on every Estimate and Quote, including pending review. Unknown properties are rejected.
price_range.minnumberYesNonnegative lower bound; application must check min ≤ max. Minimum: 0.
price_range.maxnumberYesNonnegative upper bound. Minimum: 0.
price_range.currencystringYesThree-letter uppercase currency code. Pattern: ^[A-Z]{3}$.
price_range.basisenumYesPricing basis: hourly, day_rate, from or fixed; deposit basis is pack-defined. Allowed: hourly, day_rate, from, fixed.
price_range.methodenumYesPrice method: artist_stated, rule or logged_quotes; logged_quotes requires n ≥ 5. Allowed: artist_stated, rule, logged_quotes.
price_range.nintegerNoNumber of logged quotes supporting the price; at least five for logged_quotes. Minimum: 5.
price_range.provenanceobjectYesSource of settings or price, including who stated the price and when. Unknown properties are rejected.
price_range.provenance.whostringYesWho stated the price. Minimum length: 1.
price_range.provenance.whenstringYesWhen the price was stated. Format: date-time.
sessionsobjectYesSession estimate; provisional in the tattoo reference evaluator. Unknown properties are rejected.
sessions.countintegerYesNonnegative session count. Minimum: 0.
sessions.hours_per_sessionnumberYesNonnegative estimated hours per session. Minimum: 0.
depositobjectYesPack-defined field. Provider is merchant of record; UQP never holds funds. Only processor confirmation marks a deposit paid. Unknown properties are rejected.
deposit.amountnumberYesNonnegative deposit amount. Minimum: 0.
deposit.basisstringYesPricing basis: hourly, day_rate, from or fixed; deposit basis is pack-defined. Minimum length: 1.
deposit.policiesarray of stringYesDeposit policies the client must explicitly approve before payment handoff. Minimum items: 1. Item constraints: Minimum length: 1.
deposit.statusenumYesDeposit remains unpaid until processor confirmation. Allowed: unpaid, paid.
deposit.merchant_of_recordany JSONYesThe provider, never UQP.
deposit.confirmationobjectNoPayment processor confirmation; client self-report is insufficient. Unknown properties are rejected.
deposit.confirmation.sourceany JSONYesPayment confirmation must originate from the payment processor.
deposit.confirmation.referencestringYesProcessor confirmation reference. Minimum length: 1.
deposit.confirmation.confirmed_atstringYesWhen the payment processor confirmed payment. Format: date-time.
deposit.provenanceobjectYesSource of settings or price, including who stated the price and when. Unknown properties are rejected.
deposit.provenance.whostringYesWho stated the price. Minimum length: 1.
deposit.provenance.whenstringYesWhen the price was stated. Format: date-time.
assumptionsarray of stringYesAssumptions and limitations that accompany the estimate.
intermediatesarray of objectYesOrdered audit trail of every evaluation step, including disabled factors. Minimum items: 1. Item constraints: Unknown properties are rejected.
intermediates[].stepstringYesEvaluator step name. Minimum length: 1.
intermediates[].factor_idstringYesFactor responsible for this step. Minimum length: 1.
intermediates[].inputany JSONYesOriginal input for the calculation; any JSON value.
intermediates[].valueany JSONYesPack-defined value; any JSON value is permitted.
intermediates[].skippedany JSONNoPack-defined field.
intermediates[].reasonstringNoPack-defined field.
review_statusenumYesCurrent lifecycle state; allowed transitions must be enforced by the consumer. Allowed: QUOTED, ACCEPTED, DECLINED, EXPIRED.
consentobjectYesExplicit research-sharing and anonymization preferences, with capture time. Unknown properties are rejected.
consent.share_for_pricing_researchbooleanYesWhether sharing for pricing research was authorized.
consent.anonymizebooleanYesRequested anonymization preference, not proof that the artifact is anonymous.
consent.captured_atstringYesWhen consent was recorded. Format: date-time.
anonymizedbooleanYesWhether the delivered artifact actually is anonymized, independently of the requested preference.
valid_untilstringYesBinding Quote validity deadline; EXPIRED only past this timestamp. Format: date-time.
kindany JSONYesDiscriminator: estimate or quote.
bindingany JSONYesFalse for Estimate; true only for an artist-reviewed Quote.
confidenceenumYesConfidence in this estimate or reviewed offer; not a guarantee. Allowed: low, medium, high.
client_approvalobjectNoExplicit client approval required for ACCEPTED, matching the payment amount, currency and policies. Unknown properties are rejected.
client_approval.amountnumberYesNonnegative deposit amount. Minimum: 0.
client_approval.currencystringYesThree-letter uppercase currency code. Pattern: ^[A-Z]{3}$.
client_approval.policiesarray of stringYesDeposit policies the client must explicitly approve before payment handoff. Minimum items: 1. Item constraints: Minimum length: 1.
client_approval.approved_amountany JSONYesMust be true to accept the payment amount.
client_approval.approved_policiesany JSONYesMust be true to accept the deposit policies.
client_approval.approved_atstringYesTimestamp of explicit client approval. Format: date-time.
Quote · Tattoo pack 0.1.0. Example data, not real prices.
Quote · Tattoo pack 0.1.0
{
  "kind": "quote",
  "binding": true,
  "confidence": "low",
  "pack_id": "tattoo",
  "pack_version": "0.1.0",
  "answers": {
    "description": "A small geometric mountain design with clean outlines.",
    "size": "sm",
    "placement": [
      "Lower Arm"
    ],
    "complexity": "simple",
    "style": "blackgrey"
  },
  "artist_review_mode": "post_price",
  "price_range": {
    "min": 150,
    "max": 250,
    "currency": "USD",
    "basis": "hourly",
    "method": "rule",
    "provenance": {
      "who": "Example artist (synthetic review)",
      "when": "2026-11-05T12:00:00Z"
    }
  },
  "sessions": {
    "count": 1,
    "hours_per_session": 1
  },
  "deposit": {
    "amount": 50,
    "basis": "low",
    "provenance": {
      "who": "PricingModel tattoo@0.1.0 (example rules, unvalidated)",
      "when": "2026-11-05T12:00:00.000Z"
    },
    "policies": [
      "Example data, not real prices; provider must state final deposit policies before artist review."
    ],
    "status": "unpaid",
    "merchant_of_record": "provider"
  },
  "assumptions": [
    "tattoo.co current constant, unvalidated; supplied handoff attributed to f018b86, not independently revalidated.",
    "Pricing uses unrounded hours; display logs both Formula A Math.round(t*10)/10 and Formula B toFixed(1).",
    "Sessions are a provisional single block (zero for by_piece); session splitting and caps are not modeled.",
    "Multiple placements trigger any-tricky once; area summing is not modeled.",
    "Raw answers are preserved; anonymized=false because free text has not been deidentified."
  ],
  "intermediates": [
    {
      "step": "pricing_mode",
      "factor_id": "pricing",
      "input": "hourly",
      "value": "hourly"
    },
    {
      "step": "input_resolved",
      "factor_id": "complexity",
      "input": {
        "source": "asked",
        "rule_id": null,
        "keywords": []
      },
      "value": "simple"
    },
    {
      "step": "input_resolved",
      "factor_id": "style",
      "input": {
        "source": "asked",
        "rule_id": null,
        "keywords": []
      },
      "value": "blackgrey"
    },
    {
      "step": "input_resolved",
      "factor_id": "size",
      "input": {
        "source": "asked",
        "rule_id": null,
        "keywords": []
      },
      "value": "sm"
    },
    {
      "step": "input_resolved",
      "factor_id": "placement",
      "input": {
        "source": "asked",
        "rule_id": null,
        "keywords": []
      },
      "value": [
        "Lower Arm"
      ]
    },
    {
      "step": "input_resolved",
      "factor_id": "tier",
      "input": {
        "source": "configured",
        "rule_id": null,
        "keywords": []
      },
      "value": "fallback_standard"
    },
    {
      "step": "input_resolved",
      "factor_id": "deposit",
      "input": {
        "source": "configured",
        "rule_id": null,
        "keywords": []
      },
      "value": "pct_of_low_30"
    },
    {
      "step": "dimensions",
      "factor_id": "size",
      "input": {
        "raw_size": "sm",
        "width": null,
        "height": null
      },
      "value": {
        "width": 3,
        "height": 3
      }
    },
    {
      "step": "area",
      "factor_id": "size",
      "input": {
        "width": 3,
        "height": 3
      },
      "value": 9
    },
    {
      "step": "base_hours",
      "factor_id": "size",
      "input": {
        "area": 9,
        "area_per_hour": 12,
        "minimum_hours": 1
      },
      "value": 1
    },
    {
      "step": "placement",
      "factor_id": "placement",
      "input": {
        "hours": 1,
        "placements": [
          "Lower Arm"
        ],
        "mode": "additive_0_5h",
        "match": "substring",
        "tricky": false,
        "adjustment": 0
      },
      "value": 1
    },
    {
      "step": "complexity",
      "factor_id": "complexity",
      "input": {
        "source": "asked",
        "selected": "simple",
        "hours": 1
      },
      "value": 1
    },
    {
      "step": "complexity_hours",
      "factor_id": "complexity",
      "input": {
        "multiplier": 1
      },
      "value": 1
    },
    {
      "step": "skipped",
      "factor_id": "wraparound",
      "input": {
        "skipped": true,
        "reason": "disabled",
        "question_ids": []
      },
      "value": 1,
      "skipped": true,
      "reason": "disabled"
    },
    {
      "step": "hours_raw",
      "factor_id": "hours",
      "input": {
        "pricing_mode": "hourly"
      },
      "value": 1
    },
    {
      "step": "hours_estimate",
      "factor_id": "hours",
      "input": {
        "raw": 1,
        "decimal_places": 1,
        "formula_a": 1,
        "formula_b": "1.0"
      },
      "value": 1
    },
    {
      "step": "tier",
      "factor_id": "tier",
      "input": {
        "source": "fallback_standard",
        "complexity": "simple",
        "style": "blackgrey",
        "rule_id": null,
        "keywords": [],
        "used": true
      },
      "value": "standard"
    },
    {
      "step": "rate",
      "factor_id": "rate",
      "input": {
        "pricing_mode": "hourly"
      },
      "value": {
        "amount": 200,
        "source": "tier:standard"
      }
    },
    {
      "step": "style",
      "factor_id": "style",
      "input": {
        "source": "asked",
        "selected": "blackgrey"
      },
      "value": 1
    },
    {
      "step": "subtotal",
      "factor_id": "pricing",
      "input": {
        "hours": 1,
        "rate": 200,
        "style_multiplier": 1
      },
      "value": 200
    },
    {
      "step": "shop_minimum",
      "factor_id": "minimum",
      "input": {
        "subtotal": 200,
        "minimum": 200,
        "applied": false
      },
      "value": 200
    },
    {
      "step": "total",
      "factor_id": "pricing",
      "input": {
        "by_piece": false
      },
      "value": 200
    },
    {
      "step": "range_band",
      "factor_id": "range",
      "input": {
        "total": 200,
        "low_multiplier": 0.85,
        "high_multiplier": 1.15
      },
      "value": {
        "low": 170,
        "high": 229.99999999999997
      }
    },
    {
      "step": "range",
      "factor_id": "range",
      "input": {
        "low": 170,
        "high": 229.99999999999997,
        "rounding": {
          "increment": 50,
          "mode": "nearest"
        }
      },
      "value": {
        "low": 150,
        "high": 250
      }
    },
    {
      "step": "deposit_base",
      "factor_id": "deposit",
      "input": {
        "basis": "low",
        "base": 150,
        "percentage": 0.3
      },
      "value": 45
    },
    {
      "step": "deposit_raw",
      "factor_id": "deposit",
      "input": {
        "basis": "low"
      },
      "value": 45
    },
    {
      "step": "deposit",
      "factor_id": "deposit",
      "input": {
        "mode": "pct_of_low_30",
        "unrounded": 45,
        "increment": 25,
        "minimum": 0
      },
      "value": 50
    },
    {
      "step": "skipped",
      "factor_id": "sessions",
      "input": {
        "skipped": true,
        "reason": "disabled",
        "question_ids": []
      },
      "value": 0,
      "skipped": true,
      "reason": "disabled"
    },
    {
      "step": "skipped",
      "factor_id": "cover_up",
      "input": {
        "skipped": true,
        "reason": "disabled",
        "question_ids": []
      },
      "value": 0,
      "skipped": true,
      "reason": "disabled"
    },
    {
      "step": "skipped",
      "factor_id": "multi_area",
      "input": {
        "skipped": true,
        "reason": "disabled",
        "question_ids": []
      },
      "value": 0,
      "skipped": true,
      "reason": "disabled"
    },
    {
      "step": "skipped",
      "factor_id": "city_rate",
      "input": {
        "skipped": true,
        "reason": "disabled",
        "question_ids": []
      },
      "value": 0,
      "skipped": true,
      "reason": "disabled"
    },
    {
      "step": "skipped",
      "factor_id": "non_usd",
      "input": {
        "skipped": true,
        "reason": "disabled",
        "question_ids": []
      },
      "value": 0,
      "skipped": true,
      "reason": "disabled"
    },
    {
      "step": "sessions",
      "factor_id": "sessions",
      "input": {
        "hours": 1,
        "provisional": true
      },
      "value": {
        "count": 1,
        "hours_per_session": 1
      }
    },
    {
      "step": "expiry",
      "factor_id": "expiry",
      "input": {
        "now": "2026-11-05T12:00:00.000Z",
        "expiry_days": 7
      },
      "value": "2026-11-12T12:00:00.000Z"
    }
  ],
  "review_status": "QUOTED",
  "consent": {
    "share_for_pricing_research": false,
    "anonymize": true,
    "captured_at": "2026-11-05T12:00:00Z"
  },
  "anonymized": false,
  "valid_until": "2026-11-12T12:00:00.000Z"
}

Raw Quote schema · Browse all schemas

Review and validation boundaries

Both pre_price and post_price review modes return a non-binding Estimate with a required price_range. Sessions and deposit remain required before review; provisional values must be explained in assumptions. Only artist review can issue a binding Quote.

Validate that min does not exceed max, that pack versions agree across documents and that questions and factors belong to the selected pack. JSON Schema does not execute pricing formulas or resolve model references.

Preserve raw answers such as size: "unsure". Never include contact details in answers. Known contact keys are rejected, but schema validation cannot find personal information inside arbitrary free text. Requested anonymization is not proof of deidentification; the Tattoo evaluator leaves anonymized false.

Lifecycle

The state names and every allowed edge below come from lifecycle.json. Consumers must enforce transitions: the schema validates state strings, while its transitions annotation is not an executable validation rule.

UQP lifecycle: every allowed transitionEach row connects a source state to all permitted destinations. The complete transition list follows the diagram.INITIATED→ MANIFEST_ISSUEDMANIFEST_ISSUED→ PAYLOAD_SUBMITTEDPAYLOAD_SUBMITTED→ MUTATION_REQUIRED / VALIDATED_PENDING_REVIEWMUTATION_REQUIRED→ PAYLOAD_SUBMITTEDVALIDATED_PENDING_REVIEW→ QUOTED / MUTATION_REQUIREDDECLINEDQUOTED→ ACCEPTED / DECLINEDEXPIREDACCEPTEDTerminal — no outgoing transitionsDECLINEDTerminal — no outgoing transitionsEXPIREDTerminal — no outgoing transitions
Lifecycle transitions, sourced directly from lifecycle.json.
Text equivalent: every lifecycle transition
  • INITIATED → MANIFEST_ISSUED
  • MANIFEST_ISSUED → PAYLOAD_SUBMITTED
  • PAYLOAD_SUBMITTED → MUTATION_REQUIRED VALIDATED_PENDING_REVIEW
  • MUTATION_REQUIRED → PAYLOAD_SUBMITTED
  • VALIDATED_PENDING_REVIEW → QUOTED MUTATION_REQUIRED DECLINED
  • QUOTED → ACCEPTED DECLINED EXPIRED
  • ACCEPTED → terminal; no outgoing transitions
  • DECLINED → terminal; no outgoing transitions
  • EXPIRED → terminal; no outgoing transitions

The repository’s spec/lifecycle.ts exports isLifecycleState, canTransition and transition. Illegal transitions throw. QUOTED requires artist review and a binding Quote with valid_until. ACCEPTED requires explicit client approval of the payment amount and policies before checkout handoff. EXPIRED is permitted only past valid_until. The intended checkout boundary is described under transports and checkout.

A price is always present

Every Estimate and Quote requires price_range with min, max and currency. Its basis is hourly, day_rate, from or fixed; method is artist_stated, rule or logged_quotes. Every price includes provenance: who stated it and when. logged_quotes requires n ≥ 5, enforced by the schema. A pending review never permits an absent price. Confidence and assumptions communicate uncertainty without inventing a final offer.

Motivation: UCP issue #877, “Catalog items with no public price (price on request)” describes the required catalog-price problem. This is an attributed issue report, not a normative UCP requirement or proof of UQP interoperability.

Two stages, then checkout handoff

INITIATED → MANIFEST_ISSUED → PAYLOAD_SUBMITTED returns the non-binding Estimate computed from PricingModel. Incomplete inputs lead to MUTATION_REQUIRED and resubmission; validated inputs lead to VALIDATED_PENDING_REVIEW. After artist review, QUOTED carries the binding Quote and valid_until. The client may explicitly approve the amount and policies to reach ACCEPTED, decline to reach DECLINED, or the offer becomes EXPIRED past valid_until. ACCEPTED is the handoff boundary, not a new handoff state.

Agents poll for status or receive webhooks; no long-held calls wait for artist review. The local reference uses polling; webhook delivery is deferred. UQP runs up to the binding Quote; payment is intended to use UCP or ACP checkout. No checkout adapter is implemented here.

A binding UQP Quote is intended to map 1:1 to the proposed dev.ucp.common.quote extension in UCP issue #929. That open proposal describes business-authoritative quotation on Cart and Checkout. Estimate is UQP’s pre-quote layer. This is an intended mapping to a proposal, not an implemented integration or UCP conformance claim.

Deposit and explicit approval

Deposit includes an amount, policies and provenance: who stated it and when. Before payment handoff, the client must explicitly approve that amount and those policies; merely submitting answers or viewing a quote is not acceptance. A deposit counts as paid only when the payment processor confirms it, for example through an authenticated processor webhook. Client self-report never completes payment. The provider is merchant of record; UQP never holds funds. Processor confirmation is not another quoting lifecycle state.

Versioning

The core is 0.1-draft; the Tattoo pack is 0.1.0. Pack version and core draft are separate. Every core object carries its selected pack_id and pack_version. Do not assume future drafts are backward compatible: pin a repository revision and pack version, and revalidate before updating.

No stable compatibility policy or transport negotiation contract is published yet. Track changes in the changelog and discuss schema changes through co-design and contributions.

Structure credit

The information architecture is inspired by Universal Commerce Protocol (UCP); the marketing structure is inspired by Shopify’s UCP page. UQP uses its own wording. This is structural inspiration, not an endorsement or a claim of implemented interoperability.