UCP Types
Universal Commerce Protocol type definitions for agentic commerce
@guardrail-sim/ucp-types
Type definitions aligned with the Universal Commerce Protocol for agentic commerce integration.
Installation
npm install @guardrail-sim/ucp-typesOverview
This package provides TypeScript types aligned with UCP spec revision 2026-04-08, enabling guardrail-sim to integrate with the agentic commerce ecosystem. It includes:
- Discount types - Error codes, allocations, applied discounts and rejection messages
- Checkout types - Checkout sessions, line items, buyers, totals
- Cart, Order, Catalog and Identity Linking - the remaining UCP capabilities
- Profile / discovery - the
/.well-known/ucpdocument and capability negotiation - Converters - Functions to convert between policy engine results and UCP formats
Discount Types
DiscountErrorCode
Standard UCP error codes for discount validation.
import type { DiscountErrorCode } from '@guardrail-sim/ucp-types';
const code: DiscountErrorCode = 'discount_code_invalid';Available codes:
discount_code_invalid- Code doesn't exist or exceeds limitsdiscount_code_expired- Code is past validity windowdiscount_code_already_applied- Code already in usediscount_code_combination_disallowed- Can't combine with other codesdiscount_code_user_not_logged_in- Requires authenticationdiscount_code_user_ineligible- User doesn't qualify
DiscountMethod
How discounts are allocated across line items.
import type { DiscountMethod } from '@guardrail-sim/ucp-types';
const method: DiscountMethod = 'across'; // proportional
// or 'each' - even distributionAppliedDiscount
A successfully applied discount.
import type { AppliedDiscount } from '@guardrail-sim/ucp-types';
const discount: AppliedDiscount = {
code: 'SUMMER20',
title: 'Summer Sale 20%',
amount: { amount: 10000, currency: 'USD' },
method: 'across',
priority: 1,
allocations: [
{ target: '$.line_items[0]', amount: { amount: 6000, currency: 'USD' } },
{ target: '$.line_items[1]', amount: { amount: 4000, currency: 'USD' } },
],
};RejectedDiscount
A discount that was rejected.
import type { RejectedDiscount } from '@guardrail-sim/ucp-types';
const rejected: RejectedDiscount = {
code: 'INVALID50',
error_code: 'discount_code_invalid',
message: 'Discount exceeds maximum allowed',
};DiscountValidationResult
Result of pre-checkout validation.
import type { DiscountValidationResult } from '@guardrail-sim/ucp-types';
const result: DiscountValidationResult = {
valid: false,
error_code: 'discount_code_user_ineligible',
message: 'Volume tier requires quantity >= 100',
};Checkout Types
LineItem
A priced line in a checkout response. Note id and the totals[] breakdown — a line item
does not carry a subtotal: Money field.
import type { LineItem } from '@guardrail-sim/ucp-types';
const item: LineItem = {
id: 'li-8f2a1c04',
item: { id: 'SKU-001', title: 'Widget', price: 250000 },
quantity: 2,
totals: [{ type: 'subtotal', amount: 500000 }],
};LineItemRequest is the looser shape accepted on input: just item and quantity. The
item must carry an inline title and price — bare references cannot be priced.
Money and amounts
import type { Money } from '@guardrail-sim/ucp-types';
const price: Money = {
amount: 99900, // $999.00 in cents
currency: 'USD',
};Most amounts are plain integers, not Money
UCP states amounts as raw integers in minor units. Money exists as a wrapper where currency
context is useful, but Total.amount, AppliedDiscount.amount and DiscountAllocation.amount
are all plain number.
Total and the discount sign convention
import type { Total } from '@guardrail-sim/ucp-types';
const totals: Total[] = [
{ type: 'subtotal', amount: 750000 },
{ type: 'discount', amount: -75000 }, // negative
{ type: 'total', amount: 675000 },
];Discounts are negative in totals, positive in `applied`
As of UCP 2026-04-08, a discount entry in totals[] (and line_items[].totals[]) is negative
— it states the effect on the receipt. The same discount in discounts.applied[].amount is
positive — it states the discount's value. The two are deliberately opposite signs, and
conflating them double-counts the discount.
CheckoutWithDiscounts
A checkout session carrying the discount extension under its namespaced key.
import type { CheckoutWithDiscounts } from '@guardrail-sim/ucp-types';
const checkout: CheckoutWithDiscounts = {
id: 'c-1f8e...',
status: 'ready_for_complete',
currency: 'USD',
line_items: [
/* LineItem[] */
],
totals: [
{ type: 'subtotal', amount: 750000 },
{ type: 'discount', amount: -75000 },
{ type: 'total', amount: 675000 },
],
links: [],
payment: { handlers: [] },
'dev.ucp.shopping.discount': {
codes: ['SUMMER20'],
applied: [
/* AppliedDiscount[] */
],
messages: [
/* rejections, as warnings */
],
},
};CheckoutStatus is one of incomplete, ready_for_complete, in_progress, completed,
canceled or requires_escalation. There is no open status, and there are no
applied_discounts / rejected_discounts fields — rejections travel in messages[].
Spec Version and Capabilities
import {
UCP_SPEC_VERSION, // '2026-04-08'
SUPPORTED_UCP_VERSIONS, // ['2026-04-08', '2026-01-23', '2026-01-11']
CHECKOUT_CAPABILITY,
CART_CAPABILITY,
CATALOG_CAPABILITY,
ORDER_CAPABILITY,
DISCOUNT_EXTENSION,
FULFILLMENT_EXTENSION,
IDENTITY_LINKING_CAPABILITY,
GUARDRAIL_UCP_PROFILE,
serializeProfile,
} from '@guardrail-sim/ucp-types';
// The discount extension augments both checkout and cart as of 2026-04-08.
DISCOUNT_EXTENSION.extends;
// ['dev.ucp.shopping.checkout', 'dev.ucp.shopping.cart']GUARDRAIL_UCP_PROFILE is the /.well-known/ucp document this project advertises, built from
those constants so the spec version is stated exactly once. serializeProfile() returns it
exactly as the MCP server serves it.
Other Modules
| Module | Contents |
|---|---|
profile.ts | UCPProfile, compareVersions, negotiateCapabilities, profileSupportsCapability |
cart.ts | CartResponse, CartLineItem, create/update request types |
order.ts | Order, OrderFulfillment, OrderWebhookEvent, webhook signature headers |
identity-linking.ts | OAuth 2.0 flows: server metadata (RFC 8414), token/refresh/revocation types |
Converter Functions
toUCPErrorCode
Map a policy violation to a UCP error code. Takes the whole Violation, not a rule name —
it delegates to getUCPErrorCode in @guardrail-sim/policy-engine, which owns the single
mapping table.
import { toUCPErrorCode } from '@guardrail-sim/ucp-types';
const code = toUCPErrorCode({ rule: 'margin_floor', message: 'below floor' });
// 'discount_code_invalid'toDiscountValidationResult
Convert a policy evaluation into a validation result. The code argument is echoed back on
the result.
import { toDiscountValidationResult } from '@guardrail-sim/ucp-types';
const evaluation = await engine.evaluate(order, 0.3);
const result = toDiscountValidationResult(evaluation, 'SUMMER30');
// {
// code: 'SUMMER30',
// valid: false,
// error_code: 'discount_code_invalid',
// message: 'Calculated margin falls below 15% floor',
// limiting_factor: 'margin_floor'
// }buildDiscountExtensionResponse
Build a UCP discount extension response. The amount is a plain integer in minor units.
import { buildDiscountExtensionResponse } from '@guardrail-sim/ucp-types';
const response = buildDiscountExtensionResponse(
['SUMMER20'], // codes
evaluationResult, // policy result
75000, // discount amount in cents
'Summer Sale' // title
);
// Approved:
// { codes: ['SUMMER20'], applied: [{ code: 'SUMMER20', amount: 75000, ... }] }
//
// Rejected — note there is no `rejected` array; rejections are warnings:
// {
// codes: ['SUMMER20'],
// applied: [],
// messages: [{
// type: 'warning',
// code: 'discount_code_invalid',
// message: '...',
// field: 'dev.ucp.shopping.discount.codes'
// }]
// }calculateAllocations
Distribute a discount across line items. across is proportional to each line's subtotal;
each splits evenly. Amounts are plain integers.
import { calculateAllocations } from '@guardrail-sim/ucp-types';
import type { LineItem } from '@guardrail-sim/ucp-types';
const lineItems: LineItem[] = [
{
id: 'li-a',
item: { id: 'A', title: 'Widget', price: 150000 },
quantity: 2,
totals: [{ type: 'subtotal', amount: 300000 }],
},
{
id: 'li-b',
item: { id: 'B', title: 'Gadget', price: 200000 },
quantity: 1,
totals: [{ type: 'subtotal', amount: 200000 }],
},
];
const allocations = calculateAllocations(50000, lineItems, 'across');
// [
// { target: '$.line_items[0]', amount: 30000 },
// { target: '$.line_items[1]', amount: 20000 }
// ]fromUCPLineItems
Convert UCP line items to the policy engine's Order. The option key is productMargin.
import { fromUCPLineItems, DEFAULT_PRODUCT_MARGIN } from '@guardrail-sim/ucp-types';
const order = fromUCPLineItems(lineItems, {
productMargin: 0.4, // 40% base margin
customerSegment: 'gold',
});
// { order_value: 500000, quantity: 3, product_margin: 0.4, customer_segment: 'gold' }Margin is an assumption, not a measurement
UCP line items carry price but not cost, so margin cannot be derived from a cart. Omitting
productMargin falls back to DEFAULT_PRODUCT_MARGIN (0.3), which is exported so you can see the
number you are inheriting. Pass your real margin whenever you know it.
Usage with MCP Server
The MCP server's UCP-aligned tools use these types internally:
// validate_discount_code returns DiscountValidationResult
{
"code": "SUMMER30",
"valid": false,
"error_code": "discount_code_invalid",
"message": "Calculated margin falls below 15% floor",
"limiting_factor": "margin_floor",
"max_allowed": 75000
}
// simulate_checkout_discount returns DiscountExtensionResponse
// (plus `currency` and `allocations`). Rejections are messages, not a `rejected` array.
{
"codes": ["SUMMER20"],
"applied": [],
"messages": [
{
"type": "warning",
"code": "discount_code_invalid",
"message": "Calculated margin falls below 15% floor",
"field": "dev.ucp.shopping.discount.codes"
}
],
"currency": "USD"
}See Also
- MCP Tools Reference - UCP-aligned MCP tools
- UCP Concepts - UCP integration overview
- ADR-002: UCP Alignment - Design decision
- ADR-003: UCP Spec Review - Superseded gap analysis
- ADR-004: MCP 2026-07-28 - Protocol migration