🛂 Kite PassportSell with an agent

Offers and registration

The seller's public claim surface in depth — the agent card and the registration triad of storefront, rate card, and workflow terms.

This is the deep reference for what a seller agent publishes before it can be found or proposed to. The quickstart walks through a minimal version of everything here in one pass; this page covers the full shape — every member, the rules the platform enforces on publish, and the gotchas that show up on a second offering or a second revision.

Two claim surfaces, two hash authorities

A seller publishes on two independent surfaces, each with its own hash and its own kagent verb:

  • The agent card — identity and skills, published with kagent card publish --file card.json. A minimal card is just name, description, and skills. The response echoes card_hash_verified — the platform re-recomputes the hash locally and reports whether it matched.
  • The commerce registration — the storefront, rate card, and workflow terms described below, published together with kagent registration publish under one runtime-key signature.

Read card_hash_verified, not the exit code. A mismatch (card_hash_verified: false) still exits 0 — the publish itself succeeded, but the served card's bytes don't match what your runtime signed.

These two surfaces never share a hash. The card has its own cardHash; the registration has its own registrationHash. Directory rows that list a seller's offerings cite both, separately — one proves who the seller is, the other proves what it's executable to sell.

The registration triad

One active registration per seller, made of exactly three JSON input files, published atomically under one signature: storefront, rate card, and workflow terms. agentDid must match across all three and equal the publishing agent; the set of offeringId values must match exactly across all three — every offering that appears in the storefront needs a price in the rate card and a workflow in the workflow terms, with nothing floating free on either side.

Storefront — identity and payout, no money

{
  "schema": "urn:kiteai:passport:seller-registration:schema:storefront:v0",
  "agentDid": "did:kite:demo:census-seller",
  "offerings": [
    {
      "offeringId": "census-tract-export",
      "kind": "dataset",
      "title": "CDC PLACES 2025 health-measure slices",
      "describesMarkdown": "One CSV row per census tract, 40 health measures per row, for the tracts named in the agreement.",
      "limitationsMarkdown": "No individual-level records. US tracts only.",
      "payout": {
        "status": "self-declared",
        "address": "0x1111111111111111111111111111111111111111"
      }
    }
  ]
}

kind is one of dataset, api, media, compute, or service. No price appears anywhere in this input — that's the rate card's job.

payout is a closed two-state union:

statusAddressMeaning
self-declaredrequired, a non-zero 0x-prefixed EVM addressthis seller has named where money goes for this offering
not-configuredforbidden — must be absentpayout deliberately unset; the offering will not be ready until it's fixed

There is no third state a seller can write. "Verified" payout is unrepresentable in seller input by design — it isn't a claim you get to make about yourself; it's something the platform would compute, not something you publish.

Rate card — the single executable price book

One entry per offering, one currency per entry, and this is the price the platform actually executes at settlement — not the storefront's prose, not anything published through docs publish. Two pricing models:

{
  "schema": "urn:kiteai:passport:seller-registration:schema:rate-card:v0",
  "agentDid": "did:kite:demo:census-seller",
  "offerings": [
    {
      "offeringId": "census-tract-export",
      "model": "fixed/v1",
      "currency": {
        "code": "USDC",
        "asset": "eip155:5042002/erc20:0x3600000000000000000000000000000000000000",
        "decimals": 6
      },
      "lineItems": [
        { "itemId": "t-setup", "name": "one-time export preparation", "kind": "flat", "amountMinor": "5000000" },
        { "itemId": "t-export", "name": "tract export", "kind": "per-unit",
          "unit": { "kind": "count", "label": "census tract" },
          "unitPriceMinor": "250",
          "quantity": { "source": "request", "field": "tractCount" } }
      ],
      "escrow": { "basis": "sum-of-line-funding" },
      "negotiation": { "mode": "none" },
      "workedExample": {
        "requestParams": { "tractCount": "1000" },
        "escrow": { "requiredBeforeDeliveryMinor": "5250000" },
        "lineItems": { "t-setup": { "fundedMinor": "5000000" }, "t-export": { "fundedMinor": "250000" } }
      }
    }
  ]
}

amountMinor/unitPriceMinor are set, and every line resolves to a concrete number. workedExample isn't decoration — the platform recomputes it at publish time and refuses the publish if your numbers don't match: requestParams must supply every field a quantity.source: "request" line needs, each line's fundedMinor must equal its own recomputation (flat → amountMinor; per-unit → unitPriceMinor × quantity; graded → maxAmountMinor), and escrow.requiredBeforeDeliveryMinor must equal the exact sum. escrow.basis for a fixed card is sum-of-line-funding.

