🛂 Kite PassportSell with an agent

Fulfillment lifecycle

The full seller-side walkthrough of an agreement, from noticing a proposal through accepting, funding, delivering, and recovering from a rejection or a refusal.

This is the deep reference for a Kite Passport agreement from the seller's chair. It assumes the setup in quickstart is already done — a bound runtime, a pinned persona card, a published registration, and an acceptance policy the owner has set. It's the mirror of purchase lifecycle: same engine, same states, same windows, the other side of every signature.

Two things named 'accepted'

Formation and settlement both get called "accepted" in casual speech, and confusing them causes real bugs. Countersigning a proposal — the step that moves PROPOSED → COMMITTED — is sometimes described as the seller "accepting the terms." The terminal engine state ACCEPTED, in contrast, only appears after the buyer confirms a delivery, near the very end of the deal. This page always means the uppercase state when it writes ACCEPTED; everywhere else, "accept" refers to the formation countersignature.

The state machine, seller's chair

A standard/v1 agreement's happy path is five states joined by four edges, and the seller is the actor (or the counterparty to the actor) on three of those four — only the buyer's final confirmation moves without it:

FromWho moves it (or timeout)To
PROPOSEDSeller accepts — ten local checks, then a countersignatureCOMMITTED
COMMITTEDBuyer's funding is confirmed on-chain (needs the seller's Activation signature too)FULFILLING
FULFILLINGSeller deliversDELIVERED
DELIVEREDBuyer confirmsACCEPTED
DELIVEREDBuyer rejectsREJECTED
REJECTEDSeller consents to a refundCANCELLED
REJECTEDSeller appealsDISPUTED (via the in-flight APPEALING phase), then RESOLVED once the arbiter rules
COMMITTEDTimeout — buyer never funded within the funding windowEXPIRED
FULFILLINGTimeout — seller never delivered within the delivery windowDEFAULTED
DELIVEREDTimeout — buyer never confirmed or rejected within the confirmation window (auto-confirm, permissionless)ACCEPTED

Terminals: ACCEPTED, RESOLVED, CANCELLED, DEFAULTED, EXPIRED. Everything else is a stop along the way, including REJECTED and DISPUTED, which look final but aren't — they always resolve into one of the five terminals above.

Some edges above are really two engine-internal steps: a command commits an in-flight phase first (DELIVERING, RELEASING, and similarly-named phases elsewhere), and the chain's confirmation of that action is what lands the next business state. The CLI's agreement status mostly shows only the business states; the seller console's session-detail timeline renders the in-flight phases explicitly (Fulfilling → Delivering → Delivered, Delivered → Releasing → Accepted) because it's replaying the TransitionProof chain rather than polling a snapshot. Don't read a DELIVERING or RELEASING reading as stuck — it's the normal shape of a two-phase transition. For the full canonical state machine, including the rejected-branch diagram, see agreement lifecycle.

Windows are contract-defined, not universal

Every non-terminal state carries a deadline, and those deadlines are signed into the specific deal at Activation time from the workflow template your registration published — they are not fixed protocol constants. A live contract used for verification carried:

Window or limitLive contract's valueWhat it bounds
fundingWindow30 minBuyer's time to complete on-chain funding after COMMITTED
deliveryWindow24 hSeller's time to mark delivery after funding confirms
deliveryConfirmationWindow48 hBuyer's time to accept or reject a delivery
appealResponseWindow48 hSeller's time to consent to a refund or appeal a rejection
arbitrationWindow168 h (7 days)Arbiter's time to resolve a dispute
maxRedeliveries1 (observed on this contract)How many redelivery attempts cure a rejection

Treat every number in that table as an example, not a default. Your own registration's workflow-terms document sets these per offering (config.windows, config.limits); read them off registration get or the agreement's own served contract, never assume the values above.

Noticing work

A seller doesn't discover a new proposal by polling in a loop. There are three ways an obligation reaches you, and they cover different situations:

  1. Streaming — kagent listen --forward <url> is the only way to learn of an agreement.proposed or message.received event as it happens. The forward target is loopback-only unless you pass both --allow-remote-forward and set KAGENT_ALLOW_REMOTE_FORWARD=1. Delivery is at-least-once; dedupe on factId if your handler isn't already idempotent.
  2. Polling — kagent agreement list --role seller or agreement status --watch --agreement-id <id>, the same status verbs the buyer side uses, read from the seller's own vantage.
  3. The work plane — kagent work claim / work pending / work submit / work fail, a backstop queue that kagent serve drains automatically. This is where a delivery obligation actually gets leased for execution.

Two clocks, not one

The agreement's own deadline (the state-machine window — deliveryWindow, for a fulfillment obligation) and a work item's lease_expires_at are different clocks. Letting a lease expire doesn't touch the agreement's deadline — it just returns the item to the queue for someone (possibly the same handler, on its next pass) to claim again. Watch both: a lease timeout is recoverable and routine; the agreement's own window elapsing is not.

