Skip to content

Vendor manifest spec 0.2

An open specification for how an API vendor tells an agent's account layer what it sells, which documents a human must accept, and how an account is created with that human's signed consent. Three JSON Schemas, a reference endpoint and a conformance check, under the Apache-2.0 license.

Discovery

  1. https://{vendor-domain}/.well-known/suffect.json: the manifest this spec defines.
  2. Optional: the vendor's MCP server lists Suffect's authorization server in the authorization_servers array of its RFC 9728 protected-resource metadata, so a client that hits the vendor's 401 lands on the consent sheet.
  3. Optional: an entry in the vendor's Agentic Resource Discovery ai-catalog.json pointing at the manifest.

The manifest must be served from the vendor's own domain over HTTPS. Suffect refuses a manifest served from a third-party host, so a lookalike cannot speak for a vendor.

The manifest

suffectyesthe spec version the manifest is written to; minor versions are additive
provideryesname, domain, legal name, support URL and kind
termsyesevery document a human must accept: kind, URL, version, the sha256 of the normalized text, what it requires of the human, the notices it carries and the clauses those rest on
pricingnocurrency, plans, packs and the pricing page
signupnothe level, the endpoint and its authentication, the contact fields, idempotency
credentialnotype, header, scheme, scopes, rotation and the proxy base URL
budgetnoa webhook for budget events, and whether usage events are sent
railsnocard, mpp or x402
contactnosecurity and partnerships addresses
resourcesnooptional: each resource with its own access, pricing, acquisition and, when it has them, terms; without it the manifest describes one account resource acquired through signup

What a manifest declares decides what Suffect can do at the vendor: terms alone let the sheet name the documents by version and hash; terms and pricing make the sheet exact; a signup endpoint makes the signup one call; rails including mpp or x402 will add per-request access once Suffect's payer rails are built. The conformance check prints the level a manifest earns. What each brings, restated for vendors, is on the vendors page.

How a document is hashed

A terms sha256 is computed over the normalized text: CRLF and CR become LF, runs of spaces and tabs collapse to one space, each line is trimmed, empty lines are dropped, lines join with LF. An HTML page's text is its main content first (the first main element, else the first element with role="main", else the first article, else the body) with scripts, styles, the head and the page's header, navigation and footer dropped, block boundaries as line breaks and entities decoded; a PDF's text is its pages in order. The reference implementations are normalizeTerms and stripHtml in the spec package, the one extractor Suffect's snapshot job and its Worker both run, so a vendor and Suffect compute the same hash for the same page.

The signup request

POST {signup.endpoint} with Content-Type: application/json, an Idempotency-Key header and a request signature (below). The body:

suffectyesthe spec version
request_idyesunique per vendor; the idempotency key
consentyesthe human's signed consent record (the third schema)
attestationyesa JWS Suffect signs over the record's sha256, the request id, your domain and a five-minute expiry
requestedyesthe plan, packs and budget the human approved
contactyesthe fields your manifest lists: an alias email or the human's own, name, country
credentialyesthe type and scopes the account's key should carry
paymentnothe rail and, for a card, the issuer's network token
resourcenothe account resource's slug when the manifest lists more than one
201 Created
the account id, the credential (type, value, scopes, when it rotates), the accepted terms by version and hash, and a pricing echo
202 Accepted
a status URL; Suffect polls until succeeded, failed or action required with a URL for the human
409 Conflict
account_exists, with the existing account id for the same human when the vendor can say
422
terms_version_stale, plan_unavailable, budget_below_minimum, representation_missing or country_unsupported
403
attestation_invalid

The consent record

What a human approved, signed twice: by the human's passkey and by Suffect's Ed25519 key, and chained to the human's previous record. The attestation that accompanies a signup binds this record's hash to that one request at that one vendor for five minutes; a vendor verifies both against the key set at https://suffect.com/.well-known/jwks.json and keeps the record with the account.

typeyesthe record's type name
versionyesthe record format's version
idyesthe record's id
issued_atyeswhen it was issued
issueryesSuffect, with the key id of its countersignature
subjectyesthe human, by a stable id, with the representations they made once
approveryesthe passkey that approved, and when
agentyesthe OAuth client the approval covers
provideryesthe vendor's domain and name
scopeyesthe resources, actions and amounts the approval allows, exactly
termsyesevery terms snapshot the record accepts, by kind, URL, version and sha256
commercialyesthe plan or pack and its price
budgetyesthe cap and its period
data_sharedyesname, alias email or own email, country
assent_textyesthe sentence the human approved
notices_shownyesevery notice the sheet printed
clauses_shownnothe clauses printed under the notices, each keyed to a document in terms
proofsyesthe passkey's signature and Suffect's Ed25519 countersignature
prevyesthe hash of the human's previous record

Request signatures

Every request Suffect's automation sends a vendor (signup requests, reads of a manifest and the terms it cites, and the runner's browser on the vendor's hosts) carries an RFC 9421 HTTP Message Signature (Ed25519) over the authority and the Signature-Agent header, with a five-minute window, a fresh nonce and the tag web-bot-auth, in the profile Cloudflare's verifier takes. The agent's API calls through the gateway carry the vendor's key, not a signature. Signature-Agent names the origin that publishes the key directory, a JWK Set of Ed25519 public keys each proven by a signature over the directory's authority, with the key id as the key's thumbprint. A signature proves that the holder of a published key sent this request to this host within this window, and nothing about the human; the record and the attestation name the human.

Replay and rotation

  • request_id is unique per vendor; the attestation expires after five minutes; signup requests carry idempotency keys.
  • A request signature lives five minutes with a fresh nonce; a verifier that wants replay protection inside the window keeps the nonces it has seen for as long. A signature authorizes nothing by itself.
  • The attestation a browser carries is refreshed at most every thirty seconds per job and only while the job is open; a captured one reads the human's record for the rest of its five minutes and nothing more.
  • Attestation keys rotate with overlap, kid naming the key. Request-signing keys rotate through the directory: published and proven before anything signs with them, kept listed for the directory's max-age plus the longest signature life, then dropped.
  • Vendors should rate-limit the signup endpoint per subject and per IP, and treat the alias email as the customer's contact. Suffect sends only the contact fields the manifest lists.

Conformance

suffect-spec check <domain> fetches the manifest from the vendor's own domain and validates it against the schema (manifest), hashes every document the manifest cites against the live page (terms:<kind>, and terms:<resource>:<kind> for a resource's own documents), holds every quoted clause to the fetched text (clauses:<kind>), and exercises the signup endpoint (endpoint). Suffect's own verification runs the same check and then snapshots each document, so a vendor sees the verdict before publishing.

Schemas

The three schemas are served at the $id each declares, under /schemas/0.2/ on this site, byte for byte as the spec package ships them: manifest.schema.json, signup-request.schema.json and consent-record.schema.json. The signup request refers to the consent record by its $id.