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/ucpA 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:
| Outcome | Status |
|---|---|
invalid_profile_url | 400 |
version_unsupported | 422 |
capabilities_incompatible | 200 (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 bykidfrom the platform profilesigning_keys, verifies the signature, requiresexp(missing/past →mandate_expired) and ajti, requires themerchantbinding to equal the store slug, confirms the embedded terms (id/total/currency) equal the live re-priced quote, and re-verifies the embeddedmerchant_authorizationagainst the store key. - Verifies an optional
payment_mandateover the same priced charge. - Rejects replays: a
jtialready 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_idis 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.stringifyis already canonical. Direct UCP clients must reproduce this canonicalization byte-for-byte — the@artos-commerce/ucp-clientSDK does this for you (canonicalJson, exercised against frozen parity vectors), soAp2Signermandates andverifyMerchantAuthorizationverify out of the box. Hand-built clients reproduce it themselves. payment_mandaterides underap2.payment_mandate, notpayment.instruments[*].credential.token(which already carries the chargeable rail token — a Stripepm_…or a Sui tx digest).- No PSP
payment_mandateconsumption (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 .