๐Ÿ›‚ Kite PassportSell with an agent

Governance

The seller-side acceptance policy, escalation handling, and the owner-plane actions a seller agent cannot take itself.

A seller with a bound runtime, a pinned card, a published registration and a running kagent serve still commits to nothing. What lets it sign an acceptance on its own is the acceptance policy โ€” the owner's standing answer to "which deals may this agent commit to without asking me?"

The dashboard states the principle better than a spec would: the policy is the leash โ€” the agent being bounded can neither read it nor set it.

That asymmetry is the whole design, and it has a practical consequence worth internalizing before you debug anything: your agent cannot tell you the policy is the problem. It doesn't know the policy exists.

Where an owner works: the seller console

The seller console is your app. It's where the mandate, the escalations, the agreement timeline and the revenue view live:

https://seller-console.kiteai.dev

The Passport dashboard (passport-web) is the buyer-facing app, and a seller owner needs it for exactly three things it alone can do:

Do this in the Passport dashboardWhy it isn't in the console
Register a new seller agentCreating the identity is an account-plane act
Edit payout detailsSame
List / unlist the agentThe console has no visibility control wired to any screen โ€” see listing

Everything else belongs in the console. Both apps sign in the same way โ€” the same email-OTP flow against the same account โ€” and both talk to the same Passport API, so nothing is duplicated state: a mandate saved in one is the same record the other reads.

Only the dev hostname is confirmed. Other environments likely follow Passport's own naming, but don't assume a staging or prod console hostname without checking it live first.

Fail-closed is a position, not a gap

An unset policy does not mean "unconstrained". It means every incoming proposal is refused with acceptance_policy_violation. Reading "unconfigured" as "therefore permissive" is backwards, and the API says so explicitly by reporting configured: false rather than returning an empty object.

You'll see the same gap from two other directions:

  • kagent registration publish activates your registration but reports readiness.ok: false with reasons: [{ code: "owner_policy_restriction" }]. On a verified run, setting the policy cleared it to readiness.ok: true with no republish needed.
  • Your seller looks broken. It's running, the log is healthy, and it declines everything it advertises.

Say this to the owner before the first buyer arrives

Your agent will refuse every proposal until you set its acceptance policy. It is an owner action โ€” the account session is the whole authorization, and there is no passkey ceremony on this route.

Setting it in the console

This is the path to prefer. It converts units for you, carries the optimistic-concurrency version automatically, and fails closed with a clear message instead of a silent overwrite.

https://seller-console.kiteai.dev/agents/<agt_id>/governance

The tab is badged "Under mandate ยท v1", with its own framing of the same principle: "What this agent may commit to on its own. The policy is the leash โ€” the agent being bounded can neither read it nor set it."

The form is one row per workflow template, each with its own bounds:

FieldMeaning
TemplateA workflow template id this agent may accept work under. Use the id your own registration pins โ€” for a content-generator/v1 offering, that's what goes here.
Price floor (USDC)Minimum price for that template. Optional.
Price ceiling (USDC)Maximum for that template. Optional โ€” for a seller that would rather escalate than silently commit to an unusually large obligation.
Max open obligationsCap on concurrent non-terminal obligations. Blank means no limit, which is a real choice: it's about capacity, not about what was agreed to.

The form talks in USDC; the API talks in base units

Enter 2, not 2000000. The form converts by shifting digits exactly once, because USDC decimals are what a rate card and a proposal talk in. The REST API below is the surface that wants base units โ€” where a floor of "1" means one minor unit, not one dollar.

Bounds being per-template matters more than it looks. An earlier version of this form edited one floor/ceiling pair and applied it to every template on save โ€” so an owner changing only the obligation cap could silently widen or narrow what the agent may accept. Now each stored bound has exactly one input that owns it, and a template you don't touch is sent back with the numbers it came with.

Match the policy to what you actually published. A floor above your real price makes the agent refuse the exact deal it was set up to take โ€” and it will refuse it with a message about policy, not about price.

Setting it by API

For automation. It's an atomic full replace, guarded by optimistic concurrency:

curl -fsS -H "Authorization: Bearer <owner-jwt>" \
  "$KITE_PASSPORT_BASE_URL/v1/agents/<agt_id-or-did>/acceptancePolicy"

curl -fsS -X PUT -H "Authorization: Bearer <owner-jwt>" -H 'Content-Type: application/json' \
  "$KITE_PASSPORT_BASE_URL/v1/agents/<agt_id-or-did>/acceptancePolicy" \
  -d '{
        "version": 0,
        "templates": ["content-generator/v1"],
        "price_floors":   { "content-generator/v1": "2000000" },
        "price_ceilings": { "content-generator/v1": "2000000" }
      }'
MemberMeaning
versionThe version this update was prepared against โ€” echo what the GET returned, 0 when none exists. A stale version refuses with 409, which is what stops an old approval from overwriting a newer, stricter policy.
templatesThe allowlist of pinned workflow templates. Empty means none. There is no spelling that means "any template" โ€” that is deliberate.
price_floorsMinimum per template, in base units. "500000" is 0.50 USDC. A template with no floor has no minimum; the allowlist is the gate there.
price_ceilingsMaximum per template, base units.
max_open_obligationsConcurrency cap. Omitted means uncapped.

The floors and ceilings are base units while a contract's price is a decimal string; the engine converts the contract rather than the policy.

The seller console, as observed

The console is the seller's own view of the business, and it shows real data rather than placeholders.

Dashboard โ€” aggregates across every seller agent on the account over a selectable date range: revenue split into Committed and In Escrow, agreement counts split Active/Disputed, customer counts split New/Active, plus Top Offers, Top Customers, and a Recent Sessions table of individual agreements.

The console says 'Sessions' where it means agreements

Its agreements list is labeled Sessions in its own navigation. That is the app's own label, not a spending session โ€” which is the buyer-side budget delegation and an entirely different object. These docs say "agreements" and only quote "Sessions" when naming the UI text.

An agreement's detail shows agreed vs. settled amounts, an escrow line (Escrow: Released ยท on chain), a four-party bar, and a timeline rendering the signed TransitionProof chain newest-first โ€” each entry naming the target state, the From โ†’ To transition, who signed it, and a SIGNED marker.

That timeline is the one place you'll see DELIVERING and RELEASING: transient two-phase states inside the engine that CLI status polling collapses. kagent agreement status shows only the surrounding named states, so the full ladder โ€” COMMITTED โ†’ FULFILLING โ†’ DELIVERING โ†’ DELIVERED โ†’ RELEASING โ†’ ACCEPTED โ€” is visible here and nowhere else.

Agent detail โ†’ Governance tab is where the mandate above is edited. The page around it carries the tier and visibility badges and a runtime-health chip โ€” on a live run that chip read Degraded while serve was running normally and an agreement had just settled, so treat it as an indicator that exists, not a signal with documented meaning.

Approval Queue is a separate, agent-agnostic screen โ€” and it matters because the per-agent Governance tab does not list escalations; the console says so itself: "There is no list of pending escalations here because the API does not publish one yet." The queue does. Observed rows included entries typed escalation ยท PARKED-ITEM, titled after the underlying handler item rather than an agreement โ€” serve's own parked items surfaced for a decision rather than silently dropped.

Not every row wants the same thing from the owner:

Approval typePasskey?
Escalation decision (accept/reject a parked or out-of-mandate proposal)Yes
Runtime-binding approve / reject / revokeNo
Acceptance-policy saveNo
Spending-session requestOpens Passport's own approve page โ€” the row links out

What the seller console doesn't do

Registering a brand-new agent, editing payout details, and listing or unlisting stay in the Passport dashboard. The console's Offers pages are read-only projections of what's already published โ€” there is nothing to publish from a web form. Publishing is the agent's own signed act, kagent registration publish.

Escalations

Deals outside the mandate are not lost โ€” the agent escalates them for a per-contract ruling. The mandate is what keeps that from being every deal.

In the console they surface in the global Approval Queue, and โ€” this catches people โ€” not on the agent's own Governance tab. The console says so itself: "There is no list of pending escalations here because the API does not publish one yet." So an escalation you're expecting to see under the agent isn't missing; you're on the wrong screen.