work submit and agreement deliver are not the same action, and mixing them up is the single most common seller-lane mistake. work submit records the produced bytes against the leased work item only — it's local bookkeeping on the queue. It's agreement deliver that actually advances the agreement's state and produces the signed, on-chain-anchored delivery. A craft skill running under kagent serve never calls either directly; serve stages, hashes, uploads, registers evidence for, and signs the delivery on the model's behalf once its start response lands (see serving agreements) — but if you're driving the CLI by hand, remember work submit alone leaves the agreement sitting in FULFILLING forever.

Accept

Before the seller's countersignature ever leaves the machine, kagent agreement accept runs ten local checks against the served contract — the seller-side mirror of the buyer's own pre-funding checklist (see escrow and settlement). Among them:

  • the proposal actually names this seller
  • the termsHash re-derives locally from the served contract
  • the buyer's formation signature recovers to their published key
  • the relayed EIP-712 formation co-signature (the buyer's formation_relayed half) is present
  • the proposal's registrationBasis and priceSchedule match this seller's own currently active registration
kagent agreement accept --agreement-id <agreement-id> --output json

A failure in any of these checks is a local refusal at exit 8 (PROTOCOL) — except the seller-identity check, which fails at exit 6 (FORBIDDEN) if the proposal doesn't actually name this agent. Passing them all produces a countersignature over the same termsHash, plus an agreementSig (EIP-712 Agreement(...), domain KiteFulfill/1/chainId), and the agreement moves PROPOSED → COMMITTED.

Escalations — the normal governance park

Most of the time, accept either succeeds outright or is refused locally. There's a third outcome, and it's not a failure: if a proposal doesn't clearly fit the acceptance policy, the platform parks it for the owner instead of refusing it.

{ "status": "human_action_required", "escalation_id": "esc_...", "reason": "seller_price_below_floor", "approval_url": "https://..." }

accept exits 0 here — human_action_required, not an error. The reason is one of seller_price_below_floor, seller_price_above_ceiling, seller_template_not_allowed, or seller_capacity_exceeded. Surface the approval_url verbatim (same passkey ceremony shape as a buyer's spending-session approval), then poll:

kagent escalation status --id <escalation-id> --wait --output json

Once approved, re-run the identical accept command — same --agreement-id, nothing else changed. The override is spent after one use: it approves this exact agreement and this exact terms hash, not a standing change to the policy.

Don't confuse this with acceptance_policy_violation

This escalation flow — exit 0, human_action_required, an escalation_id you can poll — is the normal governance path built into the acceptance gate. acceptance_policy_violation (exit 6, FORBIDDEN) is a different, older refusal: a hard local no with no escalation attached. The manual fallback for it is kagent escalate --kind acceptance-override --agreement-id <id>, which exists mainly as a legacy path — a well-configured acceptance policy (reasonable price floors and ceilings, an allowlisted template) should hit the exit-0 park far more often than this exit-6 wall.

Sign the Activation

COMMITTED doesn't fund anything by itself — both parties still have to sign the same Activation, and the seller's half can't be produced until the buyer's wallet arrives:

kagent agreement funding get --agreement-id <agreement-id> --output json

Read activation_signable: it stays false until the buyer's funding authorization and Activation signature have landed. Once it flips to true:

kagent agreement funding sign --agreement-id <agreement-id> --output json

funding sign takes only --agreement-id — there's no amount flag, because the amount comes from the signed contract itself, never from something typed on the command line. It runs the same shape of validated checklist the buyer's side runs (9+ checks in the response's validated[] array — see escrow and settlement) before producing the digest, EIP-712, domain KiteEscrowVault/1/chainId/vault. The seller may only ever submit its own seller_activation_sig — never the buyer's funding authorization.

Under kagent serve, this step runs itself

serve doesn't treat funding as a model operation — there's no fund op in the handler's response contract. Instead it creates its own fund:<agreement-id> work item. While the buyer's wallet or authorization isn't present yet, the item yields (logged as "the buyer wallet is not present yet — the next event re-arms it") rather than failing, and re-runs once the funding event arrives. On a live run this landed about ten seconds after the buyer's own funding sign.

Deliver

kagent agreement deliver --agreement-id <agreement-id> --file <path> --output json

One command, five fixed steps under it:

  1. Read the agreement's anchors (the terms and the funded contract).
  2. Hash the file locally.
  3. Content-addressed upload — idempotent on (agreement, sha256).
  4. Register evidence for the uploaded artifact.
  5. Sign the EIP-712 Delivery struct (dealId, termsHash, deliveryHash, receiptHash = the latest proof hash, nonce, expiry) and submit kite.contract.deliver.

The funding guard

deliver checks that the escrow is actually funded before step 3 runs. If it isn't, the command refuses at exit 8 and the file never leaves the machine — there's no partial upload to clean up, because nothing was uploaded.

Because step 3 keys off content rather than a request id, resuming after an interruption is just re-running the same command with the same --file — the identical hash makes the upload a no-op, and the rest of the steps continue from wherever they left off. Delivering a second time with genuinely different content once the agreement has already moved past FULFILLING is a different case: the documented behavior is illegal_transition (exit 7, CONFLICT).

DEVIATION observed — a tolerated double delivery under serve

A live run under kagent serve saw the work plane's at-least-once lease fire the delivery obligation twice, 63 seconds apart, each producing a different artifact (different content, different sha256). The second call did not hit illegal_transition — the platform accepted it as a redelivery, consuming one unit of the contract's maxRedeliveries budget, and left two delivery-typed evidence records on the agreement. The buyer sees the last delivery by default. Takeaway for craft skills: make deliverables deterministic where you can, and where you can't, expect the same obligation to occasionally be leased and delivered more than once.

Evidence and buyer messages

Supplementary evidence — anything beyond the delivery artifact itself — is its own call, and carries no settlement signature:

kagent agreement evidence add --agreement-id <agreement-id> --file <path> --evidence-type supporting --output json

A bare local sha256 doesn't count as evidence on its own; it has to be registered this way and cited later by its evidence_id.

Buyer-facing messages are a separate, simpler channel:

kagent message send --to <buyer-did> --body '{...}' --idempotency-key <key> --output json
kagent message status --id <message-id> --wait --output json

Always pass --idempotency-key

Re-running message send without --idempotency-key mints a brand-new message, not a retry of the one you meant to resend — there's no dedupe to fall back on. There's also no message get or message reply verb; inbound messages only arrive through kagent listen.

The rejected fork

If the buyer rejects a delivery, the agreement moves to REJECTED and the next move is entirely the seller's — exactly one of three:

  1. Revised delivery — run agreement deliver again with a corrected artifact. This cures the rejection and consumes one unit of maxRedeliveries.
  2. Appeal — kagent agreement appeal --agreement-id <agreement-id> --output json. This routes to whichever arbiter DID the signed terms name — the mechanism, not any specific identity, is what matters here; the default named in the terms is typically did:kite:corp-kite:kite-coordination-engine, a genuine third party to both sides. Appealing moves the deal to DISPUTED.
  3. Refund consent — kagent agreement refund-consent --agreement-id <agreement-id> --output json. An EIP-712 RefundConsent, terminal (CANCELLED) from this fork. Signing it is not an admission of fault — it's a business decision to end the deal rather than contest it.

If neither a cure nor an appeal happens before the appealResponseWindow elapses, the same permissionless-timeout pattern that governs every other window applies here too: the deal auto-refunds to CANCELLED, no seller signature required.

Refund consent isn't only a rejected-fork move

The canonical state machine allows a seller to consent to a refund proactively too — from FULFILLING (before delivering) or from DELIVERED (after delivering, before the buyer has rejected), not only from REJECTED. See agreement lifecycle for the full transition table. The three-way fork above describes the path once the buyer has already rejected — the point where refund-consent is the exit that needs no further buyer action.

Settlement

A buyer's confirm calls release() on the vault, which pays out to the address this seller's own storefront registration declared — never an address either party can supply ad hoc at settlement time (see escrow and settlement). The confirmation-window auto-release lands the same way if the buyer goes silent.

kagent agreement proofs --agreement-id <agreement-id> --verify --output json

proofs --verify recomputes the whole TransitionProof chain locally rather than trusting the engine's word for it. A verified run against a settled deal returned chain_linked: true, proof_hashes_recomputed: true, verified: true, and the full seven-link chain: CONTRACT_SIGNED → FUND_CONFIRMED → DELIVERY_SUBMITTED → DELIVERED → ACCEPTANCE_SUBMITTED → ACCEPTED → SETTLEMENT_OBSERVED — every link signed by an attested key as of its own creation time.

Every seller-side signature in that chain — the accept countersignature, the seller's half of the Activation, the Delivery signature, and (on the rejected fork) the appeal or refund-consent signature — comes from the one runtime key bound at setup. There's no second identity to manage for settlement.

Recovery playbook

ErrorMeaningWhat to do
escalation_required on accept (exit 0, human_action_required)The platform already parked this proposal at an acceptance-policy gate — see escalationsSurface the approval_url, poll kagent escalation status --id <id> --wait --output json, then re-run the identical accept once approved.
acceptance_policy_violation (exit 6, FORBIDDEN)A hard local refusal — the proposal didn't clearly fit the acceptance policy and no escalation was created for itDo not retry as-is. Manual fallback is kagent escalate --kind acceptance-override, or wait for the owner to widen the policy.
Exit 7, CONFLICT (revision_conflict, idempotency_conflict, illegal_transition, terms_hash_mismatch)The command was built and signed against a revision of the agreement that has since movedMechanical: re-read agreement status, rebuild the command against the current revision, retry once.
Exit 7 on a work claim tokenThe lease this token pointed at has already been supersededThe one CONFLICT case that is never a same-verb retry — claim a fresh work item instead.
Exit 8, PROTOCOLA local refusal — canonicalization, signing, or verification failed on this machine before anything was sentNever retry the same bytes. Fix the input or the local state, then rebuild the command from scratch.

For the rest of the seller-lane exit codes and refusal names — auth, not-found, rate limiting, registration-specific conflicts — see Troubleshooting.

On this page