From 00da948a0c55f4f4505225cffbed9ed3c11c8b5c Mon Sep 17 00:00:00 2001 From: Marco De Rossi Date: Thu, 27 Aug 2026 15:26:39 +0200 Subject: [PATCH 01/24] Draft AIP-5: Model Routing Peers Adds a routing peer role: discovery via a routing.v1 peer capability (mirroring AIP-3's verifier.* pattern, no new capability enum or metadata codec bump), reserved /_antseed/route and /_antseed/route/digest paths modeled on AIP-3's attestation dispatch/rate-limit rules, wire schemas for the route request/response and daily digest, and five new optional methods on the buyer-side Router interface. No contract or payment-message changes; routing-peer pricing is left to a companion AIP. Co-authored-by: Cursor --- AIPS/aip-5.md | 579 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 579 insertions(+) create mode 100644 AIPS/aip-5.md diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md new file mode 100644 index 0000000..8220705 --- /dev/null +++ b/AIPS/aip-5.md @@ -0,0 +1,579 @@ +--- +aip: 5 +title: Model Routing Peers +description: Defines the routing peer role, its discovery and reserved request paths, and the buyer-side router interface for cross-model routing. +author: Marco De Rossi (@marcoderossi90) +discussions-to: https://github.com/AntSeed/antseed/discussions +status: Draft +type: Standards Track +category: Core +created: 2026-08-27 +--- + +## Abstract + +This AIP adds a routing peer role to AntSeed: a peer that, given a buyer's +conversation and the network's live prices, returns a ranked list of +`(model, peer)` candidates instead of serving inference itself. Buyers who opt +in send a chat request whose model field is a sentinel rather than a concrete +model id; a routing-aware `Router` plugin recognizes the sentinel, asks a +routing peer to rank candidates, and dispatches through the network's existing +failover walk exactly as it would for a fixed model choice. + +The proposal specifies three things: how a routing peer advertises itself and +is discovered, without adding a new capability enum or a metadata codec +version bump; the two reserved request paths a routing peer serves and their +dispatch and rate-limit rules, modeled directly on [AIP-3](./aip-3.md)'s +attestation route; and five new optional methods on the existing buyer-side +`Router` interface that let a plugin pick model and seller together, ahead of +AntSeed's existing fixed-model peer-selection pipeline, while that pipeline +stays completely unmodified for any buyer who does not opt in. + +This AIP makes no contract change and no payment-message change. How a +routing peer charges for its service is left to a companion AIP; this one +treats routing peers as offering their service for free, exactly like +attestation. + +## Motivation + +AntSeed lets a buyer pick a model and then finds the best-priced, most +trustworthy seller of that exact model. It has no mechanism for a service +that recommends *which model* to use in the first place, weighing quality +against cost across the whole catalogue and across sellers. That +recommendation only has value if it can see the network's real, live prices — +which a buyer's own client already discovers — and if it can factor in +per-conversation state, such as which seller already holds a warm cache of +this conversation's prefix, that the recommending party cannot itself observe +without the buyer's help. A routing peer is a natural role for this: an +ordinary AntSeed seller node, paid like a seller, but one that returns a +ranked list rather than an inference result. + +Two aspects of "just add a new capability" turn out not to work with what +exists on the network today, which shapes this proposal's discovery design: + +- `ProviderCapability` and `PeerOffering`, the enum-and-topic mechanism that + looks like the obvious place for a new "routing" value, is unwired end to + end: no announcer call site ever populates `offerings`, and the + corresponding DHT lookup has no caller anywhere in the buyer path. Adding a + value there would not make a routing peer discoverable by anything that + exists today. +- A peer that runs nothing but a routing handler and no inference `Provider` + does not announce to the DHT at all under the current node implementation, + because the announcer is only constructed when at least one provider is + registered. A routing-only peer would be unreachable by construction unless + this is fixed. + +Separately, the reserved-path pattern this proposal needs — a seller-side +handler claiming its own URL prefix, served before provider matching and +before payment — already exists for attestation ([AIP-3](./aip-3.md)). That +AIP's capability-string advertisement, dispatch-and-rate-limit rules, and +plugin-registration shape transfer to routing almost unchanged; this proposal +follows that precedent deliberately rather than inventing a parallel one. + +## Specification + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", +"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this +document are to be interpreted as described in RFC 2119. + +### Routing Peer Role + +A routing peer is an ordinary AntSeed seller node — the same `PaymentMux`, +signed metadata, and connection handling any seller runs — that registers a +routing handler and MAY register no inference `Provider` at all. A routing +peer MUST NOT be required to run a `Provider` to be reachable; see +Discovery below. + +### Discovery + +A routing peer advertises support with the peer capability string +`routing.v1` in `PeerMetadata.capabilities`, following the pattern +[AIP-3](./aip-3.md) established for `verifier.`: capability strings are +appended to the existing v10+ capability list, so this requires no new +`ProviderCapability` enum value, no DHT capability topic, and no +`METADATA_VERSION` bump. `routing.v1` MUST conform to the existing capability +grammar (`^[a-z0-9][a-z0-9.-]*$`, max 64 characters) and count against the +existing per-peer capability limit (16). A future incompatible routing +protocol revision MAY advertise `routing.v2` alongside or instead of +`routing.v1`; a buyer that only understands `v1` MUST ignore capability +strings it does not recognize. + +A peer's announcer MUST start, and MUST include `routing.v1` in its +advertised capabilities, whenever a routing handler is registered, regardless +of how many inference providers are also registered — including zero. A node +implementation that only announces when at least one provider is registered +MUST be corrected as part of implementing this AIP; a routing-only peer that +never reaches the DHT is not a conforming implementation. + +A buyer discovers routing peers through its existing peer-discovery sweep +(unchanged by this AIP) and filters the resulting peer set by the `routing.v1` +capability string, the same way an [AIP-3](./aip-3.md) buyer filters for +`verifier.*` capabilities. No new DHT topic, lookup call, or discovery +round-trip is introduced. + +### Reserved Request Paths + +The path prefix `/_antseed/route` (`ANTSEED_ROUTE_PATH`) and the path +`/_antseed/route/digest` (`ANTSEED_ROUTE_DIGEST_PATH`) are reserved. A seller +MUST route a `POST` request for either path to its registered routing +handler, and MUST do so before provider matching. A routing handler MUST be +implemented as: + +```typescript +interface RoutingServerHandler { + handleRoute(buyerPeerId: string, req: SellerRequest): SellerResponse | Promise +} +``` + +`SellerRequest` is `{ method, path, headers?, body? }` and `SellerResponse` is +`{ statusCode, headers, body }`, the same shapes [AIP-3](./aip-3.md) defines +for `Prover`. A node MUST expose a single-slot registration for a routing +handler (one routing identity per seller, unlike the name-keyed list of +provers a seller MAY run), mirroring the buyer-side single `Router` slot this +AIP also extends. + +Dispatch rules: + +- Exactly one routing handler MAY be registered per node. A `POST` to either + reserved path when no handler is registered MUST return `404` with error + code `not_routing_peer`. +- Otherwise the seller MUST call `handleRoute(buyerPeerId, req)` and return + its `SellerResponse` unmodified. A handler that throws or rejects MUST come + back as a `500` response of type `routing_error` and MUST NOT crash request + handling. +- Both paths dispatch to the same handler. A handler distinguishes a route + request from a digest submission by `req.path`, since the request bodies + defined below are structurally distinct (a route request always carries + `sagePrompt`; a digest submission never does). An implementation MAY also + reject a request on the wrong path with `400` rather than relying on body + shape. +- A routing peer that gates on payment (left to a companion AIP; see Payments + below) MUST reject before calling into any ranking logic, with `402`, so an + unpaid caller never triggers the expensive part of the handler. + +### Routing Request Rate Limit + +Ranking a request is expensive — it MAY call out to a separate scoring +process — so, exactly as [AIP-3](./aip-3.md) requires for the attestation +path, a seller MUST rate-limit `/_antseed/route` per buyer peer, with the +tracking table capped so the limiter itself cannot become a memory-exhaustion +vector. Cheap `400`/`402`/`404` rejections MUST NOT count against a buyer's +quota; only a call that reaches `handleRoute` does. `/_antseed/route/digest`, +being a small, fixed-shape, once-daily submission, is not subject to this +rate limit but MUST still be size-bounded by the seller's ordinary request-size +limits. + +### Wire Schemas + +The `POST /_antseed/route` request body: + +```typescript +interface RouteRequestBody { + v: 1; + cqt: number; + sagePrompt: string; + contextTokens: number; + expectedCachedTokens: Array<{ model: string; peer: string; tokens: number }>; + constraints: { + maxInputUsdPerMillion?: number; + minTrustScore?: number; + allowedPeerIds?: string[]; + blockedPeerIds?: string[]; + }; +} +``` + +`cqt` is a cost/quality tradeoff value on a 0–10 scale, buyer-chosen. +`sagePrompt` MUST be the routing peer's own input format for whatever ranking +scheme it runs; this AIP does not standardize prompt content, only that a +string field carries it. `expectedCachedTokens` lets the buyer report, per +candidate it has itself observed billing for, how many tokens of this +conversation's prefix that seller already holds warm — a routing peer MUST +NOT be expected to track this itself, since a routing peer that retains no +per-conversation history cannot observe it. `constraints` carries the buyer's +own policy: a routing peer SHOULD use these fields to pre-filter its ranking, +but a buyer MUST NOT rely on a routing peer's filtering as authoritative — see +Rationale. + +The `POST /_antseed/route` response body, on success: + +```typescript +interface RouteResponseBody { + v: 1; + ranked: Array<{ + model: string; + peer: string; + score: number; + predictedQuality: number; + predictedCostUsd: number; + predictedInputTokens: number; + predictedCachedInputTokens: number; + predictedOutputTokens: number; + price: { inUsdPerM: number; outUsdPerM: number; cachedInUsdPerM: number }; + }>; + baselineSuggestion: { model: string; peer: string; price: { inUsdPerM: number; outUsdPerM: number } } | null; + receipt: { routerId: string; artifactVersion: string; lambdaVersion: string }; +} +``` + +`ranked` MUST be ordered by the routing peer's own objective, best candidate +first. A buyer MUST treat this order as the routing decision itself and MUST +NOT locally re-sort it by an independent signal such as reputation before +walking it, since doing so would discard the quality/cost tradeoff the +ranking encodes; a buyer's own policy filtering (allow/block lists, price and +trust ceilings) applies during the walk, skipping disallowed entries in +place, not by re-ordering the remainder. `price` MUST be populated for every +ranked entry, not only the winner, so a buyer can compute a savings figure +against any candidate without a separate price lookup and without the +routing peer needing to remember which candidates it offered to which buyer. + +The `POST /_antseed/route/digest` request body: + +```typescript +interface DailyDigestBody { + v: 1; + period: string; + routedRequests: number; + predictedCostUsd: number; + observedCostUsd: number; + predictedInputTokens: number; + predictedCachedInputTokens: number; + predictedOutputTokens: number; + observedInputTokens: number; + observedCachedInputTokens: number; + observedOutputTokens: number; + modelMix: Record; + regenerations: number; + overrides: number; + failovers: number; + timeouts: number; + avgRoutingLatencyMs: number | null; + cqtDistribution: Record; +} +``` + +`period` MUST be a calendar day in `YYYY-MM-DD` form, in the buyer's own +local time zone. This is a fixed, closed schema: it MUST carry only +period-level aggregate counters and MUST NOT carry prompt content, +per-request rows, or any field that ties a routing decision to the message +that produced it. A digest submission has no response body requirement +beyond an HTTP status; a routing peer SHOULD return `204`. + +### Interface: `Router` Extensions + +The existing buyer-side `Router` interface gains five new, all-optional +methods. A `Router` that implements none of them is fully conforming and +unaffected by this AIP; existing behavior for `selectPeer` and `onResult` is +unchanged. + +```typescript +interface Router { + // Existing, unchanged: + selectPeer(req: SerializedHttpRequest, peers: PeerInfo[]): PeerInfo | null; + onResult(peer: PeerInfo, result: { + success: boolean; + latencyMs: number; + tokens: number; + // New, optional, additive fields on the existing result object: + freshInputTokens?: number; + cachedInputTokens?: number; + outputTokens?: number; + estimatedCostUsd?: number | null; + requestId?: string; + }): void; + + // New, all optional: + selectRoute?( + req: SerializedHttpRequest, + peers: PeerInfo[], + conversation: ConversationIdentity | null, + routingPreferences: ModelRoutingPreferences | null, + defaultRoutedModel?: string | null, + ): Promise; + + getRoutingDecisions?(): RoutingDecisionRow[]; + configureDailySigning?(signDailyIfNeeded: (sellerPeerId: string) => Promise): void; + triggerDailySigningCheck?(): Promise; + updateRoutingPreferences?(preferences: ModelRoutingPreferences): void; +} + +type RouteCandidate = { + peer: PeerInfo; + peerId: string; + serviceId: string; + request: SerializedHttpRequest; + reputation: number; + hasCachedInputPricing: boolean; + inputUsdPerMillion: number | null; + outputUsdPerMillion: number | null; + minImageUsdPerImage: number | null; +}; + +type RoutingDecisionRow = { + atMs: number; + actualModel: string; + actualPeer: string; + actualPromptTokens: number; + actualCachedTokens: number; + actualCompletionTokens: number; + actualUsdcPaid: number; + predictedCostUsd: number | null; + predictedInputTokens: number | null; + predictedCachedInputTokens: number | null; + predictedOutputTokens: number | null; + cqt: number; + routingLatencyMs: number | null; + baselinePrices: Record; +}; + +type ConversationIdentity = { + tool: string; + sessionKey: string; + parentSessionKey: string | null; + isUserThread: boolean; +}; + +type ModelRoutingPreferences = { + preferFreePeers: boolean; + maxInputUsdPerMillion: number; + minTrustScore: number; + allowedPeerIds: string[]; + blockedPeerIds: string[]; + cqt?: number; + autoSubscriptionEnabled?: boolean; +}; +``` + +A host (buyer proxy) implementation MUST call `selectRoute` before its +existing fixed-model peer-narrowing step, whenever the registered `Router` +implements it, and MUST treat a `null` result — including a `Router` that +does not implement `selectRoute` at all — as a decline, falling through to +the unmodified existing pipeline with no other change in behavior. A host +MUST pass the request exactly as received, before any host-level model-alias +substitution the buyer applies for its own purposes; a plugin recognizes its +own sentinel model value from `req.body` itself. A host MUST NOT hardcode, +special-case, or validate against any specific sentinel string — that +recognition is entirely the plugin's business, so that different `Router` +implementations MAY choose different sentinels without any host change. + +When `selectRoute` returns a non-null list, the host MUST walk it in the +returned order using its existing per-candidate failover mechanism (fail over +before the first token of a response, terminal after), MUST NOT re-sort it +locally, and MUST call `onResult` for the peer actually used exactly as it +does for a fixed-model selection. A host MUST forward `routingPreferences` to +`selectRoute` on every call and MUST push preference updates to a `Router` +that implements `updateRoutingPreferences` whenever those preferences change, +including once at startup. + +A `Router` MUST NOT be given direct access to the buyer's payment-signing +key. A `Router` that needs to trigger its own periodic signing (for example, +a subscription-priced routing peer under a companion pricing AIP) MUST do so +only through a host-provided signing closure passed via +`configureDailySigning`, never by holding a reference to the buyer's signer +or payment manager directly. + +### Payments + +This AIP specifies no contract change and no payment-message change. It does +not add, remove, or modify any `MessageType` value, any EIP-712 struct, or +any Solidity contract. A routing peer served under this AIP is unpriced, +exactly like attestation under [AIP-3](./aip-3.md); the `402` gate described +under Reserved Request Paths is a hook this AIP reserves for a companion AIP +to fill in, not something this AIP itself defines the mechanism for. How a +routing peer charges — flat-rate subscription pricing, per-request metering, +or something else — is left entirely to that companion proposal. + +## Rationale + +**Capability string, not a new capability enum value.** The obvious design — +add `'routing'` to `ProviderCapability` — does not work with what the network +actually runs: the DHT capability-topic machinery that enum feeds is unwired +end to end. `routing.v1` as a peer capability string, by contrast, rides +exactly the same encode/decode/validate path `verifier.*` +([AIP-3](./aip-3.md)) already exercises in production, so it costs no new +metadata version and inherits a mechanism already proven to work. + +**Fixing announcer gating rather than working around it.** A routing peer +that never runs an inference `Provider` is not a corner case — it is the +expected shape of a peer whose entire purpose is ranking, not serving. Making +peer announcement conditional on `Provider` registration would make routing +peers structurally undiscoverable; the fix belongs in the node's +announcement logic, not in a workaround at the routing layer. + +**Modeled on attestation deliberately.** [AIP-3](./aip-3.md) already solved +the exact shape of problem this AIP has: a seller-side handler that claims a +reserved path, serves it before provider matching and payment, needs a +per-buyer rate limit because the work is expensive, and is advertised as a +capability string a buyer filters for during ordinary discovery. Rather than +re-deriving that design, this AIP reuses it, changing only what is +substantively different (ranking model-and-seller pairs, not proving a +claim) and leaving the plumbing shape the same. + +**Preserving the routing peer's returned order.** A ranked list encodes a +quality/cost tradeoff the buyer's own client cannot independently recompute +from a peer's reputation and price alone — reputation is one input among +several the routing peer already folds in. Re-sorting the list locally by +reputation, the way AntSeed's existing fixed-model pipeline does when +choosing among sellers of one already-decided model, would discard that +tradeoff. The buyer's own policy filtering still applies, but as a filter +over the given order, not a re-ranking of it. + +**The routing peer's constraints filtering is advisory, not authoritative.** +A buyer sends its own price/trust ceilings and allow/block lists so a +routing peer can pre-filter to what that buyer can actually use, but a buyer +MUST still re-apply its own policy locally before dispatching, because +constraints are a snapshot at request time: a peer can go unreachable, enter +a cooldown, or drop out of an allow list update between the routing call and +dispatch. Treating the routing peer's filtering as a courtesy rather than a +guarantee is what lets a stale constraint degrade a choice instead of +breaking it. + +**Sentinel model strings are not part of this protocol.** A host that +special-cased one router's sentinel string would privilege that +implementation over any other `Router` a buyer might load. Keeping sentinel +recognition entirely inside the plugin, with the host forwarding the +unmodified request body, is what keeps the router-plugin slot genuinely +pluggable rather than nominally so. + +**No payment mechanism in this AIP.** Coupling a new peer role to a specific +pricing scheme in the same proposal would force reviewers to accept or reject +both together. A routing peer is useful, and reviewable, as an unpriced +service first — the same bootstrapping path attestation took. A companion +AIP can specify metered, subscription, or other pricing for a routing peer +without reopening anything specified here. + +**Digest as a separate submission, not bundled into a payment message.** A +routing peer's own reserved-path infrastructure already exists once this AIP +ships; reusing it for a small, periodic aggregate submission needs no new +protocol surface. Extending a closed, protocol-wide payment-message enum for +what is explicitly optional, non-billing statistics would be a heavier change +for a smaller benefit. + +## Backwards Compatibility + +This proposal is additive. A peer that advertises no `routing.v1` capability +is unaffected by this AIP; a `Router` implementation that does not implement +any of the five new optional methods is unaffected and behaves exactly as it +does today. The `/_antseed/route` and `/_antseed/route/digest` paths are +newly reserved, so a seller MUST NOT route either to a provider — though +neither was ever a valid model or service id, so no existing conforming +seller can already be using them for anything else. No metadata codec +version bump, no `METADATA_VERSION` change, and no payment-message change is +required. Mixed-version networks interoperate: a routing-aware buyer skips +peers that advertise no `routing.v1` capability, and a routing-advertising +peer is ignored by buyers that do not implement `selectRoute` at all, exactly +as [AIP-3](./aip-3.md) describes for verifier capabilities. + +## Test Cases + +A conforming implementation MUST include tests for: + +- routing-handler dispatch on `POST /_antseed/route` and + `POST /_antseed/route/digest` before provider matching; +- `404` `not_routing_peer` when no handler is registered, and `500` + `routing_error` on a handler that throws; +- per-buyer rate limiting on `/_antseed/route`, with cheap `400`/`402`/`404` + rejections not consuming quota; +- capability round-trip: building and parsing the `routing.v1` capability + string, and confirming it fits the existing capability grammar and count + limits; +- a node with a registered routing handler and zero registered providers + still starting its announcer and including `routing.v1`; +- `selectRoute` returning `null` falling through to the unmodified existing + peer-selection pipeline, with no behavior change for a `Router` that does + not implement it; +- candidate order from a non-null `selectRoute` result being preserved + through the failover walk, with no local reputation re-sort applied; +- `onResult` receiving the new optional telemetry fields without breaking a + `Router` that ignores them; +- round-trip encode/decode of `RouteRequestBody`, `RouteResponseBody`, and + `DailyDigestBody` at `v: 1`. + +## Reference Implementation + +Reference implementation branch: +https://github.com/levantolabs/antseed-levanto-router/tree/model-routing + +Key surfaces: + +- `RoutingServerHandler`, `ANTSEED_ROUTE_PATH`, `ANTSEED_ROUTE_DIGEST_PATH` in + `packages/node/src/interfaces/plugin.ts`; +- reserved-path dispatch in `packages/node/src/seller-request-handler.ts`; +- the extended `Router` interface, `RouteCandidate`, and `RoutingDecisionRow` + in `packages/node/src/interfaces/buyer-router.ts`; +- `ConversationIdentity` in `packages/node/src/routing/conversation-identity.ts`; +- `ModelRoutingPreferences` in `packages/node/src/routing/model-route-ranking.ts`; +- the `selectRoute` call site, candidate walk, and extended `onResult` + telemetry in `apps/cli/src/proxy/buyer-proxy.ts`; +- the vendor `Router` implementation and wire-schema types in + `plugins/router-levanto/src/router.ts` and `digest.ts`. + +The reference branch's `routing.v1` capability advertisement and the +announcer fix for provider-less peers are not yet implemented as of this +AIP's Draft status; the branch currently advertises a routing peer's price +only, via a mechanism a companion pricing AIP covers, and its node +implementation still gates announcement on at least one registered provider. +Both are tracked as follow-up work against the reference implementation. + +## Security Considerations + +**Unmetered ranking is a denial-of-service surface.** Ranking a request MAY +be expensive, calling out to a separate scoring process. Exactly as with +attestation, a seller MUST rate-limit `/_antseed/route` per buyer peer with +bounded tracking state, so an unpaid or spam caller cannot exhaust a routing +peer's resources for free. This AIP defines no payment gate itself, so until +a companion pricing AIP is adopted, the rate limit is the only cost control a +routing peer has, and implementations MUST NOT skip it on the theory that +payment will handle abuse later. + +**Prompt content leaves the buyer's device.** `sagePrompt` carries +conversation content to a third party the buyer has chosen to trust for +routing decisions, which is a strictly larger disclosure than ordinary +inference — an inference seller sees the prompt it actually answers; a +routing peer sees a representation of the prompt for every conversation a +buyer routes, whether or not that peer ends up serving it. A buyer SHOULD +apply the same trust judgment to a routing peer it applies to any seller it +sends prompts to, and an implementation SHOULD give a buyer visibility into +which peer identity it is routing through. + +**A routing peer can steer buyers toward sellers it favors.** Because the +routing peer's returned order is treated as the decision (see Rationale), a +dishonest or compromised routing peer could bias its ranking toward +affiliated sellers rather than the buyer's actual best interest. This is +bounded, not eliminated, by two things this AIP requires: the buyer's own +`constraints` are still enforced locally regardless of what the routing peer +returns, and a buyer client retains final policy authority to reject or +reorder around any given candidate before dispatch — a routing peer can bias +an ordering, but it cannot force a buyer to pay for something outside that +buyer's own price, trust, or allow/block-list bounds. + +**Unfiltered constraints could otherwise let an untrusted candidate through.** +Because a routing peer's own filtering is advisory (Rationale), an +implementation that skips local re-filtering after `selectRoute` returns +would let a stale or manipulated ranked list bypass the buyer's actual +policy. A conforming `Router` implementation MUST re-apply the buyer's +`constraints` locally before dispatch, not merely forward whatever the +routing peer returned. + +**The daily digest is aggregate but still linkable across time.** Even +without prompt content, a per-subscriber sequence of daily aggregates — +`modelMix` in particular — can function as a coarse usage profile of one +buyer over time if stored under a stable, real buyer identifier. A routing +peer operator SHOULD key stored digests by a one-way function of the buyer's +peer id rather than the raw id, so that day-to-day linkage for calibration +purposes does not also reveal which real network peer a subscriber is +outside the routing peer's own records. This AIP does not mandate a specific +scheme, since digest retention and anonymization policy are the routing +peer operator's responsibility, not a wire-protocol requirement. + +**Plugins never hold a buyer's signing key.** `configureDailySigning`'s +closure-based design exists specifically so that a `Router` — including a +third-party one, since this protocol is intentionally open to competing +implementations — never receives the buyer's actual private key or a +handle capable of signing arbitrary messages. A conforming host +implementation MUST NOT expose its payment manager or signer directly to +plugin code under any of this AIP's new interface methods. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE). From bd25be1be0567eabc6dd15e88fee28756b6b40b7 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 10:07:15 +0100 Subject: [PATCH 02/24] Revise the /_antseed/route wire schema RouteRequestBody: sagePrompt -> inputMessage, contextTokens -> promptTokens (the field names read as an implementation detail of one particular scoring backend, not a protocol-level concept every routing peer needs to share). RouteResponseBody: drop score and predictedQuality from ranked[] -- a routing peer's own internal ranking signal, not something a buyer can recompute or verify, and not needed once the returned order itself is the decision (see Rationale, "Preserving the routing peer's returned order"). Wrap the four predicted* fields into a single estimate object (costUsd/inputTokens/cachedInputTokens/outputTokens), matching the units a buyer already reconciles against observed usage elsewhere in this protocol. Drop baselineSuggestion entirely -- a routing peer's own alternate suggestion duplicates what `ranked`'s own order already expresses, and a buyer's fallback-target logic has its own, better-motivated source for a fallback model (its own already-chosen default), not a third party's opinion of one. Collapse the three-field receipt object into a single router: string -- a plain identifier for logging/diagnostics, not a structured field this AIP has any reason to standardize further. Also flags a real implementation gap found while updating the reference branch: its desktop application hardcodes a single vendor router as the only one its UI can enable, and its persisted preference (autoSubscriptionEnabled: boolean) has no field for which router a multi-router build would select -- noted under Reference Implementation alongside the two gaps already disclosed there. --- AIPS/aip-5.md | 50 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index 8220705..2ff80dd 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -171,8 +171,8 @@ The `POST /_antseed/route` request body: interface RouteRequestBody { v: 1; cqt: number; - sagePrompt: string; - contextTokens: number; + inputMessage: string; + promptTokens: number; expectedCachedTokens: Array<{ model: string; peer: string; tokens: number }>; constraints: { maxInputUsdPerMillion?: number; @@ -184,10 +184,10 @@ interface RouteRequestBody { ``` `cqt` is a cost/quality tradeoff value on a 0–10 scale, buyer-chosen. -`sagePrompt` MUST be the routing peer's own input format for whatever ranking -scheme it runs; this AIP does not standardize prompt content, only that a -string field carries it. `expectedCachedTokens` lets the buyer report, per -candidate it has itself observed billing for, how many tokens of this +`inputMessage` MUST be the routing peer's own input format for whatever +ranking scheme it runs; this AIP does not standardize prompt content, only +that a string field carries it. `expectedCachedTokens` lets the buyer report, +per candidate it has itself observed billing for, how many tokens of this conversation's prefix that seller already holds warm — a routing peer MUST NOT be expected to track this itself, since a routing peer that retains no per-conversation history cannot observe it. `constraints` carries the buyer's @@ -203,16 +203,10 @@ interface RouteResponseBody { ranked: Array<{ model: string; peer: string; - score: number; - predictedQuality: number; - predictedCostUsd: number; - predictedInputTokens: number; - predictedCachedInputTokens: number; - predictedOutputTokens: number; + estimate: { costUsd: number; inputTokens: number; cachedInputTokens: number; outputTokens: number }; price: { inUsdPerM: number; outUsdPerM: number; cachedInUsdPerM: number }; }>; - baselineSuggestion: { model: string; peer: string; price: { inUsdPerM: number; outUsdPerM: number } } | null; - receipt: { routerId: string; artifactVersion: string; lambdaVersion: string }; + router: string; } ``` @@ -226,6 +220,19 @@ place, not by re-ordering the remainder. `price` MUST be populated for every ranked entry, not only the winner, so a buyer can compute a savings figure against any candidate without a separate price lookup and without the routing peer needing to remember which candidates it offered to which buyer. +`estimate` carries the routing peer's own per-candidate prediction, in the +same units a buyer already reconciles against observed usage elsewhere in +this protocol; it MUST be populated for every ranked entry, same as `price`. +Whatever internal signal the routing peer used to produce the ranking itself +(a numeric score, a raw quality prediction, or anything else its own ranking +scheme computes) is that routing peer's own implementation detail and is NOT +part of this wire response — a buyer has no use for a number it cannot +recompute or independently verify, only for the order it produces and the +concrete price/estimate figures it can check usage against. `router` is a +single, routing-peer-chosen string identifying which router/version served +this response, for a buyer's own logging or support diagnostics; this AIP +does not standardize its format beyond being a plain string, and a buyer +MUST NOT parse it for routing decisions. The `POST /_antseed/route/digest` request body: @@ -515,6 +522,21 @@ only, via a mechanism a companion pricing AIP covers, and its node implementation still gates announcement on at least one registered provider. Both are tracked as follow-up work against the reference implementation. +The reference implementation's CLI (`apps/cli`) already loads a `Router` by +plugin package name, matching this AIP's plugin-agnostic design (see +Rationale, "Sentinel model strings are not part of this protocol") — any +router package name works, and its own default is the unrelated fixed-model +`local` router, not any particular routing peer's plugin. Its desktop +application, however, currently hardcodes a single vendor router package as +the only one its UI can enable, and the buyer preference it persists +(`ModelRoutingPreferences.autoSubscriptionEnabled`) is a plain boolean -- +sufficient to represent "the one router this build knows about is on or +off," but with no field recording *which* router package a multi-router +build would have selected. A conforming desktop implementation that wants to +offer more than one `Router` plugin will need a router-identifying field +here, not just a boolean; tracked as follow-up work against the reference +implementation, same as the two gaps above. + ## Security Considerations **Unmetered ranking is a denial-of-service surface.** Ranking a request MAY From ed76bb514170a5798fda2beecdb192193dc8a7f5 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 10:32:17 +0100 Subject: [PATCH 03/24] Add Dawid (@Dawe000) as a co-author --- AIPS/aip-5.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index 2ff80dd..88d0e44 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -2,7 +2,7 @@ aip: 5 title: Model Routing Peers description: Defines the routing peer role, its discovery and reserved request paths, and the buyer-side router interface for cross-model routing. -author: Marco De Rossi (@marcoderossi90) +author: Marco De Rossi (@marcoderossi90), Dawid (@Dawe000) discussions-to: https://github.com/AntSeed/antseed/discussions status: Draft type: Standards Track From 1efc29ce51deb13acfc3769fa3534669325feb28 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 10:42:02 +0100 Subject: [PATCH 04/24] Drop regenerations/overrides from DailyDigestBody --- AIPS/aip-5.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index 88d0e44..389808f 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -2,7 +2,7 @@ aip: 5 title: Model Routing Peers description: Defines the routing peer role, its discovery and reserved request paths, and the buyer-side router interface for cross-model routing. -author: Marco De Rossi (@marcoderossi90), Dawid (@Dawe000) +author: Marco De Rossi (@marcoderossi90) discussions-to: https://github.com/AntSeed/antseed/discussions status: Draft type: Standards Track @@ -250,8 +250,6 @@ interface DailyDigestBody { observedCachedInputTokens: number; observedOutputTokens: number; modelMix: Record; - regenerations: number; - overrides: number; failovers: number; timeouts: number; avgRoutingLatencyMs: number | null; From d3cc7f395bbb9a6740ddca44a4d2ee45a754aece Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 10:50:47 +0100 Subject: [PATCH 05/24] Drop the desktop hardcoding note -- being fixed separately, not a spec concern for now --- AIPS/aip-5.md | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index 389808f..f4891bf 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -520,21 +520,6 @@ only, via a mechanism a companion pricing AIP covers, and its node implementation still gates announcement on at least one registered provider. Both are tracked as follow-up work against the reference implementation. -The reference implementation's CLI (`apps/cli`) already loads a `Router` by -plugin package name, matching this AIP's plugin-agnostic design (see -Rationale, "Sentinel model strings are not part of this protocol") — any -router package name works, and its own default is the unrelated fixed-model -`local` router, not any particular routing peer's plugin. Its desktop -application, however, currently hardcodes a single vendor router package as -the only one its UI can enable, and the buyer preference it persists -(`ModelRoutingPreferences.autoSubscriptionEnabled`) is a plain boolean -- -sufficient to represent "the one router this build knows about is on or -off," but with no field recording *which* router package a multi-router -build would have selected. A conforming desktop implementation that wants to -offer more than one `Router` plugin will need a router-identifying field -here, not just a boolean; tracked as follow-up work against the reference -implementation, same as the two gaps above. - ## Security Considerations **Unmetered ranking is a denial-of-service surface.** Ranking a request MAY From f356a9cd4a7d2a9181eafb484cd1f94233d9a0b8 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 14:04:00 +0100 Subject: [PATCH 06/24] Drop the Test Cases section Premature for a Draft AIP whose reference implementation is still WIP -- tests belong once the interface stabilizes, not pinned to this revision. --- AIPS/aip-5.md | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index f4891bf..397c3a4 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -469,31 +469,6 @@ peers that advertise no `routing.v1` capability, and a routing-advertising peer is ignored by buyers that do not implement `selectRoute` at all, exactly as [AIP-3](./aip-3.md) describes for verifier capabilities. -## Test Cases - -A conforming implementation MUST include tests for: - -- routing-handler dispatch on `POST /_antseed/route` and - `POST /_antseed/route/digest` before provider matching; -- `404` `not_routing_peer` when no handler is registered, and `500` - `routing_error` on a handler that throws; -- per-buyer rate limiting on `/_antseed/route`, with cheap `400`/`402`/`404` - rejections not consuming quota; -- capability round-trip: building and parsing the `routing.v1` capability - string, and confirming it fits the existing capability grammar and count - limits; -- a node with a registered routing handler and zero registered providers - still starting its announcer and including `routing.v1`; -- `selectRoute` returning `null` falling through to the unmodified existing - peer-selection pipeline, with no behavior change for a `Router` that does - not implement it; -- candidate order from a non-null `selectRoute` result being preserved - through the failover walk, with no local reputation re-sort applied; -- `onResult` receiving the new optional telemetry fields without breaking a - `Router` that ignores them; -- round-trip encode/decode of `RouteRequestBody`, `RouteResponseBody`, and - `DailyDigestBody` at `v: 1`. - ## Reference Implementation Reference implementation branch: From 6d03426b5f4ba8e41996ca530d9e8ee90a609b4a Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 14:36:09 +0100 Subject: [PATCH 07/24] Clarify ModelRoutingPreferences scope: buyer preferences, not Router config A routing peer's own setup belongs to the host's plugin-configuration mechanism, not this type or the wire protocol. --- AIPS/aip-5.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index 397c3a4..2184a5c 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -349,6 +349,14 @@ type ModelRoutingPreferences = { }; ``` +`ModelRoutingPreferences` is buyer routing preference data, not `Router` +configuration — a routing peer's own setup (its URL, credentials, seller +identity) lives in whatever plugin-configuration mechanism the host already +uses for its other plugins, not in this type or on the wire. A `Router` +implementation MUST be fully configurable through its own plugin +configuration alone; it MUST NOT require `ModelRoutingPreferences` to carry +anything beyond the buyer-facing preference fields already listed above. + A host (buyer proxy) implementation MUST call `selectRoute` before its existing fixed-model peer-narrowing step, whenever the registered `Router` implements it, and MUST treat a `null` result — including a `Router` that From 5b94382e2235b0b3f59173d7b323a6d2d3d105b0 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 14:58:21 +0100 Subject: [PATCH 08/24] Comment every field in every wire-schema and interface block RouteRequestBody, RouteResponseBody, DailyDigestBody, the Router extensions, and RouteCandidate/RoutingDecisionRow/ConversationIdentity/ ModelRoutingPreferences now carry an inline comment on every field, not just the subset already covered by surrounding prose. --- AIPS/aip-5.md | 182 ++++++++++++++++++++++++++++---------------------- 1 file changed, 101 insertions(+), 81 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index 2184a5c..7499e3c 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -169,16 +169,20 @@ The `POST /_antseed/route` request body: ```typescript interface RouteRequestBody { - v: 1; - cqt: number; - inputMessage: string; - promptTokens: number; - expectedCachedTokens: Array<{ model: string; peer: string; tokens: number }>; + v: 1; // wire schema version + cqt: number; // cost/quality tradeoff, 0-10, buyer-chosen + inputMessage: string; // routing peer's own ranking-input format; opaque to this AIP + promptTokens: number; // buyer's own token estimate for inputMessage + expectedCachedTokens: Array<{ + model: string; // candidate model this cache-hit estimate applies to + peer: string; // candidate peer this cache-hit estimate applies to + tokens: number; // tokens of this conversation's prefix the buyer has observed that peer/model already holding warm + }>; constraints: { - maxInputUsdPerMillion?: number; - minTrustScore?: number; - allowedPeerIds?: string[]; - blockedPeerIds?: string[]; + maxInputUsdPerMillion?: number; // buyer's price ceiling; advisory pre-filter, not authoritative + minTrustScore?: number; // buyer's trust floor, 0-100 scale; advisory pre-filter, not authoritative + allowedPeerIds?: string[]; // buyer's explicit allow-list; advisory pre-filter, not authoritative + blockedPeerIds?: string[]; // buyer's explicit block-list; advisory pre-filter, not authoritative }; } ``` @@ -199,14 +203,23 @@ The `POST /_antseed/route` response body, on success: ```typescript interface RouteResponseBody { - v: 1; + v: 1; // wire schema version ranked: Array<{ - model: string; - peer: string; - estimate: { costUsd: number; inputTokens: number; cachedInputTokens: number; outputTokens: number }; - price: { inUsdPerM: number; outUsdPerM: number; cachedInUsdPerM: number }; + model: string; // candidate's model id + peer: string; // candidate's peer id + estimate: { + costUsd: number; // routing peer's predicted total cost for this candidate + inputTokens: number; // routing peer's predicted fresh input tokens + cachedInputTokens: number; // routing peer's predicted cached input tokens + outputTokens: number; // routing peer's predicted output tokens + }; + price: { + inUsdPerM: number; // candidate's fresh input price, USD per million tokens + outUsdPerM: number; // candidate's output price, USD per million tokens + cachedInUsdPerM: number; // candidate's cached input price, USD per million tokens + }; }>; - router: string; + router: string; // routing-peer-chosen identifier for which router/version produced this response; diagnostics only, not parseable } ``` @@ -238,22 +251,22 @@ The `POST /_antseed/route/digest` request body: ```typescript interface DailyDigestBody { - v: 1; - period: string; - routedRequests: number; - predictedCostUsd: number; - observedCostUsd: number; - predictedInputTokens: number; - predictedCachedInputTokens: number; - predictedOutputTokens: number; - observedInputTokens: number; - observedCachedInputTokens: number; - observedOutputTokens: number; - modelMix: Record; - failovers: number; - timeouts: number; - avgRoutingLatencyMs: number | null; - cqtDistribution: Record; + v: 1; // wire schema version + period: string; // calendar day, YYYY-MM-DD, buyer's own local time zone + routedRequests: number; // count of requests routed this period + predictedCostUsd: number; // sum of estimate.costUsd across this period's routed requests + observedCostUsd: number; // sum of what the buyer actually paid this period + predictedInputTokens: number; // sum of predicted fresh input tokens this period + predictedCachedInputTokens: number; // sum of predicted cached input tokens this period + predictedOutputTokens: number; // sum of predicted output tokens this period + observedInputTokens: number; // sum of actual fresh input tokens this period + observedCachedInputTokens: number; // sum of actual cached input tokens this period + observedOutputTokens: number; // sum of actual output tokens this period + modelMix: Record; // request count per model actually used this period + failovers: number; // count of requests that failed over to a later-ranked candidate + timeouts: number; // count of requests that timed out waiting on a routing decision + avgRoutingLatencyMs: number | null; // mean wall-clock time to produce a routing decision this period; null if none were timed + cqtDistribution: Record; // request count per cqt value used this period } ``` @@ -274,78 +287,85 @@ unchanged. ```typescript interface Router { // Existing, unchanged: - selectPeer(req: SerializedHttpRequest, peers: PeerInfo[]): PeerInfo | null; + selectPeer(req: SerializedHttpRequest, peers: PeerInfo[]): PeerInfo | null; // fixed-model peer selection, predates this AIP onResult(peer: PeerInfo, result: { success: boolean; latencyMs: number; tokens: number; // New, optional, additive fields on the existing result object: - freshInputTokens?: number; - cachedInputTokens?: number; - outputTokens?: number; - estimatedCostUsd?: number | null; - requestId?: string; + freshInputTokens?: number; // observed fresh (non-cached) input tokens for this request + cachedInputTokens?: number; // observed cached input tokens for this request + outputTokens?: number; // observed output tokens for this request + estimatedCostUsd?: number | null; // observed cost for this request, same computation buyer-proxy already derives at both onResult call sites + requestId?: string; // the originating client request's id, stable across a peer walk for one client request; lets a router pair this result with the selectRoute decision that produced it }): void; // New, all optional: selectRoute?( - req: SerializedHttpRequest, - peers: PeerInfo[], - conversation: ConversationIdentity | null, - routingPreferences: ModelRoutingPreferences | null, - defaultRoutedModel?: string | null, - ): Promise; - - getRoutingDecisions?(): RoutingDecisionRow[]; - configureDailySigning?(signDailyIfNeeded: (sellerPeerId: string) => Promise): void; - triggerDailySigningCheck?(): Promise; - updateRoutingPreferences?(preferences: ModelRoutingPreferences): void; + req: SerializedHttpRequest, // raw, unmodified request -- a plugin reads its own sentinel from req.body itself + peers: PeerInfo[], // the buyer's currently discovered peer set + conversation: ConversationIdentity | null, // identity of the chat this request belongs to, null when it can't be determined + routingPreferences: ModelRoutingPreferences | null, // the buyer's current routing preferences + defaultRoutedModel?: string | null, // the pre-existing "antseed" alias's currently-resolved target, host-owned state + ): Promise; // null is a decline, falling through to the unmodified fixed-model pipeline + + getRoutingDecisions?(): RoutingDecisionRow[]; // the router's own local routing_decisions ledger, if it keeps one, for a host's savings-dashboard UI + configureDailySigning?(signDailyIfNeeded: (sellerPeerId: string) => Promise): void; // hands the router a host-provided signing closure it can call to trigger its own daily payment; the router never touches the buyer's actual signer directly + triggerDailySigningCheck?(): Promise; // lets a host-owned background timer poke the same daily-signing gate outside of any request in flight, so a subscription doesn't lapse on a day with no chat traffic + updateRoutingPreferences?(preferences: ModelRoutingPreferences): void; // pushed by the host whenever live preferences change, including once at startup, so an already-running router picks up changes without a request in flight } type RouteCandidate = { - peer: PeerInfo; - peerId: string; - serviceId: string; - request: SerializedHttpRequest; - reputation: number; - hasCachedInputPricing: boolean; - inputUsdPerMillion: number | null; - outputUsdPerMillion: number | null; - minImageUsdPerImage: number | null; + peer: PeerInfo; // the candidate seller's full discovered peer record + peerId: string; // the candidate seller's peer id + serviceId: string; // the model this candidate serves + request: SerializedHttpRequest; // already model-substituted for this candidate + reputation: number; // the candidate's reputation, 0-100 scale + hasCachedInputPricing: boolean; // whether the candidate advertises a cached-input price for this service + inputUsdPerMillion: number | null; // the candidate's advertised fresh input price, null if unadvertised + outputUsdPerMillion: number | null; // the candidate's advertised output price, null if unadvertised + minImageUsdPerImage: number | null; // the candidate's advertised image-generation price, null for a non-image service }; type RoutingDecisionRow = { - atMs: number; - actualModel: string; - actualPeer: string; - actualPromptTokens: number; - actualCachedTokens: number; - actualCompletionTokens: number; - actualUsdcPaid: number; - predictedCostUsd: number | null; - predictedInputTokens: number | null; - predictedCachedInputTokens: number | null; - predictedOutputTokens: number | null; - cqt: number; - routingLatencyMs: number | null; + atMs: number; // wall-clock time this decision was made + actualModel: string; // the model actually used + actualPeer: string; // the peer actually used + actualPromptTokens: number; // observed prompt tokens, fresh and cached combined + actualCachedTokens: number; // observed cached-subset of actualPromptTokens + actualCompletionTokens: number; // observed output tokens + actualUsdcPaid: number; // what actually settled for this request; not a billing record itself, real settlement is governed by the signed SpendingAuth + predictedCostUsd: number | null; // this decision's own predicted cost, captured at selectRoute time; null when the gate skipped the call entirely + predictedInputTokens: number | null; // predicted fresh input tokens; null when the gate skipped the call entirely + predictedCachedInputTokens: number | null; // predicted cached input tokens; null when the gate skipped the call entirely + predictedOutputTokens: number | null; // predicted output tokens; null when the gate skipped the call entirely + cqt: number; // the cost/quality dial value in effect for this decision + routingLatencyMs: number | null; // wall-clock time the routing call itself took; null when the gate skipped the call entirely (e.g. a reused decision) + // Price snapshot for each fixed, curated baseline model that was actually + // present in this decision's ranked response, collapsed across peers to + // the best available offer per model, keyed by model name -- absent + // entirely for a baseline model not offered at the moment of this + // decision. Lets a savings dashboard compare actual paid against one + // fixed reference model's real price at the time of THIS decision, without + // holding a live price table or re-fetching anything. baselinePrices: Record; }; type ConversationIdentity = { - tool: string; - sessionKey: string; - parentSessionKey: string | null; - isUserThread: boolean; + tool: string; // slug for the originating tool (e.g. 'claude-code', 'codex', 'opencode', 'unknown') + sessionKey: string; // stable per-conversation key as sent by the tool + parentSessionKey: string | null; // parent session for subagent traffic, when the tool advertises one + isUserThread: boolean; // false when the tool declares this thread its own housekeeping rather than a user's chat; true by default }; type ModelRoutingPreferences = { - preferFreePeers: boolean; - maxInputUsdPerMillion: number; - minTrustScore: number; - allowedPeerIds: string[]; - blockedPeerIds: string[]; - cqt?: number; - autoSubscriptionEnabled?: boolean; + preferFreePeers: boolean; // prefer $0-priced candidates when ranking + maxInputUsdPerMillion: number; // buyer's price ceiling + minTrustScore: number; // buyer's trust floor, 0-100 scale + allowedPeerIds: string[]; // buyer's explicit allow-list + blockedPeerIds: string[]; // buyer's explicit block-list + cqt?: number; // cost/quality tradeoff dial, one of the five discrete values {1,3,5,7,9}; only meaningful to a router that implements selectRoute + autoSubscriptionEnabled?: boolean; // explicit opt-in to a subscription-priced router's daily signing; off by default, a router gating real signing on this MUST treat "unknown" the same as false, never as implicit consent }; ``` From 1dd38416e3a7e20296128ea446bce9db7c5c63c6 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 15:03:58 +0100 Subject: [PATCH 09/24] Fix two regressions from earlier edits - The co-author line was silently reverted by the digest-field-removal commit (1efc29c) -- restored. - Two references to sagePrompt survived the field's rename to inputMessage earlier tonight. --- AIPS/aip-5.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index 7499e3c..cf3ad94 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -2,7 +2,7 @@ aip: 5 title: Model Routing Peers description: Defines the routing peer role, its discovery and reserved request paths, and the buyer-side router interface for cross-model routing. -author: Marco De Rossi (@marcoderossi90) +author: Marco De Rossi (@marcoderossi90), Dawid (@Dawe000) discussions-to: https://github.com/AntSeed/antseed/discussions status: Draft type: Standards Track @@ -144,7 +144,7 @@ Dispatch rules: - Both paths dispatch to the same handler. A handler distinguishes a route request from a digest submission by `req.path`, since the request bodies defined below are structurally distinct (a route request always carries - `sagePrompt`; a digest submission never does). An implementation MAY also + `inputMessage`; a digest submission never does). An implementation MAY also reject a request on the wrong path with `400` rather than relying on body shape. - A routing peer that gates on payment (left to a companion AIP; see Payments @@ -534,7 +534,7 @@ a companion pricing AIP is adopted, the rate limit is the only cost control a routing peer has, and implementations MUST NOT skip it on the theory that payment will handle abuse later. -**Prompt content leaves the buyer's device.** `sagePrompt` carries +**Prompt content leaves the buyer's device.** `inputMessage` carries conversation content to a third party the buyer has chosen to trust for routing decisions, which is a strictly larger disclosure than ordinary inference — an inference seller sees the prompt it actually answers; a From 08739ce0dbee209ab47b7280b381c33eac5ddf9b Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 15:07:02 +0100 Subject: [PATCH 10/24] Declare requires: 3 -- reuses AIP-3's SellerRequest/SellerResponse shapes and dispatch pattern --- AIPS/aip-5.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index cf3ad94..a54b3be 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -8,6 +8,7 @@ status: Draft type: Standards Track category: Core created: 2026-08-27 +requires: 3 --- ## Abstract From 6bd5524f00a3934998a9c1a4a8b9344a8010fb4a Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 15:11:52 +0100 Subject: [PATCH 11/24] Drop the Reference Implementation section --- AIPS/aip-5.md | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index a54b3be..d7bd1df 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -498,32 +498,6 @@ peers that advertise no `routing.v1` capability, and a routing-advertising peer is ignored by buyers that do not implement `selectRoute` at all, exactly as [AIP-3](./aip-3.md) describes for verifier capabilities. -## Reference Implementation - -Reference implementation branch: -https://github.com/levantolabs/antseed-levanto-router/tree/model-routing - -Key surfaces: - -- `RoutingServerHandler`, `ANTSEED_ROUTE_PATH`, `ANTSEED_ROUTE_DIGEST_PATH` in - `packages/node/src/interfaces/plugin.ts`; -- reserved-path dispatch in `packages/node/src/seller-request-handler.ts`; -- the extended `Router` interface, `RouteCandidate`, and `RoutingDecisionRow` - in `packages/node/src/interfaces/buyer-router.ts`; -- `ConversationIdentity` in `packages/node/src/routing/conversation-identity.ts`; -- `ModelRoutingPreferences` in `packages/node/src/routing/model-route-ranking.ts`; -- the `selectRoute` call site, candidate walk, and extended `onResult` - telemetry in `apps/cli/src/proxy/buyer-proxy.ts`; -- the vendor `Router` implementation and wire-schema types in - `plugins/router-levanto/src/router.ts` and `digest.ts`. - -The reference branch's `routing.v1` capability advertisement and the -announcer fix for provider-less peers are not yet implemented as of this -AIP's Draft status; the branch currently advertises a routing peer's price -only, via a mechanism a companion pricing AIP covers, and its node -implementation still gates announcement on at least one registered provider. -Both are tracked as follow-up work against the reference implementation. - ## Security Considerations **Unmetered ranking is a denial-of-service surface.** Ranking a request MAY From 3695c0ba45e21d3a3e3698a02ce8eb64e61d1167 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 15:23:10 +0100 Subject: [PATCH 12/24] Consolidate Rationale and Security Considerations Rationale: 8 points down to 4 -- merged the three points that were all really "modeled on AIP-3" (capability string, announcer fix, overall shape) into one, and folded the payment/digest scope points together. Security Considerations: 6 points down to 4 -- merged "routing peer can steer" with "unfiltered constraints let an untrusted candidate through", since both were the same underlying point (advisory filtering, buyer re-applies policy locally) stated twice across the two sections. Every MUST/SHOULD is preserved; only the surrounding prose is trimmed. --- AIPS/aip-5.md | 183 ++++++++++++++++++-------------------------------- 1 file changed, 66 insertions(+), 117 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index d7bd1df..8e4455c 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -419,69 +419,40 @@ or something else — is left entirely to that companion proposal. ## Rationale -**Capability string, not a new capability enum value.** The obvious design — -add `'routing'` to `ProviderCapability` — does not work with what the network -actually runs: the DHT capability-topic machinery that enum feeds is unwired -end to end. `routing.v1` as a peer capability string, by contrast, rides -exactly the same encode/decode/validate path `verifier.*` -([AIP-3](./aip-3.md)) already exercises in production, so it costs no new -metadata version and inherits a mechanism already proven to work. - -**Fixing announcer gating rather than working around it.** A routing peer -that never runs an inference `Provider` is not a corner case — it is the -expected shape of a peer whose entire purpose is ranking, not serving. Making -peer announcement conditional on `Provider` registration would make routing -peers structurally undiscoverable; the fix belongs in the node's -announcement logic, not in a workaround at the routing layer. - -**Modeled on attestation deliberately.** [AIP-3](./aip-3.md) already solved -the exact shape of problem this AIP has: a seller-side handler that claims a -reserved path, serves it before provider matching and payment, needs a -per-buyer rate limit because the work is expensive, and is advertised as a -capability string a buyer filters for during ordinary discovery. Rather than -re-deriving that design, this AIP reuses it, changing only what is -substantively different (ranking model-and-seller pairs, not proving a -claim) and leaving the plumbing shape the same. - -**Preserving the routing peer's returned order.** A ranked list encodes a -quality/cost tradeoff the buyer's own client cannot independently recompute -from a peer's reputation and price alone — reputation is one input among -several the routing peer already folds in. Re-sorting the list locally by -reputation, the way AntSeed's existing fixed-model pipeline does when -choosing among sellers of one already-decided model, would discard that -tradeoff. The buyer's own policy filtering still applies, but as a filter -over the given order, not a re-ranking of it. - -**The routing peer's constraints filtering is advisory, not authoritative.** -A buyer sends its own price/trust ceilings and allow/block lists so a -routing peer can pre-filter to what that buyer can actually use, but a buyer -MUST still re-apply its own policy locally before dispatching, because -constraints are a snapshot at request time: a peer can go unreachable, enter -a cooldown, or drop out of an allow list update between the routing call and -dispatch. Treating the routing peer's filtering as a courtesy rather than a -guarantee is what lets a stale constraint degrade a choice instead of -breaking it. - -**Sentinel model strings are not part of this protocol.** A host that -special-cased one router's sentinel string would privilege that -implementation over any other `Router` a buyer might load. Keeping sentinel -recognition entirely inside the plugin, with the host forwarding the -unmodified request body, is what keeps the router-plugin slot genuinely -pluggable rather than nominally so. - -**No payment mechanism in this AIP.** Coupling a new peer role to a specific -pricing scheme in the same proposal would force reviewers to accept or reject -both together. A routing peer is useful, and reviewable, as an unpriced -service first — the same bootstrapping path attestation took. A companion -AIP can specify metered, subscription, or other pricing for a routing peer -without reopening anything specified here. - -**Digest as a separate submission, not bundled into a payment message.** A -routing peer's own reserved-path infrastructure already exists once this AIP -ships; reusing it for a small, periodic aggregate submission needs no new -protocol surface. Extending a closed, protocol-wide payment-message enum for -what is explicitly optional, non-billing statistics would be a heavier change -for a smaller benefit. +**Modeled on AIP-3's attestation design, not a new pattern.** +[AIP-3](./aip-3.md) already solved this exact shape of problem: a seller-side +handler on a reserved path, served before provider matching and payment, +rate-limited per buyer, advertised as a capability string. This AIP reuses +that shape rather than inventing a parallel one. Two specific choices follow +from it: `routing.v1` is a capability *string*, not a new `ProviderCapability` +enum value, because the DHT machinery that enum would need is unwired end to +end and the capability-string path already works in production; and a routing +peer's announcer MUST start even with zero registered providers, since a +peer whose entire purpose is ranking rather than serving is the normal case +for this role, not a corner case to work around. + +**The routing peer's ranked order is the decision, not a suggestion.** The +order encodes a quality/cost tradeoff a buyer can't independently recompute +from price and reputation alone, so a buyer MUST NOT locally re-sort it — +that would discard the tradeoff the ranking exists to provide. But the +routing peer's own `constraints` filtering is advisory only: a buyer MUST +still re-apply its own price/trust/allow-block policy locally before +dispatch, since a candidate can go stale (peer drops offline, cools down) +between the routing call and dispatch. Policy filters the given order in +place; it never re-ranks it. + +**Sentinel strings stay inside the plugin.** A host that recognized one +router's sentinel model string by name would privilege that implementation +over any other `Router` a buyer might load. The host forwards the request +unmodified; only the plugin knows what its own sentinel means. + +**Pricing and the digest are both deliberately out of scope here.** This AIP +treats a routing peer as free, the same bootstrapping path attestation took — +bundling a pricing scheme into this proposal would force reviewers to accept +or reject both together. The daily digest reuses this AIP's own reserved-path +infrastructure rather than the protocol's payment-message enum, since it's +optional, non-billing statistics, not something that needs a new +protocol-wide surface. ## Backwards Compatibility @@ -500,62 +471,40 @@ as [AIP-3](./aip-3.md) describes for verifier capabilities. ## Security Considerations -**Unmetered ranking is a denial-of-service surface.** Ranking a request MAY -be expensive, calling out to a separate scoring process. Exactly as with -attestation, a seller MUST rate-limit `/_antseed/route` per buyer peer with -bounded tracking state, so an unpaid or spam caller cannot exhaust a routing -peer's resources for free. This AIP defines no payment gate itself, so until -a companion pricing AIP is adopted, the rate limit is the only cost control a -routing peer has, and implementations MUST NOT skip it on the theory that -payment will handle abuse later. - -**Prompt content leaves the buyer's device.** `inputMessage` carries -conversation content to a third party the buyer has chosen to trust for -routing decisions, which is a strictly larger disclosure than ordinary -inference — an inference seller sees the prompt it actually answers; a -routing peer sees a representation of the prompt for every conversation a -buyer routes, whether or not that peer ends up serving it. A buyer SHOULD -apply the same trust judgment to a routing peer it applies to any seller it -sends prompts to, and an implementation SHOULD give a buyer visibility into -which peer identity it is routing through. - -**A routing peer can steer buyers toward sellers it favors.** Because the -routing peer's returned order is treated as the decision (see Rationale), a -dishonest or compromised routing peer could bias its ranking toward -affiliated sellers rather than the buyer's actual best interest. This is -bounded, not eliminated, by two things this AIP requires: the buyer's own -`constraints` are still enforced locally regardless of what the routing peer -returns, and a buyer client retains final policy authority to reject or -reorder around any given candidate before dispatch — a routing peer can bias -an ordering, but it cannot force a buyer to pay for something outside that -buyer's own price, trust, or allow/block-list bounds. - -**Unfiltered constraints could otherwise let an untrusted candidate through.** -Because a routing peer's own filtering is advisory (Rationale), an -implementation that skips local re-filtering after `selectRoute` returns -would let a stale or manipulated ranked list bypass the buyer's actual -policy. A conforming `Router` implementation MUST re-apply the buyer's -`constraints` locally before dispatch, not merely forward whatever the -routing peer returned. - -**The daily digest is aggregate but still linkable across time.** Even -without prompt content, a per-subscriber sequence of daily aggregates — -`modelMix` in particular — can function as a coarse usage profile of one -buyer over time if stored under a stable, real buyer identifier. A routing -peer operator SHOULD key stored digests by a one-way function of the buyer's -peer id rather than the raw id, so that day-to-day linkage for calibration -purposes does not also reveal which real network peer a subscriber is -outside the routing peer's own records. This AIP does not mandate a specific -scheme, since digest retention and anonymization policy are the routing -peer operator's responsibility, not a wire-protocol requirement. +**Unmetered ranking is a denial-of-service surface.** Ranking MAY be +expensive (a separate scoring process). This AIP defines no payment gate, so +until a companion pricing AIP exists, per-buyer rate limiting on +`/_antseed/route` is the only cost control a routing peer has — an +implementation MUST NOT skip it on the theory that payment will handle abuse +later. + +**Prompt content leaves the buyer's device.** `inputMessage` discloses +conversation content to a third party for every routed request, whether or +not that peer ends up serving it — strictly more exposure than ordinary +inference, where a seller only sees prompts it actually answers. A buyer +SHOULD trust a routing peer the way it trusts any seller it sends prompts to, +and SHOULD be able to see which peer identity it's routing through. + +**A dishonest routing peer can steer, but not override, a buyer's policy.** +Because the returned order is treated as the decision, a compromised routing +peer could bias its ranking toward affiliated sellers. This is bounded, not +eliminated: a `Router` implementation MUST re-apply the buyer's own +`constraints` locally before dispatch rather than trusting the routing peer's +own filtering (which is advisory, not authoritative — see Rationale), so a +routing peer can bias an ordering but cannot force a buyer to pay outside its +own price, trust, or allow/block bounds. + +**Daily digests are aggregate, but a stable buyer id still makes them +linkable over time.** A sequence of `modelMix` values keyed to a real peer id +is a coarse usage profile. A routing peer operator SHOULD key stored digests +by a one-way function of the buyer's peer id rather than the raw id; this AIP +leaves the specific scheme to the operator. **Plugins never hold a buyer's signing key.** `configureDailySigning`'s -closure-based design exists specifically so that a `Router` — including a -third-party one, since this protocol is intentionally open to competing -implementations — never receives the buyer's actual private key or a -handle capable of signing arbitrary messages. A conforming host -implementation MUST NOT expose its payment manager or signer directly to -plugin code under any of this AIP's new interface methods. +closure-based design means a `Router` — including a third-party one — never +receives the buyer's actual private key or a handle that can sign arbitrary +messages. A host implementation MUST NOT expose its payment manager or +signer directly to plugin code under any of this AIP's new methods. ## Copyright From 60916982b6e1eb87f633f549b268238410aabf75 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 15:28:00 +0100 Subject: [PATCH 13/24] Clarify prompt-disclosure point: routing peer sees it in addition to the seller, not instead of it --- AIPS/aip-5.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index 8e4455c..fadeecb 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -478,12 +478,13 @@ until a companion pricing AIP exists, per-buyer rate limiting on implementation MUST NOT skip it on the theory that payment will handle abuse later. -**Prompt content leaves the buyer's device.** `inputMessage` discloses -conversation content to a third party for every routed request, whether or -not that peer ends up serving it — strictly more exposure than ordinary -inference, where a seller only sees prompts it actually answers. A buyer -SHOULD trust a routing peer the way it trusts any seller it sends prompts to, -and SHOULD be able to see which peer identity it's routing through. +**Prompt content leaves the buyer's device.** `inputMessage` gives the +routing peer itself access to conversation content on every routed request — +in addition to, not instead of, whichever inference seller ultimately +answers it. Routing a request means two parties see the prompt instead of +one. A buyer SHOULD trust a routing peer the way it trusts any seller it +sends prompts to, and SHOULD be able to see which peer identity it's routing +through. **A dishonest routing peer can steer, but not override, a buyer's policy.** Because the returned order is treated as the decision, a compromised routing From 8488781c8660c2e784d3ff18159290d6ffc6b672 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 15:32:34 +0100 Subject: [PATCH 14/24] Match baselinePrices' comment style to every other field in the block --- AIPS/aip-5.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index fadeecb..8e9ee42 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -342,14 +342,7 @@ type RoutingDecisionRow = { predictedOutputTokens: number | null; // predicted output tokens; null when the gate skipped the call entirely cqt: number; // the cost/quality dial value in effect for this decision routingLatencyMs: number | null; // wall-clock time the routing call itself took; null when the gate skipped the call entirely (e.g. a reused decision) - // Price snapshot for each fixed, curated baseline model that was actually - // present in this decision's ranked response, collapsed across peers to - // the best available offer per model, keyed by model name -- absent - // entirely for a baseline model not offered at the moment of this - // decision. Lets a savings dashboard compare actual paid against one - // fixed reference model's real price at the time of THIS decision, without - // holding a live price table or re-fetching anything. - baselinePrices: Record; + baselinePrices: Record; // best available price per fixed, curated baseline model actually offered in this decision's ranked response, keyed by model name, absent when a baseline model wasn't offered; lets a savings dashboard compare against a fixed reference without a live price table }; type ConversationIdentity = { From 323fa81dbf51011e3d9bf674d25d2680dd3252e6 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 15:38:50 +0100 Subject: [PATCH 15/24] Move autoSubscriptionEnabled off ModelRoutingPreferences; drop DailyDigestBody autoSubscriptionEnabled gates configureDailySigning specifically -- a metered or free router has nothing to check it against, unlike the other ModelRoutingPreferences fields every router can use regardless of its own pricing model. It now lives with the rest of a subscription router's own setup, in its plugin configuration, not in this type. DailyDigestBody and the /_antseed/route/digest reserved path are implementation-specific to a subscription/metered pricing model, not something every routing peer needs -- dropped along with every reference to it (Reserved Request Paths, the rate-limit carve-out, Rationale, Backwards Compatibility, Security Considerations). Left a pointer in Rationale that a companion pricing AIP can define a digest or other periodic-reporting mechanism if a subscription-priced routing peer needs one, same as pricing itself. --- AIPS/aip-5.md | 94 +++++++++++++++------------------------------------ 1 file changed, 27 insertions(+), 67 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index 8e9ee42..3b3409e 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -23,7 +23,7 @@ failover walk exactly as it would for a fixed model choice. The proposal specifies three things: how a routing peer advertises itself and is discovered, without adding a new capability enum or a metadata codec -version bump; the two reserved request paths a routing peer serves and their +version bump; the reserved request path a routing peer serves and its dispatch and rate-limit rules, modeled directly on [AIP-3](./aip-3.md)'s attestation route; and five new optional methods on the existing buyer-side `Router` interface that let a plugin pick model and seller together, ahead of @@ -114,11 +114,10 @@ round-trip is introduced. ### Reserved Request Paths -The path prefix `/_antseed/route` (`ANTSEED_ROUTE_PATH`) and the path -`/_antseed/route/digest` (`ANTSEED_ROUTE_DIGEST_PATH`) are reserved. A seller -MUST route a `POST` request for either path to its registered routing -handler, and MUST do so before provider matching. A routing handler MUST be -implemented as: +The path `/_antseed/route` (`ANTSEED_ROUTE_PATH`) is reserved. A seller MUST +route a `POST` request for this path to its registered routing handler, and +MUST do so before provider matching. A routing handler MUST be implemented +as: ```typescript interface RoutingServerHandler { @@ -135,19 +134,13 @@ AIP also extends. Dispatch rules: -- Exactly one routing handler MAY be registered per node. A `POST` to either +- Exactly one routing handler MAY be registered per node. A `POST` to the reserved path when no handler is registered MUST return `404` with error code `not_routing_peer`. - Otherwise the seller MUST call `handleRoute(buyerPeerId, req)` and return its `SellerResponse` unmodified. A handler that throws or rejects MUST come back as a `500` response of type `routing_error` and MUST NOT crash request handling. -- Both paths dispatch to the same handler. A handler distinguishes a route - request from a digest submission by `req.path`, since the request bodies - defined below are structurally distinct (a route request always carries - `inputMessage`; a digest submission never does). An implementation MAY also - reject a request on the wrong path with `400` rather than relying on body - shape. - A routing peer that gates on payment (left to a companion AIP; see Payments below) MUST reject before calling into any ranking logic, with `402`, so an unpaid caller never triggers the expensive part of the handler. @@ -159,10 +152,7 @@ process — so, exactly as [AIP-3](./aip-3.md) requires for the attestation path, a seller MUST rate-limit `/_antseed/route` per buyer peer, with the tracking table capped so the limiter itself cannot become a memory-exhaustion vector. Cheap `400`/`402`/`404` rejections MUST NOT count against a buyer's -quota; only a call that reaches `handleRoute` does. `/_antseed/route/digest`, -being a small, fixed-shape, once-daily submission, is not subject to this -rate limit but MUST still be size-bounded by the seller's ordinary request-size -limits. +quota; only a call that reaches `handleRoute` does. ### Wire Schemas @@ -248,36 +238,6 @@ this response, for a buyer's own logging or support diagnostics; this AIP does not standardize its format beyond being a plain string, and a buyer MUST NOT parse it for routing decisions. -The `POST /_antseed/route/digest` request body: - -```typescript -interface DailyDigestBody { - v: 1; // wire schema version - period: string; // calendar day, YYYY-MM-DD, buyer's own local time zone - routedRequests: number; // count of requests routed this period - predictedCostUsd: number; // sum of estimate.costUsd across this period's routed requests - observedCostUsd: number; // sum of what the buyer actually paid this period - predictedInputTokens: number; // sum of predicted fresh input tokens this period - predictedCachedInputTokens: number; // sum of predicted cached input tokens this period - predictedOutputTokens: number; // sum of predicted output tokens this period - observedInputTokens: number; // sum of actual fresh input tokens this period - observedCachedInputTokens: number; // sum of actual cached input tokens this period - observedOutputTokens: number; // sum of actual output tokens this period - modelMix: Record; // request count per model actually used this period - failovers: number; // count of requests that failed over to a later-ranked candidate - timeouts: number; // count of requests that timed out waiting on a routing decision - avgRoutingLatencyMs: number | null; // mean wall-clock time to produce a routing decision this period; null if none were timed - cqtDistribution: Record; // request count per cqt value used this period -} -``` - -`period` MUST be a calendar day in `YYYY-MM-DD` form, in the buyer's own -local time zone. This is a fixed, closed schema: it MUST carry only -period-level aggregate counters and MUST NOT carry prompt content, -per-request rows, or any field that ties a routing decision to the message -that produced it. A digest submission has no response body requirement -beyond an HTTP status; a routing peer SHOULD return `204`. - ### Interface: `Router` Extensions The existing buyer-side `Router` interface gains five new, all-optional @@ -311,7 +271,7 @@ interface Router { ): Promise; // null is a decline, falling through to the unmodified fixed-model pipeline getRoutingDecisions?(): RoutingDecisionRow[]; // the router's own local routing_decisions ledger, if it keeps one, for a host's savings-dashboard UI - configureDailySigning?(signDailyIfNeeded: (sellerPeerId: string) => Promise): void; // hands the router a host-provided signing closure it can call to trigger its own daily payment; the router never touches the buyer's actual signer directly + configureDailySigning?(signDailyIfNeeded: (sellerPeerId: string) => Promise): void; // hands the router a host-provided signing closure it can call to trigger its own daily payment; the router never touches the buyer's actual signer directly, and is responsible for its own consent-gating (e.g. reading an opt-in from its own plugin configuration) before ever calling this triggerDailySigningCheck?(): Promise; // lets a host-owned background timer poke the same daily-signing gate outside of any request in flight, so a subscription doesn't lapse on a day with no chat traffic updateRoutingPreferences?(preferences: ModelRoutingPreferences): void; // pushed by the host whenever live preferences change, including once at startup, so an already-running router picks up changes without a request in flight } @@ -359,7 +319,6 @@ type ModelRoutingPreferences = { allowedPeerIds: string[]; // buyer's explicit allow-list blockedPeerIds: string[]; // buyer's explicit block-list cqt?: number; // cost/quality tradeoff dial, one of the five discrete values {1,3,5,7,9}; only meaningful to a router that implements selectRoute - autoSubscriptionEnabled?: boolean; // explicit opt-in to a subscription-priced router's daily signing; off by default, a router gating real signing on this MUST treat "unknown" the same as false, never as implicit consent }; ``` @@ -369,7 +328,14 @@ identity) lives in whatever plugin-configuration mechanism the host already uses for its other plugins, not in this type or on the wire. A `Router` implementation MUST be fully configurable through its own plugin configuration alone; it MUST NOT require `ModelRoutingPreferences` to carry -anything beyond the buyer-facing preference fields already listed above. +anything beyond the buyer-facing preference fields already listed above. This +excludes payment-model-specific consent, too: whether a buyer has opted into +a subscription-priced router's daily signing is not a preference every +router shares a use for (a metered or free router has nothing to check it +against), so it is not part of this type either — a subscription-priced +`Router` implementation reads that consent from its own plugin +configuration, the same place its other setup lives, never treating an +absent or unreadable value as consent. A host (buyer proxy) implementation MUST call `selectRoute` before its existing fixed-model peer-narrowing step, whenever the registered `Router` @@ -439,23 +405,23 @@ router's sentinel model string by name would privilege that implementation over any other `Router` a buyer might load. The host forwards the request unmodified; only the plugin knows what its own sentinel means. -**Pricing and the digest are both deliberately out of scope here.** This AIP -treats a routing peer as free, the same bootstrapping path attestation took — -bundling a pricing scheme into this proposal would force reviewers to accept -or reject both together. The daily digest reuses this AIP's own reserved-path -infrastructure rather than the protocol's payment-message enum, since it's -optional, non-billing statistics, not something that needs a new -protocol-wide surface. +**Pricing is deliberately out of scope here.** This AIP treats a routing +peer as free, the same bootstrapping path attestation took — bundling a +pricing scheme into this proposal would force reviewers to accept or reject +both together. A companion pricing AIP can specify metered, subscription, or +periodic-reporting mechanisms (such as a performance digest) a +subscription-priced routing peer might need, without reopening anything +specified here. ## Backwards Compatibility This proposal is additive. A peer that advertises no `routing.v1` capability is unaffected by this AIP; a `Router` implementation that does not implement any of the five new optional methods is unaffected and behaves exactly as it -does today. The `/_antseed/route` and `/_antseed/route/digest` paths are -newly reserved, so a seller MUST NOT route either to a provider — though -neither was ever a valid model or service id, so no existing conforming -seller can already be using them for anything else. No metadata codec +does today. The `/_antseed/route` path is newly reserved, so a seller MUST +NOT route it to a provider — though it was never a valid model or service +id, so no existing conforming seller can already be using it for anything +else. No metadata codec version bump, no `METADATA_VERSION` change, and no payment-message change is required. Mixed-version networks interoperate: a routing-aware buyer skips peers that advertise no `routing.v1` capability, and a routing-advertising @@ -488,12 +454,6 @@ own filtering (which is advisory, not authoritative — see Rationale), so a routing peer can bias an ordering but cannot force a buyer to pay outside its own price, trust, or allow/block bounds. -**Daily digests are aggregate, but a stable buyer id still makes them -linkable over time.** A sequence of `modelMix` values keyed to a real peer id -is a coarse usage profile. A routing peer operator SHOULD key stored digests -by a one-way function of the buyer's peer id rather than the raw id; this AIP -leaves the specific scheme to the operator. - **Plugins never hold a buyer's signing key.** `configureDailySigning`'s closure-based design means a `Router` — including a third-party one — never receives the buyer's actual private key or a handle that can sign arbitrary From ed382018eb3a5962f163685f64ed612756dc1b56 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 15:42:01 +0100 Subject: [PATCH 16/24] Drop the last pointer to digests --- AIPS/aip-5.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index 3b3409e..b69476a 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -408,10 +408,9 @@ unmodified; only the plugin knows what its own sentinel means. **Pricing is deliberately out of scope here.** This AIP treats a routing peer as free, the same bootstrapping path attestation took — bundling a pricing scheme into this proposal would force reviewers to accept or reject -both together. A companion pricing AIP can specify metered, subscription, or -periodic-reporting mechanisms (such as a performance digest) a -subscription-priced routing peer might need, without reopening anything -specified here. +both together. A companion pricing AIP can specify whatever metered or +subscription mechanism a priced routing peer needs, without reopening +anything specified here. ## Backwards Compatibility From fb1ecb3b04b9bf0d83dd9bd76afd9c5420ca25b9 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 15:50:05 +0100 Subject: [PATCH 17/24] State the missing payment gate as an operator MUST, not a forward reference to a companion AIP --- AIPS/aip-5.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index b69476a..f767e65 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -430,11 +430,12 @@ as [AIP-3](./aip-3.md) describes for verifier capabilities. ## Security Considerations **Unmetered ranking is a denial-of-service surface.** Ranking MAY be -expensive (a separate scoring process). This AIP defines no payment gate, so -until a companion pricing AIP exists, per-buyer rate limiting on -`/_antseed/route` is the only cost control a routing peer has — an -implementation MUST NOT skip it on the theory that payment will handle abuse -later. +expensive (a separate scoring process). This AIP defines no payment gate — +an operator MUST put its own payment or cost-control gate in front of a +routing peer before running it against real traffic. Per-buyer rate limiting +on `/_antseed/route` is REQUIRED regardless of that gate, and an +implementation MUST NOT skip it on the theory that payment handles abuse +instead. **Prompt content leaves the buyer's device.** `inputMessage` gives the routing peer itself access to conversation content on every routed request — From 95cdd7c67d6cbcb3e9f75241387096db76b1c034 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 15:58:47 +0100 Subject: [PATCH 18/24] Drop the per-buyer rate-limit requirement on /_antseed/route --- AIPS/aip-5.md | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index f767e65..ca5300c 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -24,8 +24,8 @@ failover walk exactly as it would for a fixed model choice. The proposal specifies three things: how a routing peer advertises itself and is discovered, without adding a new capability enum or a metadata codec version bump; the reserved request path a routing peer serves and its -dispatch and rate-limit rules, modeled directly on [AIP-3](./aip-3.md)'s -attestation route; and five new optional methods on the existing buyer-side +dispatch rules, modeled directly on [AIP-3](./aip-3.md)'s attestation route; +and five new optional methods on the existing buyer-side `Router` interface that let a plugin pick model and seller together, ahead of AntSeed's existing fixed-model peer-selection pipeline, while that pipeline stays completely unmodified for any buyer who does not opt in. @@ -67,8 +67,8 @@ exists on the network today, which shapes this proposal's discovery design: Separately, the reserved-path pattern this proposal needs — a seller-side handler claiming its own URL prefix, served before provider matching and before payment — already exists for attestation ([AIP-3](./aip-3.md)). That -AIP's capability-string advertisement, dispatch-and-rate-limit rules, and -plugin-registration shape transfer to routing almost unchanged; this proposal +AIP's capability-string advertisement, dispatch rules, and plugin-registration +shape transfer to routing almost unchanged; this proposal follows that precedent deliberately rather than inventing a parallel one. ## Specification @@ -145,15 +145,6 @@ Dispatch rules: below) MUST reject before calling into any ranking logic, with `402`, so an unpaid caller never triggers the expensive part of the handler. -### Routing Request Rate Limit - -Ranking a request is expensive — it MAY call out to a separate scoring -process — so, exactly as [AIP-3](./aip-3.md) requires for the attestation -path, a seller MUST rate-limit `/_antseed/route` per buyer peer, with the -tracking table capped so the limiter itself cannot become a memory-exhaustion -vector. Cheap `400`/`402`/`404` rejections MUST NOT count against a buyer's -quota; only a call that reaches `handleRoute` does. - ### Wire Schemas The `POST /_antseed/route` request body: @@ -381,8 +372,8 @@ or something else — is left entirely to that companion proposal. **Modeled on AIP-3's attestation design, not a new pattern.** [AIP-3](./aip-3.md) already solved this exact shape of problem: a seller-side handler on a reserved path, served before provider matching and payment, -rate-limited per buyer, advertised as a capability string. This AIP reuses -that shape rather than inventing a parallel one. Two specific choices follow +advertised as a capability string. This AIP reuses that shape rather than +inventing a parallel one. Two specific choices follow from it: `routing.v1` is a capability *string*, not a new `ProviderCapability` enum value, because the DHT machinery that enum would need is unwired end to end and the capability-string path already works in production; and a routing @@ -432,10 +423,7 @@ as [AIP-3](./aip-3.md) describes for verifier capabilities. **Unmetered ranking is a denial-of-service surface.** Ranking MAY be expensive (a separate scoring process). This AIP defines no payment gate — an operator MUST put its own payment or cost-control gate in front of a -routing peer before running it against real traffic. Per-buyer rate limiting -on `/_antseed/route` is REQUIRED regardless of that gate, and an -implementation MUST NOT skip it on the theory that payment handles abuse -instead. +routing peer before running it against real traffic. **Prompt content leaves the buyer's device.** `inputMessage` gives the routing peer itself access to conversation content on every routed request — From 936a8caca29b3b610b09a0d5981dbe698ffa81b0 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 16:10:04 +0100 Subject: [PATCH 19/24] Pull configureDailySigning/triggerDailySigningCheck out of Router Both were payment-shaped (daily signing cadence) despite this AIP's own "no payment mechanism" stance -- a companion pricing AIP defines its own Router extension for whatever signing its pricing model needs instead. The host-mediated-closure security rule (a Router never gets direct signer access) stays, generalized to apply to whatever signing mechanism a companion AIP adds, not tied to a method this AIP no longer defines. Router interface goes from five new optional methods to three (selectRoute, getRoutingDecisions, updateRoutingPreferences). --- AIPS/aip-5.md | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index ca5300c..68c9f91 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -25,7 +25,7 @@ The proposal specifies three things: how a routing peer advertises itself and is discovered, without adding a new capability enum or a metadata codec version bump; the reserved request path a routing peer serves and its dispatch rules, modeled directly on [AIP-3](./aip-3.md)'s attestation route; -and five new optional methods on the existing buyer-side +and three new optional methods on the existing buyer-side `Router` interface that let a plugin pick model and seller together, ahead of AntSeed's existing fixed-model peer-selection pipeline, while that pipeline stays completely unmodified for any buyer who does not opt in. @@ -231,7 +231,7 @@ MUST NOT parse it for routing decisions. ### Interface: `Router` Extensions -The existing buyer-side `Router` interface gains five new, all-optional +The existing buyer-side `Router` interface gains three new, all-optional methods. A `Router` that implements none of them is fully conforming and unaffected by this AIP; existing behavior for `selectPeer` and `onResult` is unchanged. @@ -262,8 +262,6 @@ interface Router { ): Promise; // null is a decline, falling through to the unmodified fixed-model pipeline getRoutingDecisions?(): RoutingDecisionRow[]; // the router's own local routing_decisions ledger, if it keeps one, for a host's savings-dashboard UI - configureDailySigning?(signDailyIfNeeded: (sellerPeerId: string) => Promise): void; // hands the router a host-provided signing closure it can call to trigger its own daily payment; the router never touches the buyer's actual signer directly, and is responsible for its own consent-gating (e.g. reading an opt-in from its own plugin configuration) before ever calling this - triggerDailySigningCheck?(): Promise; // lets a host-owned background timer poke the same daily-signing gate outside of any request in flight, so a subscription doesn't lapse on a day with no chat traffic updateRoutingPreferences?(preferences: ModelRoutingPreferences): void; // pushed by the host whenever live preferences change, including once at startup, so an already-running router picks up changes without a request in flight } @@ -350,11 +348,12 @@ that implements `updateRoutingPreferences` whenever those preferences change, including once at startup. A `Router` MUST NOT be given direct access to the buyer's payment-signing -key. A `Router` that needs to trigger its own periodic signing (for example, -a subscription-priced routing peer under a companion pricing AIP) MUST do so -only through a host-provided signing closure passed via -`configureDailySigning`, never by holding a reference to the buyer's signer -or payment manager directly. +key or payment manager. This AIP defines no signing mechanism of its own; a +`Router` that needs to trigger its own periodic signing (for example, a +subscription-priced routing peer) gets that capability from whatever +companion pricing AIP defines it, and that mechanism MUST itself be a +host-provided closure the router calls, never a direct reference to the +buyer's signer. ### Payments @@ -407,7 +406,7 @@ anything specified here. This proposal is additive. A peer that advertises no `routing.v1` capability is unaffected by this AIP; a `Router` implementation that does not implement -any of the five new optional methods is unaffected and behaves exactly as it +any of the three new optional methods is unaffected and behaves exactly as it does today. The `/_antseed/route` path is newly reserved, so a seller MUST NOT route it to a provider — though it was never a valid model or service id, so no existing conforming seller can already be using it for anything @@ -442,11 +441,14 @@ own filtering (which is advisory, not authoritative — see Rationale), so a routing peer can bias an ordering but cannot force a buyer to pay outside its own price, trust, or allow/block bounds. -**Plugins never hold a buyer's signing key.** `configureDailySigning`'s -closure-based design means a `Router` — including a third-party one — never -receives the buyer's actual private key or a handle that can sign arbitrary -messages. A host implementation MUST NOT expose its payment manager or -signer directly to plugin code under any of this AIP's new methods. +**Plugins never hold a buyer's signing key.** None of this AIP's own new +`Router` methods expose signer access, since this AIP defines no signing +mechanism. Any signing capability a companion pricing AIP adds MUST follow +the same closure-based pattern established elsewhere in this protocol: a +`Router` — including a third-party one — gets a host-provided closure to +call, never the buyer's actual private key or a handle that can sign +arbitrary messages. A host implementation MUST NOT expose its payment +manager or signer directly to plugin code. ## Copyright From 73f78d747266ddd16d9d75452ce68fbfd4a3b3b7 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 16:18:05 +0100 Subject: [PATCH 20/24] Pull updateRoutingPreferences -- its only real use case is already gone selectRoute already receives fresh routingPreferences on every call, so any router that only reads preferences inside selectRoute never needed a push. The one thing that did need preferences outside of a live request -- triggerDailySigningCheck's background timer -- was pulled in the previous commit. Nothing left in this AIP's two-method Router surface (selectRoute, getRoutingDecisions) has a background/non-request code path that would ever consult it. --- AIPS/aip-5.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index 68c9f91..7a35ed6 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -25,7 +25,7 @@ The proposal specifies three things: how a routing peer advertises itself and is discovered, without adding a new capability enum or a metadata codec version bump; the reserved request path a routing peer serves and its dispatch rules, modeled directly on [AIP-3](./aip-3.md)'s attestation route; -and three new optional methods on the existing buyer-side +and two new optional methods on the existing buyer-side `Router` interface that let a plugin pick model and seller together, ahead of AntSeed's existing fixed-model peer-selection pipeline, while that pipeline stays completely unmodified for any buyer who does not opt in. @@ -231,7 +231,7 @@ MUST NOT parse it for routing decisions. ### Interface: `Router` Extensions -The existing buyer-side `Router` interface gains three new, all-optional +The existing buyer-side `Router` interface gains two new, all-optional methods. A `Router` that implements none of them is fully conforming and unaffected by this AIP; existing behavior for `selectPeer` and `onResult` is unchanged. @@ -262,7 +262,6 @@ interface Router { ): Promise; // null is a decline, falling through to the unmodified fixed-model pipeline getRoutingDecisions?(): RoutingDecisionRow[]; // the router's own local routing_decisions ledger, if it keeps one, for a host's savings-dashboard UI - updateRoutingPreferences?(preferences: ModelRoutingPreferences): void; // pushed by the host whenever live preferences change, including once at startup, so an already-running router picks up changes without a request in flight } type RouteCandidate = { @@ -342,10 +341,8 @@ When `selectRoute` returns a non-null list, the host MUST walk it in the returned order using its existing per-candidate failover mechanism (fail over before the first token of a response, terminal after), MUST NOT re-sort it locally, and MUST call `onResult` for the peer actually used exactly as it -does for a fixed-model selection. A host MUST forward `routingPreferences` to -`selectRoute` on every call and MUST push preference updates to a `Router` -that implements `updateRoutingPreferences` whenever those preferences change, -including once at startup. +does for a fixed-model selection. A host MUST forward the buyer's current +`routingPreferences` to `selectRoute` on every call. A `Router` MUST NOT be given direct access to the buyer's payment-signing key or payment manager. This AIP defines no signing mechanism of its own; a @@ -406,7 +403,7 @@ anything specified here. This proposal is additive. A peer that advertises no `routing.v1` capability is unaffected by this AIP; a `Router` implementation that does not implement -any of the three new optional methods is unaffected and behaves exactly as it +either of the two new optional methods is unaffected and behaves exactly as it does today. The `/_antseed/route` path is newly reserved, so a seller MUST NOT route it to a provider — though it was never a valid model or service id, so no existing conforming seller can already be using it for anything From cb7759450f26b238f5f24be3fcc9a0be5e9f19c5 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 16:45:31 +0100 Subject: [PATCH 21/24] Drop the 'internal ranking signal is not on the wire' aside --- AIPS/aip-5.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index 7a35ed6..318559d 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -218,16 +218,10 @@ routing peer needing to remember which candidates it offered to which buyer. `estimate` carries the routing peer's own per-candidate prediction, in the same units a buyer already reconciles against observed usage elsewhere in this protocol; it MUST be populated for every ranked entry, same as `price`. -Whatever internal signal the routing peer used to produce the ranking itself -(a numeric score, a raw quality prediction, or anything else its own ranking -scheme computes) is that routing peer's own implementation detail and is NOT -part of this wire response — a buyer has no use for a number it cannot -recompute or independently verify, only for the order it produces and the -concrete price/estimate figures it can check usage against. `router` is a -single, routing-peer-chosen string identifying which router/version served -this response, for a buyer's own logging or support diagnostics; this AIP -does not standardize its format beyond being a plain string, and a buyer -MUST NOT parse it for routing decisions. +`router` is a single, routing-peer-chosen string identifying which +router/version served this response, for a buyer's own logging or support +diagnostics; this AIP does not standardize its format beyond being a plain +string, and a buyer MUST NOT parse it for routing decisions. ### Interface: `Router` Extensions From 42613fd5ea14c8687e0127fe0869edbd7941a419 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Fri, 28 Aug 2026 16:59:30 +0100 Subject: [PATCH 22/24] Resolve who re-applies constraints: the plugin, not the host Two of the three mentions read as if the host's own dispatch walk was responsible for policy filtering (skipping disallowed entries "during the walk"), contradicting the third (Security Considerations), which already correctly placed this on the Router implementation inside selectRoute. All three now consistently say the plugin drops disallowed candidates itself before ever returning the list, so by the time the host walks it, it's already policy-compliant -- the host's walk is then a pure failover mechanism, not a filtering one. --- AIPS/aip-5.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index 318559d..c94761f 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -206,12 +206,15 @@ interface RouteResponseBody { ``` `ranked` MUST be ordered by the routing peer's own objective, best candidate -first. A buyer MUST treat this order as the routing decision itself and MUST +first. A host MUST treat this order as the routing decision itself and MUST NOT locally re-sort it by an independent signal such as reputation before walking it, since doing so would discard the quality/cost tradeoff the -ranking encodes; a buyer's own policy filtering (allow/block lists, price and -trust ceilings) applies during the walk, skipping disallowed entries in -place, not by re-ordering the remainder. `price` MUST be populated for every +ranking encodes. The `Router` implementation returning this list to the host +(inside `selectRoute`) is responsible for re-applying the buyer's own policy +(allow/block lists, price and trust ceilings) before returning it, dropping +disallowed entries rather than reordering around them — by the time the host +receives and walks the list, it MUST already reflect that policy. `price` +MUST be populated for every ranked entry, not only the winner, so a buyer can compute a savings figure against any candidate without a separate price lookup and without the routing peer needing to remember which candidates it offered to which buyer. @@ -373,13 +376,14 @@ for this role, not a corner case to work around. **The routing peer's ranked order is the decision, not a suggestion.** The order encodes a quality/cost tradeoff a buyer can't independently recompute -from price and reputation alone, so a buyer MUST NOT locally re-sort it — +from price and reputation alone, so a host MUST NOT locally re-sort it — that would discard the tradeoff the ranking exists to provide. But the -routing peer's own `constraints` filtering is advisory only: a buyer MUST -still re-apply its own price/trust/allow-block policy locally before -dispatch, since a candidate can go stale (peer drops offline, cools down) -between the routing call and dispatch. Policy filters the given order in -place; it never re-ranks it. +routing peer's own `constraints` filtering is advisory only, since a host +has no way to verify a remote routing peer actually applied it correctly: +a `Router` implementation MUST still re-apply the buyer's own +price/trust/allow-block policy itself, inside `selectRoute`, dropping +disallowed candidates before ever returning the list to the host. Policy +filters the given order in place; it never re-ranks it. **Sentinel strings stay inside the plugin.** A host that recognized one router's sentinel model string by name would privilege that implementation From 5d57b3de6cd03fcbe4e4f42c34589c0b13839b61 Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Sat, 29 Aug 2026 11:33:56 +0100 Subject: [PATCH 23/24] Reframe the routing peer's ranked order as a suggestion, not a binding decision Downgrades the host-side MUST/MUST NOT around following the routing peer's ranking to SHOULD/SHOULD NOT: a decentralized buyer retains full discretion to disregard a routing peer's suggestion, consult a different one, or blend rankings, rather than being bound to follow whichever routing peer answered. The routing peer's own obligation to produce a well-ordered response (ranked MUST be ordered...) is unaffected -- this only softens what the CLIENT is compelled to do with that response. Constraint re-application (a Router MUST enforce the buyer's own price/trust/allow-block limits regardless of what a routing peer returns) is untouched -- that protects buyer autonomy, it doesn't constrain it, so it stays a MUST throughout. Also: "contract" -> "smart contract" in both Payments-section mentions, for clarity. NOT PUSHED YET -- holding for review per explicit request. --- AIPS/aip-5.md | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index c94761f..71eef1e 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -30,7 +30,7 @@ and two new optional methods on the existing buyer-side AntSeed's existing fixed-model peer-selection pipeline, while that pipeline stays completely unmodified for any buyer who does not opt in. -This AIP makes no contract change and no payment-message change. How a +This AIP makes no smart contract change and no payment-message change. How a routing peer charges for its service is left to a companion AIP; this one treats routing peers as offering their service for free, exactly like attestation. @@ -206,10 +206,13 @@ interface RouteResponseBody { ``` `ranked` MUST be ordered by the routing peer's own objective, best candidate -first. A host MUST treat this order as the routing decision itself and MUST +first. A host SHOULD treat this order as its routing decision and SHOULD NOT locally re-sort it by an independent signal such as reputation before walking it, since doing so would discard the quality/cost tradeoff the -ranking encodes. The `Router` implementation returning this list to the host +ranking encodes — but this is a suggestion, not a binding instruction: a +buyer retains full discretion to disregard it, consult a different routing +peer it trusts more, or impose its own ranking on top of the returned order. +The `Router` implementation returning this list to the host (inside `selectRoute`) is responsible for re-applying the buyer's own policy (allow/block lists, price and trust ceilings) before returning it, dropping disallowed entries rather than reordering around them — by the time the host @@ -334,12 +337,15 @@ special-case, or validate against any specific sentinel string — that recognition is entirely the plugin's business, so that different `Router` implementations MAY choose different sentinels without any host change. -When `selectRoute` returns a non-null list, the host MUST walk it in the +A host that acts on a non-null `selectRoute` result SHOULD walk it in the returned order using its existing per-candidate failover mechanism (fail over -before the first token of a response, terminal after), MUST NOT re-sort it -locally, and MUST call `onResult` for the peer actually used exactly as it -does for a fixed-model selection. A host MUST forward the buyer's current -`routingPreferences` to `selectRoute` on every call. +before the first token of a response, terminal after) and SHOULD NOT re-sort +it locally, for the same reason given in Rationale — a host MAY instead +disregard the result, consult a different routing peer, or impose its own +ranking. Whichever path a host takes, it MUST call `onResult` for the peer +actually used exactly as it does for a fixed-model selection, and MUST +forward the buyer's current `routingPreferences` to `selectRoute` on every +call. A `Router` MUST NOT be given direct access to the buyer's payment-signing key or payment manager. This AIP defines no signing mechanism of its own; a @@ -351,7 +357,7 @@ buyer's signer. ### Payments -This AIP specifies no contract change and no payment-message change. It does +This AIP specifies no smart contract change and no payment-message change. It does not add, remove, or modify any `MessageType` value, any EIP-712 struct, or any Solidity contract. A routing peer served under this AIP is unpriced, exactly like attestation under [AIP-3](./aip-3.md); the `402` gate described @@ -374,10 +380,14 @@ peer's announcer MUST start even with zero registered providers, since a peer whose entire purpose is ranking rather than serving is the normal case for this role, not a corner case to work around. -**The routing peer's ranked order is the decision, not a suggestion.** The -order encodes a quality/cost tradeoff a buyer can't independently recompute -from price and reputation alone, so a host MUST NOT locally re-sort it — -that would discard the tradeoff the ranking exists to provide. But the +**The routing peer's ranked order is a suggestion, not a binding decision.** +The order encodes a quality/cost tradeoff a buyer can't independently +recompute from price and reputation alone, so a host that follows it SHOULD +NOT locally re-sort it — that would discard the tradeoff the ranking exists +to provide. But nothing in this AIP compels a buyer to follow it: on a +decentralized network a buyer MAY disregard the suggestion, ask a different +routing peer it trusts more, or blend several routing peers' rankings — the +routing peer proposes, the buyer's own client decides. Separately, the routing peer's own `constraints` filtering is advisory only, since a host has no way to verify a remote routing peer actually applied it correctly: a `Router` implementation MUST still re-apply the buyer's own @@ -427,10 +437,11 @@ one. A buyer SHOULD trust a routing peer the way it trusts any seller it sends prompts to, and SHOULD be able to see which peer identity it's routing through. -**A dishonest routing peer can steer, but not override, a buyer's policy.** -Because the returned order is treated as the decision, a compromised routing -peer could bias its ranking toward affiliated sellers. This is bounded, not -eliminated: a `Router` implementation MUST re-apply the buyer's own +**A dishonest routing peer can steer, but never override, a buyer's policy.** +Even though following a routing peer's ranking is the host's own choice, not +a requirement (Rationale), a compromised routing peer could still bias its +ranking toward affiliated sellers, hoping a host follows it uncritically. +This is bounded regardless: a `Router` implementation MUST re-apply the buyer's own `constraints` locally before dispatch rather than trusting the routing peer's own filtering (which is advisory, not authoritative — see Rationale), so a routing peer can bias an ordering but cannot force a buyer to pay outside its From e2b0cbb16e1e894ce3a45ea5583c15a9a020d5fe Mon Sep 17 00:00:00 2001 From: Dawe000 Date: Sat, 29 Aug 2026 11:57:56 +0100 Subject: [PATCH 24/24] Pare the SHOULD reframing down to plain verb swaps, no added editorializing Previous commit over-explained the autonomy point with new sentences ("a buyer retains full discretion...", "the routing peer proposes, the buyer decides", etc.). Reworked to just weaken MUST/MUST NOT to SHOULD/SHOULD NOT in place, keeping every sentence's original structure otherwise -- the Rationale point's title still flips (it directly restates the reviewer's own wording), but the body is back to near-original phrasing. --- AIPS/aip-5.md | 50 ++++++++++++++++++++------------------------------ 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/AIPS/aip-5.md b/AIPS/aip-5.md index 71eef1e..06a1280 100644 --- a/AIPS/aip-5.md +++ b/AIPS/aip-5.md @@ -209,10 +209,7 @@ interface RouteResponseBody { first. A host SHOULD treat this order as its routing decision and SHOULD NOT locally re-sort it by an independent signal such as reputation before walking it, since doing so would discard the quality/cost tradeoff the -ranking encodes — but this is a suggestion, not a binding instruction: a -buyer retains full discretion to disregard it, consult a different routing -peer it trusts more, or impose its own ranking on top of the returned order. -The `Router` implementation returning this list to the host +ranking encodes. The `Router` implementation returning this list to the host (inside `selectRoute`) is responsible for re-applying the buyer's own policy (allow/block lists, price and trust ceilings) before returning it, dropping disallowed entries rather than reordering around them — by the time the host @@ -337,15 +334,12 @@ special-case, or validate against any specific sentinel string — that recognition is entirely the plugin's business, so that different `Router` implementations MAY choose different sentinels without any host change. -A host that acts on a non-null `selectRoute` result SHOULD walk it in the +When `selectRoute` returns a non-null list, the host SHOULD walk it in the returned order using its existing per-candidate failover mechanism (fail over -before the first token of a response, terminal after) and SHOULD NOT re-sort -it locally, for the same reason given in Rationale — a host MAY instead -disregard the result, consult a different routing peer, or impose its own -ranking. Whichever path a host takes, it MUST call `onResult` for the peer -actually used exactly as it does for a fixed-model selection, and MUST -forward the buyer's current `routingPreferences` to `selectRoute` on every -call. +before the first token of a response, terminal after), SHOULD NOT re-sort it +locally, and MUST call `onResult` for the peer actually used exactly as it +does for a fixed-model selection. A host MUST forward the buyer's current +`routingPreferences` to `selectRoute` on every call. A `Router` MUST NOT be given direct access to the buyer's payment-signing key or payment manager. This AIP defines no signing mechanism of its own; a @@ -382,15 +376,11 @@ for this role, not a corner case to work around. **The routing peer's ranked order is a suggestion, not a binding decision.** The order encodes a quality/cost tradeoff a buyer can't independently -recompute from price and reputation alone, so a host that follows it SHOULD -NOT locally re-sort it — that would discard the tradeoff the ranking exists -to provide. But nothing in this AIP compels a buyer to follow it: on a -decentralized network a buyer MAY disregard the suggestion, ask a different -routing peer it trusts more, or blend several routing peers' rankings — the -routing peer proposes, the buyer's own client decides. Separately, the -routing peer's own `constraints` filtering is advisory only, since a host -has no way to verify a remote routing peer actually applied it correctly: -a `Router` implementation MUST still re-apply the buyer's own +recompute from price and reputation alone, so a host SHOULD NOT locally +re-sort it — that would discard the tradeoff the ranking exists to provide. +But the routing peer's own `constraints` filtering is advisory only, since a +host has no way to verify a remote routing peer actually applied it +correctly: a `Router` implementation MUST still re-apply the buyer's own price/trust/allow-block policy itself, inside `selectRoute`, dropping disallowed candidates before ever returning the list to the host. Policy filters the given order in place; it never re-ranks it. @@ -437,15 +427,15 @@ one. A buyer SHOULD trust a routing peer the way it trusts any seller it sends prompts to, and SHOULD be able to see which peer identity it's routing through. -**A dishonest routing peer can steer, but never override, a buyer's policy.** -Even though following a routing peer's ranking is the host's own choice, not -a requirement (Rationale), a compromised routing peer could still bias its -ranking toward affiliated sellers, hoping a host follows it uncritically. -This is bounded regardless: a `Router` implementation MUST re-apply the buyer's own -`constraints` locally before dispatch rather than trusting the routing peer's -own filtering (which is advisory, not authoritative — see Rationale), so a -routing peer can bias an ordering but cannot force a buyer to pay outside its -own price, trust, or allow/block bounds. +**A dishonest routing peer can steer, but not override, a buyer's policy.** +Because a host that follows it treats the returned order as its routing +decision, a compromised routing peer could bias its ranking toward +affiliated sellers. This is bounded, not eliminated: a `Router` +implementation MUST re-apply the buyer's own `constraints` locally before +dispatch rather than trusting the routing peer's own filtering (which is +advisory, not authoritative — see Rationale), so a routing peer can bias an +ordering but cannot force a buyer to pay outside its own price, trust, or +allow/block bounds. **Plugins never hold a buyer's signing key.** None of this AIP's own new `Router` methods expose signer access, since this AIP defines no signing