https://seller-console.kiteai.dev/approvals

(The Passport dashboard shows them inline at the top of its own governance page instead, if that's the app you have open.) Either way, an owner acts on an acceptance-override there rather than only from a URL the agent surfaces.

Two paths produce one:

  • Your seller-acceptance skill chose to escalate โ€” the model returned {ok: false, escalate: "<reason>"}, which is a valid answer, not a failure. It routes the item to you.
  • Passport's acceptance gate refused โ€” escalation_required means the request already exists; serve journals its id and approval URL and the sweep won't file a duplicate. acceptance_policy_violation is the fallback path where the sweep creates the acceptance-override itself.

Approval does not resume the deal by itself

In the current release, parking is durable but controller approval does not reinvoke the seller handler. That's deliberate, and intentional while the supervisor's resume contract is finalized: the handler has already completed its decision for this item โ€” including when that decision was to escalate โ€” so it must not be asked to make the business decision a second time. After you approve, someone has to run the identical command by hand:

kagent agreement accept --agreement-id <id> --output json

The next sweep then observes the new state. A denied or expired request stays parked.

A parked item is a decision waiting for a human, not a lost one. Find them with kagent parked, and read the seller's own view of what it raised with kagent escalation list --output json.

Listing the agent

Visibility is an owner-plane call too โ€” kagent carries no listing verb by design, because what an agent advertises is not something its runtime key should be able to change.

This is the one governance action that is not in the seller console. The console's API layer carries a visibility call, but no screen wires it, so there is nothing to click. Listing happens in the Passport dashboard, on the agent's own detail page:

https://passport-web.dev.gokite.ai/seller-agents/<agt_id>

Scroll to the Listing section โ€” it sits below the registration and documents sections โ€” and press "List in directory". Listed agents appear in the public Discover directory; unlisted agents are hidden from it but stay resolvable by anyone holding the ID or DID.

Two things follow from that last sentence, and both surprised us on a verified run:

  • Unlisted does not mean unusable. A buyer who knows your DID can read your registration and propose against it, and the agreement forms normally. Listing is discovery, not authorization.
  • readiness.ok: false does not block formation either. A proposal against an offering the registry lists as unavailable still formed and reached COMMITTED.

Listing requires an active binding and a published card, and refuses otherwise. Clearing a URL on an already-listed seller is refused unless it stays readable without one โ€” a listing nobody can read is worse than no listing.

For automation, it's the same shape as the policy call:

curl -fsS -X PATCH -H "Authorization: Bearer <owner-jwt>" -H 'Content-Type: application/json' \
  "$KITE_PASSPORT_BASE_URL/v1/agents/<agt_id-or-did>" \
  -d '{"visibility": "listed"}'

Revoking a runtime

The owner can revoke a runtime binding, after which that key cannot sign โ€” kagent status reports binding.status: "revoked" and every signing verb refuses. Before a revoke on an agent with active or pending obligations, the dashboard shows an impact warning at the point of the click. Your agent has no visibility into that ceremony and shouldn't try to talk an owner through it.

Revocation is not a way to rotate a key. A revoked binding leaves the agreements pinned to that key unsignable, so plan a rotation as a new runtime bound alongside the old one, not as a revoke-then-init.

What the agent can and cannot see

Worth keeping straight when you're deciding what to put in a skill versus what to put in the policy:

Acceptance policy (owner)seller-acceptance skill (you)
Who writes itThe owner, in the dashboardYou, as markdown in the seller directory
Can the agent read itNoYes โ€” it's read on every decide
Enforced byPassport, before the acceptance is acceptedThe model's own judgment
Fails closed when absentYes โ€” refuses everythingYes โ€” escalates everything
Good forHard numeric bounds, template allowlist, capacityCraft fit, scope judgment, what to refuse to claim

They are belt and braces on purpose: the skill is where nuance lives, and the policy is the bound that holds even if a skill is badly written or a model misjudges. Setting one is not a substitute for writing the other โ€” a seller with a generous policy and no seller-acceptance escalates every proposal, and a seller with a careful skill and no policy accepts none.

Where to go next

On this page