Skip to Content
Profiles & trust

Profiles & trust

UCP agents and stores publish profiles at /.well-known/ucp declaring their protocol version, capabilities, signing keys, and (for stores) enabled payment handlers.

Store profiles

# per-store profile (signing keys, payment_handlers, capabilities) curl https://api.artos.sh/s/energy-sport/.well-known/ucp # platform apex (no store resolved) — advertises the global catalog profile curl https://api.artos.sh/.well-known/ucp

A store profile carries signing_keys (ES256 JWKs) used to verify the store’s merchant_authorization on checkout terms and to verify order webhooks. It’s cacheable (Cache-Control: public, max-age=300).

Agent profile

Your agent publishes its own profile (signing keys + capabilities). On Path A the hosted agent profile is at profile-artos.vercel.app/.well-known/ucp . On Path B, the URL you publish is what you pass as meta["ucp-agent"].profile (MCP) or the UCP-Agent header (REST).

Capability negotiation

When a request carries the agent profile, the API fetches it, validates the protocol version, and computes the capability intersection between agent and store. Outcomes follow the spec:

OutcomeStatus
invalid_profile_url400
version_unsupported422
capabilities_incompatible200 (business-outcome error envelope)

Two cases are handled leniently rather than rejected: no UCP-Agent header (anonymous browsing) and a momentarily unreachable profile — both fall back to the store’s own capabilities. The bridge handles negotiation for Path A; Direct UCP clients implement discovery and pass their profile themselves.

Trust tiers

See Authentication → Trust tiers for the enforced matrix (anonymous / token / signed). The signed tier is reached with a valid RFC 9421 HTTP Message Signature.

AP2 conformance

Artos implements the UCP AP2 Mandates extension with a few deliberate, documented deviations. What the server enforces on an AP2-negotiated complete_checkout:

  • Verifies the checkout_mandate (compact ES256 JWS): resolves the agent key by kid from the platform profile signing_keys, verifies the signature, requires exp (missing/past → mandate_expired) and a jti, requires the merchant binding to equal the store slug, confirms the embedded terms (id / total / currency) equal the live re-priced quote, and re-verifies the embedded merchant_authorization against the store key.
  • Verifies an optional payment_mandate over the same priced charge.
  • Rejects replays: a jti already recorded is refused; jtis are recorded only after a successful completion (a decline does not burn the credential).
  • Enforces the server-side allowance on all rails: when a payment_mandate_id is present, the referenced allowance is checked up front and consumed once on settlement (including the hosted Stripe webhook), with no double-consume.

Deliberate deviations

  • Compact ES256 JWS, not SD-JWT+kb. No selective disclosure or holder key-binding JWT; the verifier validates the issuer JWT only. The wire form is header..signature (empty middle segment).
  • Sorted-key JSON, not RFC 8785 (JCS). No JCS number canonicalization — safe because every monetary value is an integer (minor units) and no floats are emitted, so JSON.stringify is already canonical. Direct UCP clients must reproduce this canonicalization byte-for-byte — the @artos-commerce/ucp-client SDK does this for you (canonicalJson, exercised against frozen parity vectors), so Ap2Signer mandates and verifyMerchantAuthorization verify out of the box. Hand-built clients reproduce it themselves.
  • payment_mandate rides under ap2.payment_mandate, not payment.instruments[*].credential.token (which already carries the chargeable rail token — a Stripe pm_… or a Sui tx digest).
  • No PSP payment_mandate consumption (BYOK Stripe). It is verified and audited, and spending limits are enforced via the server-side allowance rather than the card network.

The full server-side reference is ap2/CONFORMANCE.md .

Last updated on