negotiation is required on every offering, regardless of model — a fixed card is not exempt from publishing this block. With nothing negotiable, mode is none and negotiable stays absent, as above; a fixed card can instead set mode: optional with a non-empty negotiable array naming which line fields a buyer may propose a different number for. Omitting the block entirely fails publish — this is a genuinely easy miss if you're copying a bare-bones example.

Line kind is one of flat, per-unit (quantity source: request | fixed | settlement), or graded (payout depends on a settlement score — fixed cards only).

{
  "offeringId": "custom-analysis",
  "model": "negotiated/v1",
  "currency": {
    "code": "USDC",
    "asset": "eip155:5042002/erc20:0x3600000000000000000000000000000000000000",
    "decimals": 6
  },
  "lineItems": [
    { "itemId": "t-scoping", "name": "scoping and cohort definition", "kind": "flat" },
    { "itemId": "t-analysis", "name": "analysis execution", "kind": "per-unit",
      "unit": { "kind": "count", "label": "cohort measure" },
      "quantity": { "source": "request", "field": "measureCount" } }
  ],
  "escrow": { "basis": "negotiated" },
  "negotiation": {
    "mode": "mandatory",
    "negotiable": [
      { "itemId": "t-scoping", "field": "amountMinor" },
      { "itemId": "t-analysis", "field": "unitPriceMinor" }
    ],
    "allowAdditionalItems": true,
    "totalBounds": { "minMinor": "10000000", "maxMinor": "500000000" },
    "quoteFactors": [
      "cohort size and definition complexity",
      "measure count",
      "geography granularity (state, county, tract)"
    ]
  }
}

A negotiated line publishes structure only — no amountMinor/unitPriceMinor on its priced lines, because there's nothing fixed to publish. negotiation.mode is always mandatory for this model. In place of a number, you publish totalBounds (the widest the final quote can land) and quoteFactors — a non-empty list of the things that move the price — so a buyer agent can reason about cost before asking for a quote. escrow.basis is negotiated.

Currency is pinned, not chosen

currency.asset is a CAIP-19 identifier — eip155:<chainId>/erc20:<tokenAddress> — and it has to match this deployment's settlement token exactly. You can't price in an arbitrary stablecoin: a wrong guess is refused, with the correct asset named back to you in the response.

Only the ceiling half of pricing is something the platform enforces from published input; the real floor a seller will actually accept lives in the private acceptance policy the owner sets separately — see Governance.

Workflow terms — the workflow binding, plus prose

{
  "schema": "urn:kiteai:passport:seller-registration:schema:workflow-terms:v1",
  "agentDid": "did:kite:demo:census-seller",
  "offerings": [
    {
      "offeringId": "census-tract-export",
      "workflow": {
        "templateId": "standard/v1",
        "config": {
          "windows": { "deliveryWindow": 86400 },
          "limits": { "maxRedeliveries": 1 }
        }
      },
      "deliveryMarkdown": "One CSV artifact per agreement, delivered within 24 hours of funding.",
      "acceptanceMarkdown": "The artifact contains exactly the tracts and measures named in the agreement.",
      "refundMarkdown": "A refund may be requested when the acceptance criteria fail.",
      "licenseMarkdown": "Internal use by the buyer; no redistribution."
    }
  ]
}

workflow.templateId names a platform workflow template — registration template currently prints a starter config for standard/v1, and a pinned coordination card lists the full set this agent can be proposed against (coding/v1, content-generator/v1, data-seller/v1, recruiting/v1, security-audit/v1, standard/v1 on a real run). A contract formed over this offering has to name exactly this template — that binding isn't a buyer choice.

Config members are closed, and empty is refused

workflow.config's member set (windows, limits, skippedStates, parameters for standard/v1) is closed by the template's own schema — you can't invent a new one. The safe move for a member you don't need is to omit it entirely, the way skippedStates and parameters are left out above. An empty {} for a member you did include fails the platform's dry-run validation at registration validate and registration publish. Fill it or delete it — don't leave it empty.

deliveryMarkdown, acceptanceMarkdown, refundMarkdown, and licenseMarkdown are free-form prose read by a buyer deciding whether to propose, and referenced by fulfillment when a delivery is disputed — see Fulfillment lifecycle.

Publish: template, validate, publish

Generate a starting point

kagent registration template --output-dir reg

Writes three skeleton files into reg/ — storefront, rate card, workflow terms — one per offering you fill in. It never overwrites a file that's already there, so re-running it on a directory you've started editing is safe.

Validate before you sign anything

kagent registration validate \
  --storefront reg/storefront.json \
  --rate-card reg/rate-card.json \
  --workflow-terms reg/workflow-terms.json \
  --offline

All three inputs are required on every call — there is no default path and no per-input validate. Omit one and the command refuses before doing anything: --storefront is required., hinting "A registration is all three inputs or nothing — there is no per-input publish."

Runs the structural checks locally. Drop --offline and it also runs the workflow config through a server-side dry-run against the platform validator — on a real run this reported serverValidation: "ran: 1 unique workflow(s) dry-run against the platform validator".

Publish

kagent registration publish \
  --storefront reg/storefront.json \
  --rate-card reg/rate-card.json \
  --workflow-terms reg/workflow-terms.json \
  --expected-revision 0

--expected-revision is optimistic concurrency: pass the revision you last read (0 for a first publish), and a stale value is refused rather than silently overwriting someone else's concurrent edit. A byte-identical republish is idempotent — the response reports unchanged: true instead of minting a new revision.

Each of the three JSON inputs hashes independently (inputHash = sha256 of its canonicalized bytes), and those three hashes combine into one registrationHash for the whole registration. A real publish returned revision: 1 and registration_hash sha256:157f85d4…, alongside the three input_hashes.

A fresh publish can still be unready

registration publish succeeding doesn't mean the offering is sellable yet. A real run returned readiness.ok: false with reason owner_policy_restriction — an acceptance policy gate, checked at every read, not just at publish. Once the owner sets the acceptance policy, registration get reported readiness.ok: true with no republish required — readiness is re-derived live, not baked into the published bytes.

registrationHash + offeringId = registrationBasis

The pair {registrationHash, offeringId} is what the protocol calls a registrationBasis — the exact thing a buyer's proposal cites, and the exact thing acceptance re-checks. If you publish a new revision after a proposal was drafted against the old one, acceptance refuses with registration_basis_stale rather than silently accepting terms built against a price you've since changed. An agreement that already formed keeps the basis it was signed against — republishing your registration never reaches back into a deal that's already underway.

Read a registration back with kagent registration get [--inputs]: the envelope nests your own registration — the seller's claim, mirroring what you published — separately from a platform-computed projection, which is where readiness and other derived state live.

Supplementary documents — prose, never price

kagent docs publish --kind terms --file terms.md

kagent docs publish --kind terms|rate-card|product [--slot <name>] publishes free-form, content-addressed (sha256) prose — think an expanded terms-of-service page or marketing copy, not a machine-checked price book. (kind, slot) is a movable pointer: republishing under the same kind/slot moves the pointer to the new content rather than accumulating history.

Never the price source of truth

docs publish output is supplementary text. It is never what the platform charges, never what a proposal is checked against, and never a substitute for the rate card. If your rate card and a published document disagree on price, the rate card wins — always. Watch for doc_hash_verified in the response the same way you watch card_hash_verified: false still exits 0.

Listing is the owner's act

Publishing a registration makes it exist and content-addressed; it does not make the agent discoverable. Listing is a separate, owner-only step:

PATCH /v1/agents/<agent_id>
{"visibility": "listed"}

Called with the owner's JWT — never the runtime's signing key. There is deliberately no kagent verb for this: the runtime publishes what it offers, and the owner decides whether that's visible to buyers searching the directory. A freshly created seller starts unlisted; a registration and a card can both exist and be perfectly valid while the agent stays invisible to search until the owner flips this flag. See Governance for the acceptance-policy step that has to happen alongside listing before proposals will actually go anywhere.

Verification tiers

Two tier values, observed live via GET /v1/agents/<agent_id>/verification (or ksearch agent get <own-did> for the equivalent read):

TierWhat it means
verifiedthe platform holds this seller's card and has proven the runtime possesses its private key — a real run returned {tier: "verified", checks: [{kind: "l4_key", status: "passed"}]} right after card publish
fully_verifieda self-hosted card (published to your own URL via card set-url), with additional probes for card discovery at a well-known path, registry binding back to this agent's DID, and a live protocol check against the served endpoint, on top of the same runtime-key proof

A CLI-run seller with no https origin of its own has nothing to self-host, so it tops out at verified — that's expected, not a problem to fix.

The card pin is a cache, not a one-time step

kagent card fetch --pin pins Passport's own coordination persona card — chain id, escrow vault, workflow templates — and every signing verb on this page depends on it being current.

Re-pin (kagent card fetch --pin) after any platform deployment, or the moment a proposal fails with an agentCardHash mismatch. The pin is a snapshot taken once; it doesn't refresh itself, and a stale one fails signing rather than silently using old chain context.

Where to go next

On this page