← Back to Home

Agent Federation Protocol (AFP) Specification

Verifiable, metadata-private federation between tenants

Version 0.3.12-draft | Status: Draft | License: Apache 2.0 | Domain: AFP.dev

Date: 2026-09-12

Abstract

The Agent Federation Protocol (AFP) is the protocol layer for verifiable interaction between tenants. It defines how two tenants coordinate work across organizational boundaries, how an instance proves operational health to an operator without disclosing its work, and how an instance's identity survives a change of deployment topology or operator.

AFP is a protocol, not a profile in the sense of §13.1: it is a wire contract between independent, mutually-distrusting implementations, and an AFP endpoint is any conforming agent-platform instance — not an instance of any one vendor's platform. ADAMAS is AFP's originating implementation and is cited throughout as a concrete example; nothing in this specification requires either endpoint to be an ADAMAS instance.

AFP deliberately excludes third-party agent registration, capability-discovery feeds, marketplace task delegation, observation contribution, portable reputation, and protocol-level trust ladders. Section 1.2 states these current boundaries and the narrow constructs that AFP does permit.

AFP rests on KERI-aligned self-certifying identity, ZKA and ZKC as substrate (with ZKM as an optional authorization peer), and a structural operator-blindness guarantee: the operator of any AFP transport is cryptographically and topologically incapable of observing tenant work content or reconstructing the graph of which tenants coordinate with which.

This specification targets ZKA v0.9.2-draft, ZKC v0.4.1-draft, and AFP-KDC v1.0.3. ZKA carries ZKA/Coord work-attestation and profile-aware ZKC proofs inside zka:bundle:v1. ZKC v0.4.1-draft supports anonymous, scoped, and explicitly-consented linkable presentation profiles; AFP selects among them per flow and binds every accepted proof to the KERI-authenticated presenter, credential-subject AID, APR signer, AFP session context, and authoritative AFP trust-flow purpose (§5.9). AFP-KDC v1.0.3 defines the external fixed-byte ownership boundary while reusing the v1.0.2 Poseidon2 parameter and conformance artifacts; it changes no KDC-owned tag, derivation, value, vector, hash, proof, circuit, verification key, or wire record (Appendix A). AFP inherits the six Freedom Safeguards defined normatively in ZKA §1.6, including Safeguard 6 (Verifier Accountability), which is load-bearing for unknown-counterparty federation.

The machine-checked composition boundary is epoch zka:protocol-family:2026-09-10.1 in ZKA's canonical protocol-family/ package. It pins ZKM 0.1.0-draft (rev 12) and statement-set identity sha256:e4604c92377ab121ab8834c1243935b77c32969d129564574f54e72ae55ecab7 with its Nargo 1.0.0-beta.25 and Barretenberg 5.0.0 toolchain; AFP's wire/profile surface is unchanged. AFP vendors a commit-locked snapshot and rejects unknown, unmatched, or ambiguous version/profile combinations.

Public release changes are recorded in the root CHANGELOG.md of each public snapshot.


1. Overview

1.1 Design Goals

  1. Tenant Federation: Two tenants coordinate work across organizational boundaries with neither side revealing more than the work requires.
  2. Operator Blindness: Whoever operates an AFP transport can verify the platform is healthy without learning what tenants do, who they coordinate with, or what moves between them. This is structural, not policy.
  3. Identity Continuity: A tenant's identity, reputation relationships, and attestations survive a change of deployment topology or operator.
  4. Substrate Reuse: AFP defines how ZK-mediated interaction is initiated, authenticated, sequenced, and recorded. It does not reinvent the cryptography of ZKA, ZKC, or ZKM.
  5. Freedom by Default: The six Freedom Safeguards (ZKA v0.9.2-draft §1.6) hold across every AFP interaction.
  6. Minimality: AFP specifies only the protocol surface needed for the three relationships of §1.3. Anything expressible as tenant-internal behavior is not in the protocol.

1.2 What AFP Is Not

AFP does not define the following protocol surface. Conforming implementations MUST NOT add it under an AFP message, profile, registry, or transport extension.

Excluded concern Current AFP rule
Agent manifest schema for third-party agents Entity manifests remain inside the tenant; no record of that shape crosses the protocol boundary.
Capability discovery or a marketplace feed Exposure to a counterparty is negotiated bilaterally under §5 and is not advertised as a feed.
Task delegation as a central wire protocol Intra-tenant routing is tenant-internal; cross-tenant work requests use the specific §5 message class.
Observation contribution from external agents Observations originate only from tenant-internal entities.
Individual-agent reputation accumulation Trust is bilateral under §5.5 or per-interaction under §5.8; AFP carries no portable positive score.
Trust-level escalation ladder AFP has one trust level: verified conforming instance under a verifier-accepted attestation authority (§6.3.1). A verifier either accepts an authority or does not; neither root class nor authority creates a rank. Whether to require instance attestation is the verifier's own policy (§1.5.1, §6.3.1).
HTTP bridge Cross-instance traffic is between tenants on the common transport; AFP defines no HTTP-only third-party path.
Operator-observed per-tenant telemetry Per-tenant operator knowledge is proved under §6 and MUST NOT be learned by observing transport traffic (§3, §7).

Related behavior MAY exist as tenant-internal implementation, never as AFP wire protocol. In particular, a conforming implementation MUST NOT:

Except for the constructs §14 expressly permits, surface with any of these effects is outside AFP regardless of its name.

1.3 The Three Relationships

AFP defines exactly three relationships, all between identifiable, cryptographically verifiable principals:

  1. Tenant ↔︎ tenant — two tenants, each running their own entities, collaborating across organizational lines. The protocol makes the interaction safe, verifiable, and minimally disclosing. "Federation" means two instances talking, never a third-party agent integrated into an instance. The two instances may be known to each other — joined by a pre-existing bilateral federation agreement (§5.5) — or mutually unknown, establishing trust per-interaction by ZKC compliance attestation (open federation, §5.8). Both are tenant↔︎tenant federation; they differ only in how trust is established, never in who the principals are.
  2. Tenant ↔︎ operator — the operator of a transport needs to know the platform is healthy without learning what tenants do. The protocol enforces that asymmetry structurally.
  3. Identity continuity — a tenant's identity and accumulated relationships survive a move between deployment topologies (managed transport ↔︎ self-hosted appliance) and a change of operator.
Protocol Relationship
ZKA v0.9.2-draft Settlement and coordination substrate. Tenant ↔︎ tenant work artifacts, attestations, and value transfers are ZKA notes and ZKA/Coord proofs. AFP defines the interaction envelope around them. ZKA normatively references the Key Derivation Core (Appendix A) for its key hierarchy and carries ZKA/Coord work-attestation proofs inside zka:bundle:v1 (§12.7). The AFP↔︎ZKA dependency remains acyclic: ZKA depends only on Appendix A, which depends on nothing else in AFP (§2.2).
ZKM v0.1.0-draft Optional principal-issued spend-authorization layer. An AFP tenant's agent MAY exercise a ZKM mandate inside an AFP session, but AFP owns bilateral session policy while ZKM owns unilateral mandate state. Both constraints must pass independently; neither mutates or replaces the other. ZKM references AFP-KDC v1.0.3 for its seed/key-derivation model, retains the v1.0.2 cryptographic profile unchanged, and owns its additional zkm/* tags.
ZKC v0.4.1-draft Compliance substrate. Tenant ↔︎ operator telemetry and tenant ↔︎ tenant compliance claims are profile-aware ZKC proofs. AFP defines which claims are proved, their cadence, identity/session binding, accepted presentation profiles, downgrade rules, and failure semantics (§5.9). ZKC references AFP-KDC v1.0.3 for credential binding and owns the v1/v2 proof, rebind, APR, scope, pseudonym, and receipt cryptography.
KERI Identity substrate. AFP identities are KERI Autonomic Identifiers (AIDs) with Key Event Logs (KELs). See §4. KERI machinery sits above the Key Derivation Core: KDC produces seeds and keys; AFP §4 wraps a KERI AID around them.
DGP Out of AFP scope except for the boundary rule in §1.4.1. A tenant may expose a DGP gateway to a third-party tool, but that gateway is not an AFP endpoint, does not receive an AFP discovery feed, and does not expand the AFP trust boundary.
PRP v0.2 · VXP v0.2 AFP profiles, not peer protocols. PRP (pairwise reconciliation) and VXP (value exchange) are named behaviors over AFP's Bilateral Session Core (§12); each adds its own atom kinds and state machine but reuses the core's session, identity binding, NATS namespace, dispute/arbiter model, commitment + optional-anchor primitive, session-envelope bundle, and Freedom-Safeguards application. See §13.

1.4.1 DGP Gateway Boundary

A tenant may expose a DGP gateway to a third-party tool, internal integration, or adjacent tenant-internal workflow. That gateway is a DGP surface controlled by the tenant; it is not an AFP endpoint and does not become part of AFP federation.

The boundary is:

This rule keeps DGP gateways useful at tenant integration edges without creating AFP discovery, observation, or trust-score surfaces.

1.5 Conformance Terminology

The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, MAY, and OPTIONAL are to be interpreted as in RFC 2119 / RFC 8174.

A conforming AFP 0.3.12-draft implementation satisfies all normative requirements of §3–§8 and all six Freedom Safeguards (ZKA v0.9.2-draft §1.6). Sections or subsections marked Draft state a normative requirement whose concrete mechanism is not yet finalized; a conforming implementation MUST satisfy the requirement and MAY choose any mechanism that does, pending a future revision that fixes the mechanism.

1.5.1 The tenant conformance profile

A conforming AFP tenant satisfies the tenant conformance profile stated below. The two conformance subjects are distinct: a conforming implementation is a codebase that satisfies the normative requirements of §3–§8 and the six Freedom Safeguards (§1.5); a conforming tenant is a deployed runtime, addressed by its tenant root AID (§1.6, §2.1), that federates as a principal (§2.1) and additionally carries the tenant-side obligations enumerated here. The profile groups the applicable requirements from §3–§12 into one list and introduces no separate wire surface. ADAMAS, published by Pyramidal Inc., is the reference implementation of this profile; that attribution is a fact about one implementation, not a conformance gate, and publishing an implementation is unrelated to standing as an attestation authority (§6.3.1).

A conforming tenant MUST satisfy all of the following:

  1. KERI identity custody (§4). The tenant MUST be identified by a KERI tenant root AID governed by a KEL, with each entity a delegated AID whose inception is anchored in that KEL (§4.2). Pre-rotation is REQUIRED on every KEL event, and the tenant MUST reject a KEL whose rotation events do not honor prior pre-rotation commitments (§4.4). The tenant MUST publish its witness set and threshold as part of its KEL, MUST check that a counterparty's KEL events carry receipts from at least that counterparty's declared witness threshold, and MUST carry its witness configuration across a topology change (§4.5, §4.5.1, §4.5.2). Rotation, recovery, and succession run as KEL events, with guardian designation itself a published KEL event (§4.6); the tenant MUST NOT treat a KEL rotation alone as sufficient while notes or credentials derived from the old seed remain in use, and MUST publish the §4.6.1 rotation presentation when a rotation affects ZKA or ZKC material, under the sequencing and verifier rules of §4.6.2–§4.6.3.
  2. Operator blindness and deployment topology (§3, §7.1, §7.3). The tenant MUST deliver both OB-1 (content) and OB-2 (coordination graph); OB-1 alone does not satisfy the invariant (§3.1). OB is a capability standard, not a conduct standard: the operator must be unable to obtain the protected information (§3.2). The tenant MUST NOT route tenant↔︎tenant coordination traffic through a transport whose operator can correlate the endpoints (§3.3), and MUST NOT deploy in the topology §7.3 excludes — one in which an operator runs the transport that tenant↔︎tenant coordination traffic transits. Its OB-relevant behavior MUST be auditable (§7.1). Where OB-2 is not structurally satisfied, the tenant MUST NOT represent it as satisfied and MUST disclose the limitation (§3.5).
  3. Routing participation, including cover traffic (§8). The tenant MUST route coordination traffic through a layer that satisfies the §8.2 conformance properties, which are the conformance test (§8.1, §8.2). Constant-rate emission is REQUIRED, not optional: the tenant MUST emit coordination-plane traffic into its routing layer at a constant, scheduled rate at all times while federated, whether or not it has real coordination traffic to send, with real afp.* messages (§5.3) slotted into that stream (§8.5). Where the deployment uses the AFP Federation Mixnet, §8.4–§8.5 fix the packet form, the indistinguishability of cover from real packets, and a schedule derived from the active directory epoch and the tenant's local scheduler rather than from the presence of application work; missed cover slots are an OB-2 health failure and MUST be disclosed in operator telemetry without identifying counterparties (§8.5). An implementation MAY satisfy §8.2 by another construction, but it MUST satisfy §8.2, including an equivalent constant-emission property (§8). A tenant that does not emit at a fixed rate is not OB-2-conforming and MUST disclose this per §3.5.
  4. The bilateral session core (§12). Every AFP-mediated interaction the tenant takes part in MUST run inside a bilateral session (§12.1): exactly two principals, mutually authenticated against the declared witness threshold, end-to-end encrypted, metadata-private under §8, anchored to one of the three trust bases of §12.1, and context-bound. The tenant MUST carry the core's establishment, lifecycle, and timeout rules (§12.2), identity binding (§12.3), namespace ownership (§12.4), dispute and arbiter model (§12.5), commitment and optional-anchor primitive (§12.6), and session-envelope wire format (§12.7). Any profile the tenant runs MUST run over that core and MUST NOT re-derive or weaken it (§13.4).
  5. Operator telemetry (§6). Toward any operator to which the tenant stands in the §6 tenant↔︎operator relationship, the tenant MUST supply operational assurance only as claims it proves — never by permitting observation of tenant work or the coordination graph (§6.1) — pushed by the tenant on a telemetry subject at the tenant's own timing (§6.5). Every such proof MUST use a registered predicate identifier and supported version from the §6.3 catalog at that predicate's stated cadence; a proof outside the catalog is not AFP-conforming. Every operator-facing catalog proof MUST bind the tenant AID, operator AID, predicate ID, predicate version, telemetry subject, telemetry window, and active ZKC presentation profile into the proof transcript (§6.3), and every self-attested predicate MUST carry the public-sample binding of §6.4. This surface is identical in every deployment topology (§6.7).
  6. The six Freedom Safeguards (§9). The tenant MUST satisfy all six Freedom Safeguards (ZKA v0.9.2-draft §1.6) as §9 applies them to AFP — predicate pluralism, minimal disclosure, no revocability, open-source predicates, agent exit rights, and verifier accountability — and as §12.8 applies them to every bilateral session. A profile the tenant runs inherits the §12.8 application and MUST NOT weaken any safeguard (§12.8, §13.4).
  7. Tenant-internal obligations (§5.6, §5.7). The tenant MUST contain, capability-scope, and observe its own entities; the mechanism is tenant-internal and outside AFP scope (§5.7). Routing an inbound work request to the correct entity within the tenant is likewise tenant-internal, and the tenant MUST keep that boundary: no AFP wire message carries a tenant's internal routing topology (§5.6). These are tenant-side duties discharged behind the tenant boundary; nothing in this item crosses the wire.

Where a provision cited above is itself OPTIONAL or conditional — open federation (§5.8), or §8's allowance for an alternative construction satisfying §8.2 — the profile inherits that condition unchanged and does not strengthen it. The profile confers no discovery, registry, reputation, or capability-publication surface of any kind (§1.2); it states what a tenant must satisfy, not what a tenant may advertise. A tenant that does not meet an OB-relevant obligation of this profile MUST NOT claim the corresponding property and MUST disclose the limitation under §3.5.

A shortfall does not by itself end tenanthood or bar a session. Where §1.2, §12.1, §12.5, §13.1, and §5.8 speak of a verified conforming instance, of a verified conforming instance under a verifier-accepted attestation authority, or of conforming tenants, they state the standard each party is entitled to expect and to verify — not a protocol-level registry check, and not a requirement that every session carry an instance attestation. Whether a party requires instance attestation at all, and which (root class, root authority) pairs it accepts if it does, is that party's own policy (§6.3.1, Safeguard 1); the §12.1 trust bases include one — a pre-existing §5.5 agreement — that may require no attestation whatever. Acceptance remains each counterparty's own policy (§5.5, §5.8, §6.3.1, Safeguard 1): a tenant that has disclosed a shortfall under §3.5 or §8.9 remains a principal (§2.1), and whether a given counterparty opens or continues a session with it is that counterparty's decision. AFP defines no gate that excludes a non-conforming tenant from principalhood, and none that compels a counterparty to transact with one.

Conformance to this profile is asserted and bilaterally verified, not certified: there is no conformance registry, mark, or published implementer list (§1.2), and a counterparty's acceptance of an implementation is its own policy decision (§5.5, §5.8, §6.3.1). An attestation root authority (§6.3.1) is not a conformance certifier: a root authority attests what build a machine subject runs, never that a tenant satisfies this profile. Accepting an authority is not accepting a conformance claim, and no authority — named in this specification or otherwise — certifies conformance. The formal multi-vendor conformance program — published tenant-profile vectors as a normative gate — is deferred (§10).

1.6 Notation

Symbol Meaning
H(domain, x...) The AFP-KDC v1.0.3 interface using the exact retained v1.0.2 Poseidon2 sponge over BN254 with a field domain tag derived per Appendix A
AID KERI Autonomic Identifier
KEL Key Event Log
M Tenant master seed (256 bits)
M' Per-entity sub-seed, M' = HKDF(M, "entity/{name}")
π A zero-knowledge proof (UltraHonk / BN254 unless stated otherwise)
dip KERI delegated-inception event
tenant A runtime that federates as a principal, addressed by its own tenant root AID; a tenant MUST satisfy the tenant conformance profile (§1.5.1)
entity A named domain multi-agent system within a tenant, addressed by a delegated AID

2. Architecture

2.1 Principals

Agent-platform instance (definition). An agent-platform instance is one deployment of a multi-agent platform under a single administrative authority: it holds its own KERI root AID and key material (§4), runs one or more entities on its own infrastructure, and is operationally independent of every other instance. AFP places no requirement on which platform an instance runs, on its internal architecture, or on its vendor — only that it satisfies this specification's normative requirements at the wire. Two federating instances MAY run different platforms, implementations, and product releases provided they implement a mutually compatible, pinned AFP interface/profile/substrate tuple; unknown, unmatched, or ambiguous tuples fail closed (§1.4). Where this specification names ADAMAS, it names AFP's originating implementation as a concrete example (§13.1); such a mention is never a conformance condition.

AFP recognizes exactly three kinds of principal. All three are KERI AIDs (§4); they differ only in role, never in identity machinery.

HyperTalk sessions are not AFP principals. A HyperTalk session is a tenant-facing surface; when work must cross a tenant boundary, the entity is the principal, never the session. Within-tenant user identity is internal access control and outside AFP scope.

A §6.3.1 attestation root authority is likewise not an AFP principal, and the enumeration above remains closed at three. An authority is an AID whose published trust material a verifier may elect to accept into its own verification policy; it addresses no AFP message, opens no session, holds no role under this section, and holds no protocol privilege. Its AID is ordinary KERI machinery (§4) used outside the principal roles, exactly as the AIDs of a mix node, a witness, or an external digest source are. Standing as an authority is neither a fourth principal class nor a tier of any of the three; where the same legal entity is also an operator, the roles are independent (§8.6).

2.2 Layering

+-------------------------------------------------------------+
|                AFP 0.3.12-draft                           |
|   - relationship envelopes (tenant<->tenant, tenant<->op)   |
|   - identity continuity (KERI KEL exchange, witnesses)      |
|   - metadata-privacy routing requirements                   |
+-------------------------------------------------------------+
        |                  |                   |
        v                  v                   v
+---------------+  +----------------+  +----------------------+
| ZKA 0.9.2-draft|  |ZKC 0.4.1-draft|  |        KERI          |
| notes, coord, |  | predicates,    |  | AIDs, KELs,          |
| bundles       |  | proofs         |  | witnesses, rotation  |
+---------------+  +----------------+  +----------------------+
        |                  |                   |
        +------------------+-------------------+
                           |
                           v
            +-------------------------------+
            |  Key Derivation Core (KDC)    |
            |  v1.0.3 -- AFP Appendix A:     |
            |  master-seed derivation,      |
            |  Poseidon2/BN254 domain tags, |
            |  per-entity HKDF sub-seeds    |
            +-------------------------------+
                           |
                           v
            +-------------------------------+
            |   Transport (NATS / leaf      |
            |   nodes) + metadata-privacy   |
            |   routing layer (§8)          |
            +-------------------------------+

AFP sits above ZKA, ZKC, and KERI and below nothing — it is the top protocol of the federation stack. ZKM is a composition peer rather than a new AFP layer: it can constrain an agent's spend inside a session without replacing AFP policy or ZKA settlement. AFP never reimplements a primitive that ZKA, ZKC, ZKM, or KERI already defines; where this specification needs such a primitive it cites the owning specification.

Note on the Key Derivation Core. Master-seed derivation, the Poseidon2/BN254 field-tag scheme, and the per-entity HKDF sub-derivation used by ZKA, ZKC, ZKM, and AFP are specified once as the self-contained AFP-KDC v1.0.3 module in Appendix A. AFP-KDC v1.0.3 reuses the v1.0.2 Poseidon2 parameters and conformance artifacts and defines the external fixed-byte ownership boundary without changing a KDC-owned tag, derivation, value, vector, hash, proof, circuit, verification key, or wire record. KDC owns the six derivation tag strings. Each referencing protocol owns its operational tag strings and MUST derive their field constants with §A.3, except for the ZKA pool-adjacent fixed-byte fold tags carved out in §A.4. A dependent pins AFP-KDC v1.0.3 without depending on any other AFP section, so the dependency graph remains acyclic.

2.3 Transport

AFP messages travel over a NATS-based transport (subjects, JetStream for durable streams, leaf nodes for cross-instance links). Transport selection is otherwise unconstrained. The transport is assumed untrusted for confidentiality purposes: §3 and §8 specify what the transport operator may and may not learn, and those guarantees MUST hold regardless of who runs the transport.

AFP defines no HTTP bridge. All AFP traffic is between tenants speaking AFP over the common transport.


3. The Operator-Blindness Invariant

This section is normative and central. It states the property that AFP is taken to promise: a tenant need not trust the operator, because the operator is structurally incapable of compromising tenant confidentiality.

3.1 Statement

Invariant OB (Operator Blindness). No operator of an AFP transport shall be able to learn, from operating that transport:

  1. the content of any tenant's work — work artifacts, payment amounts, counterparty identities, coordination payloads; or
  2. the coordination graph — which tenant is coordinating with which.

OB-1 (content) and OB-2 (graph) are distinct properties with distinct mechanisms. A conforming implementation MUST deliver both. Delivering OB-1 while leaving OB-2 unmet — encrypted payloads over an operator-observable relationship graph — does not satisfy this invariant.

3.2 Capability, not conduct

OB is a statement about capability, not conduct. An implementation satisfies OB only if the operator cannot obtain the protected information, not if the operator chooses not to. "The operator does not look" is not conformance. A design in which the operator could reconstruct the coordination graph by correlation — even if no operator ever does — fails OB.

This follows the same discipline as ZKA Safeguard 3 (No Revocability): the absence of a capability, verifiable by inspection, rather than a promise of good behavior.

3.3 Consequence for deployment topology

OB has a hard consequence. Whoever runs a transport can observe that transport's traffic graph — connection metadata, timing, correlation of endpoints. Therefore, an operator who runs the transport that tenant↔︎tenant traffic transits cannot, by §3.2, satisfy OB-2 for that traffic. Payload encryption (OB-1) is achievable on an operator-run transport; relationship-graph privacy (OB-2) is not.

AFP requires that the operator is not on the coordination path:

A conforming AFP deployment MUST NOT route tenant↔︎tenant coordination traffic through a transport whose operator can correlate the endpoints. Where the §8 routing layer is not yet available in a given deployment, that deployment MUST NOT claim OB-2 conformance and MUST disclose the limitation to its tenants (§3.5).

3.4 Metadata is in scope

OB-2 exists because encrypting content while leaking metadata is a known and unacceptable failure mode for a confidentiality platform. AFP treats the following as protected metadata, not merely the payloads they carry: the existence of a coordination session between two named tenants; the pairing of tenant AIDs in a session; the timing and volume correlation that would reveal such a pairing; and stable identifiers (NKeys, subject names) that would let an observer link a tenant across sessions. §8 specifies the routing requirements that protect these.

3.5 Honest disclosure

A deployment that delivers OB-1 but not OB-2 — for example, an interim deployment before the §8 routing layer is operational — is a valid lesser tier, but it MUST be labeled as such. An implementation MUST NOT represent OB-2 as satisfied unless it is structurally satisfied per §3.2. Claiming appliance-grade or routing-layer-grade confidentiality for a deployment that does not have it is a conformance violation, independent of any cryptographic correctness.

The duty stated in this subsection is a duty of the party making the claim. Two of its addressees are AFP conformance subjects (§1.5): a deployment discloses the tier it actually delivers, as above, and an implementation that cannot support inspection of its OB-relevant behavior discloses that limitation rather than claiming OB (§7.1). A §6.3.1 attestation root authority is named here as a third addressee, but on a different footing, because an authority is not an AFP principal and not an AFP conformance subject (§1.5, §2.1): the character of its attestation is fixed by §6.3.1's class rules, and the duty to characterize it truthfully in published trust material is given normative effect as a verifier acceptance condition in §6.3.1 rather than as a conformance obligation this subsection could enforce. An authority that wants to be accepted discloses accordingly; nothing here binds a party AFP does not otherwise bind. This subsection is otherwise scoped to OB-1/OB-2 claims and neither imposes an OB obligation on an authority nor converts an attestation into an OB claim.


4. Identity

AFP identity uses KERI-aligned self-certifying identifiers. This section is normative; hash-derived identifiers and flat nkey_history arrays are not AFP identity mechanisms.

4.1 Why KERI

A self-certifying identifier can be verified from the identifier and its Key Event Log alone, with no registry lookup and no dependency on any operator. Identity continuity across an operator change or a move to a self-hosted appliance (§1.3 relationship 3) therefore requires KERI alignment; it is not optional.

4.2 Identifier hierarchy

AFP identity is a two-level KERI delegation tree per tenant, with a master-seed derivation beneath it.

4.3 Relationship to ZKA and ZKC keys

The master-seed derivation tree extends below the KERI AID, and ZKA/ZKC keys derive from the per-entity sub-seed, not from the tenant master directly:

            Tenant master seed  M
                     |
                     |  KERI tenant root AID  (KEL governed by witnesses)
                     |
        +------------+------------+
        |                         |
   M' = HKDF(M,              M' = HKDF(M,
   "entity/viksana")         "entity/alochana")     ...
        |                         |
   KERI delegated AID         KERI delegated AID
   (dip anchored in           (dip anchored in
    tenant KEL)                tenant KEL)
        |                         |
   ZKA/ZKC key hierarchy      ZKA/ZKC key hierarchy
   from M' per Appendix A     from M' per Appendix A
   (KDC v1.0.3):              (KDC v1.0.3):
     sk  = H("zka/spending", M')
     vk  = H("zka/viewing", sk)
     pk  = H("zka/proof", sk)
     cbk = H("zkc/credential", M')

For each entity, the entity's sub-seed M' is the master-seed input to AFP-KDC v1.0.3 (Appendix A) — specifically to deriveKeys (the ZKA key hierarchy) and the credential-binding derivation (the ZKC cbk and private/global holderCommitment). The HKDF sub-derivation M' = HKDF(M, "entity/{name}") is itself defined by KDC §A.5. Consequently:

4.4 Key Event Log

Each tenant root and each entity AID has a KEL — an append-only, self-certifying log of inception, rotation, delegation, and interaction events. A flat key-history array is not a conforming substitute.

Pre-rotation is REQUIRED, not optional. Every KEL event commits to the digest of the next key set (next_key_digest); a rotation event reveals the pre-committed next key and commits to the one after. An AFP implementation MUST reject a KEL whose rotation events do not honor prior pre-rotation commitments.

4.4.1 Strict Ed25519 verification profile

Every AFP path that verifies an Ed25519 signature — including KEL and witness receipts, session party references, channel attestations, commitments and dispute records, mixnet-directory quorum signatures, and payment-quote signatures — MUST apply the same strict acceptance profile. After decoding the canonical wire representation to octets, the public key A and signature point R MUST each be a canonical 32-byte Edwards25519 encoding and MUST each belong to the prime-order subgroup. The identity, every non-identity torsion point, every mixed-order point, a non-canonical y >= 2^255 - 19, and the non-canonical sign bit for x = 0 MUST be rejected in either position. The scalar S MUST be a canonical 32-byte little-endian integer strictly less than the group order L. Only then may the verifier evaluate the Ed25519 equation with k = SHA-512(R || A || message) mod L; malformed input and any failed check MUST produce rejection, never coercion or a weaker verification path.

Externally supplied Ed25519 public keys MUST also be validated against the same canonical and prime-order rules when they enter a KEL, witness-key map, mixnet directory, payment policy, or authority trust boundary. A production implementation MAY replace the reference arithmetic with an independently audited library, but its adapter MUST preserve every acceptance and rejection rule above and MUST pass the published AFP strict-verifier conformance cases before the deployment trusts it. An audit claim, matching key/signature lengths, or a library's default verification mode does not satisfy this requirement by itself.

4.5 Witnesses

Each tenant chooses a set of KERI witnesses that receipt its KEL events. Witnesses are the mechanism by which a counterparty — including an appliance with no access to the tenant's original operator — can verify that the tenant's key history is consistent and has not forked.

Witnesses receipt KEL events only; they never see AFP work content and are not a data path under §3. A witness learns that a tenant rotated a key, not what the tenant does.

4.5.1 Witness bootstrap record

A new tenant establishes its initial witness configuration with a witness bootstrap record:

interface AfpWitnessBootstrap {
  kind: 'afp:witness-bootstrap:v1';
  tenant_aid: string;                    // tenant root AID being bootstrapped
  kel_inception_dig: string;             // digest of the tenant root inception event
  witness_set: Array<{
    witness_aid: string;
    witness_endpoint: string;            // transport endpoint for KEL receipts only
    witness_kel_dig: string;             // latest witnessed event for the witness AID
  }>;
  threshold: number;                     // minimum receipts required for KEL acceptance
  bootstrap_mode: 'managed-bootstrap' | 'bring-your-own';
  bootstrap_issuer_aid?: string;         // optional candidate-set publisher, never a trust root
  issued_at: datetime;
  expires_at: datetime;
  tenant_signature: string;              // tenant signs all fields above
  witness_receipts: Array<{
    witness_aid: string;
    kel_event_dig: string;               // MUST equal kel_inception_dig
    receipt: string;                     // KERI receipt over the inception event
  }>;
  bootstrap_issuer_signature?: string;   // optional signature over the offered candidate set
}

The tenant root inception event MUST commit to the same witness_set and threshold carried in the bootstrap record. The bootstrap record is a convenience envelope for discovery and verification; the KEL remains authoritative. A conforming implementation MUST reject a bootstrap record whose signed fields do not match the tenant root inception event.

bootstrap_issuer_aid is optional and advisory. In a managed deployment, an operator or consortium MAY publish a signed candidate set to help a tenant find witnesses, but the tenant chooses the witness set and signs the bootstrap record. The operator's signature is never a trust root and never substitutes for witness receipts.

4.5.2 Managed and appliance bootstrap paths

Two bootstrap paths are conforming:

Both paths produce the same verifier-facing artifact. A counterparty MUST NOT distinguish their trust semantics: the only acceptance criteria are the tenant signature, the KEL/inception match, the declared threshold, and valid KERI receipts from the declared witnesses.

4.5.3 Verification and failure semantics

A verifier that receives a tenant's initial KEL state MUST:

  1. verify the tenant root AID from the inception event;
  2. verify the tenant_signature over the bootstrap record;
  3. verify that kel_inception_dig, witness_set, and threshold match the inception event;
  4. verify that at least threshold receipts in witness_receipts are valid KERI receipts from AIDs in witness_set over kel_inception_dig;
  5. verify each witness AID against its own KEL state up to witness_kel_dig; and
  6. reject expired bootstrap records unless the same witness configuration is already established by later KEL events.

Bootstrap failure has exactly one protocol consequence: the verifier treats the tenant identity as not yet established and declines AFP session establishment with that tenant. It MUST NOT trigger operator intervention, key revocation, note invalidation, or any change to tenant state.

After bootstrap, witness changes are ordinary KEL events: the tenant rotates the witness configuration by publishing the new set and threshold in the KEL and obtaining receipts under the old and new threshold policy according to KERI witness-change rules. Appliance migration (§4.7) carries the current KEL and witness configuration; it is not a re-bootstrap unless the tenant intentionally changes its witness set.

4.6 Rotation, recovery, succession

Identity-lifecycle operations use the KEL:

A master-seed rotation re-derives the entity's ZKA/ZKC keys, which orphans the credential binding (cbk) and any on-chain ZKA notes bound to the old keys. ZKC v0.4.1-draft owns both credential-side continuity primitives: scoped zkc:proof:rebind:v2 exposes only old/new presentation pseudonyms plus their shared scopeId; the frozen zkc:proof:credential_rebind:v1 exposes old/new global holder commitments and is therefore the explicitly-consented linkable profile. There is no anonymous rebind, because a rebind is a continuity claim. Issuer migration tokens MAY optimize integrations but do not replace the ZKC-Core proof. ZKA §5.7 owns note migration.

AFP owns the upper-layer sequencing and presentation of those facts to counterparties. An AFP implementation MUST NOT treat the KEL rotation alone as sufficient when notes or credentials derived from the old seed remain in use.

4.6.1 Rotation presentation artifact

When a tenant or delegated entity rotates a seed that affects ZKA or ZKC material, it MUST publish a rotation presentation:

interface AfpRotationPresentation {
  kind: 'afp:rotation-presentation:v1';
  tenant_aid: string;
  entity_aid?: string;                  // absent for tenant-root rotation
  old_kel_event_dig: string;            // last accepted event before rotation
  rotation_event_dig: string;           // KEL rotation event being presented
  new_kel_event_dig: string;            // latest witnessed event after rotation
  affected_material: Array<'kel' | 'zkc-credential-binding' | 'zka-notes'>;
  presentation_profile: 'zkc:presentation:anonymous:v1' | 'zkc:presentation:scoped:v1' | 'zkc:presentation:linkable:v1';
  zkc_credential_rebind?: {
    proof_kind: 'zkc:proof:rebind:v2' | 'zkc:proof:credential_rebind:v1';
    proof: string;
    public_inputs: {
      // scoped v2: the only holder-derived fields permitted
      old_presentation_pseudonym?: string;
      new_presentation_pseudonym?: string;
      scope_id?: string;
      // linkable v1: present only with explicit holder consent
      old_holder_commitment?: string;
      new_holder_commitment?: string;
      presentation_profile: string;
      issuer_id: string;
      credential_schema_id: string;
      context: string;                   // binds AID, KEL rotation, session, APR, profile, and active handles
      timestamp: datetime;
    };
    proof_public_inputs_hash: string;    // hash of the ZKC-owned public statement above
    issuer_migration_token?: string;     // optional optimization; not a substitute for the proof
  };
  zka_note_migration?: {
    status: 'not-applicable' | 'complete' | 'pending-zka-draft';
    proof_kind?: string;                // ZKA-owned when status is complete
    proof?: string;
    migrated_note_root?: string;
  };
  presented_at: datetime;
  tenant_signature: string;
}

The presentation artifact is an AFP envelope over KEL state plus lower-layer proofs. It does not define either ZKC circuit or the ZKA note-migration proof. Its handle surface is profile-exclusive: scoped v2 MUST omit both holder commitments; linkable v1 MUST omit scoped pseudonyms and scope_id; an anonymous flow that does not require continuity omits zkc_credential_rebind entirely and re-establishes the rotated credential as a fresh anonymous presentation.

4.6.2 Sequencing rules

A conforming implementation MUST present rotation in this order:

  1. Pre-rotation commitment. The prior KEL event MUST already commit to the next key set (next_key_digest, §4.4). If the pre-rotation commitment is missing or not honored, the rotation is invalid.
  2. KEL rotation. The tenant publishes the KEL rotation event, obtains the declared witness-threshold receipts, and makes the witnessed KEL state available to counterparties.
  3. Credential rebinding. If affected_material includes zkc-credential-binding and the consuming flow requires continuity, the tenant MUST include scoped zkc:proof:rebind:v2, or linkable zkc:proof:credential_rebind:v1 after explicit consent. The proof context MUST bind the entity AID, KEL rotation, AFP session, authenticated APR signer, active presentation profile, and exactly the active profile's handles. An issuer migration token MAY accompany the proof but MUST NOT replace it. Anonymous flows that do not require continuity MAY present the rotated credential as fresh and MUST NOT publish an old↔︎new link.
  4. ZKA note migration. If affected_material includes zka-notes, the tenant MUST include a zka_note_migration block. While ZKA §5.7 remains Draft, the only conforming unresolved status is pending-zka-draft; counterparties MUST treat notes bound to the old seed as not yet migrated for new AFP work, but MUST NOT invalidate, freeze, or redirect them. When ZKA finalizes the note-migration proof, status: 'complete' MUST carry the ZKA-owned proof and migrated note root.
  5. Presentation binding. The tenant signs the complete afp:rotation-presentation:v1 artifact and presents it on the relevant tenant↔︎tenant or tenant↔︎operator channel before relying on the rotated material in a new AFP session, telemetry proof, or work bundle.

4.6.3 Verifier behavior

A verifier MUST:

  1. verify the tenant or entity AID against the KEL chain through new_kel_event_dig;
  2. verify witness receipts for rotation_event_dig and new_kel_event_dig under §4.5;
  3. verify that old_kel_event_dig immediately precedes the rotation being presented;
  4. verify the tenant signature over the presentation artifact;
  5. when ZKC credential material is affected, require either (a) a valid profile-specific credential-rebind proof when the consuming flow requires continuity, or (b) the anonymous profile with no zkc_credential_rebind when continuity is unnecessary, treating the rotated credential as a fresh presentation and accepting no old↔︎new linkage; and
  6. require either no affected ZKA notes, zka_note_migration.status == 'not-applicable', or a ZKA-owned completion proof before accepting rotated ZKA notes for new AFP work.

Rotation-presentation failure has the same consequence as other AFP verification failures: the verifier declines the affected session, telemetry proof, or work bundle and emits an alert where applicable. It MUST NOT trigger operator intervention, credential revocation, note invalidation, or any mutation of tenant state.

4.7 Appliance migration

A tenant may move between deployment topologies — for example, from a managed transport to a self-hosted appliance. Because all entity sub-seeds derive from the tenant master seed M, migration is one move: relocating M re-derives every M' and therefore every entity's ZKA/ZKC key hierarchy. The KEL state and witness configuration move with the tenant. No counterparty relationship, attestation, or identifier changes as a result of the move — which is the point of choosing self-certifying identity in §4.1.


5. Tenant ↔︎ Tenant Federation

This section specifies the first relationship: two tenants coordinating work across organizational boundaries.

Relationship to the Bilateral Session Core (§12). Section 12 is the canonical, normative home of the session, identity, dispute, commitment, bundle, and safeguard machinery reused by the §13 profiles. This §5 specifies AFP's base behavior, direct tenant↔︎tenant work exchange, over that core. Where §5 and §12 describe the same construct, §12 states the shared profile-facing rule and §5 applies it to work exchange.

5.1 Model

A tenant↔︎tenant interaction is a bilateral, ZKA-mediated work exchange between two entities, one in each tenant. It is not a marketplace transaction: there is no discovery feed, no open call, no solver. The protocol's job is to make the exchange verifiable and minimally disclosing once the two counterparties are in contact.

Trust between the two tenants rests on one of two bases:

In both cases the counterparty is a specific, identified principal by the time a session is established — open federation removes the prior-agreement assumption, not the identified-principal one. What AFP does not provide in either case is discovery: how two strangers find each other and route first contact is out of scope (§8.8), because a queryable directory of tenants would itself be the coordination graph (§3).

A typical exchange: an entity in tenant A (e.g. Viksana) needs an analysis run by an entity in tenant B; B performs the work; the result returns to A. Neither side reveals more than the work requires.

5.2 Session establishment

A tenant↔︎tenant session is established between two entity AIDs:

  1. Address resolution. The initiating entity resolves the counterparty tenant AID (not the entity AID) to a transport route via the §8 routing layer. The protocol addresses tenants; routing a request to the correct entity inside the receiving tenant is tenant-internal (§5.6).

  2. Mutual authentication. Each side verifies the other's KEL against its declared witness threshold (§4.5). Authentication is of the entity delegated AID, with its delegation chain verified up to the tenant root AID.

  3. Channel encryption. The session establishes an end-to-end encrypted channel between the two entities. All subsequent AFP payloads on the session are ciphertext to any transport operator (OB-1, §3.3). Channel key agreement uses keys derived from the entities' M' hierarchies.

  4. Context binding. The session context binds every proof and message in the session and prevents replay to another session or counterparty. The AFP session atom, envelope, and presentation-context preimage retain exactly the following readable qualified digest. ZKA's BN254-valued bundle, transaction, binding, and Coord context slots receive the canonical field projection defined immediately below; a current ZKC presentation instead receives the distinct purpose-bound §5.9 proof context.

    timestamp = RFC3339_UTC_whole_second(opened_at)
    preimage = RFC8785_JCS_UTF8({
      "initiator_entity_aid": initiatorEntityAID,
      "responder_entity_aid": responderEntityAID,
      "session_id": sessionId,
      "timestamp": timestamp
    })
    session_context = "X_" || lowercase_hex(SHA-256(preimage))

    Let session_digest be the exact 32 bytes encoded after X_, let OS2IP read those bytes as one unsigned big-endian integer, and let r = 21888242871839275222246405745257275088548364400416034343698204186575808495617, the BN254 scalar-field modulus. The ZKA session-context projection is OS2IP(session_digest) mod r, serialized as exactly 0x plus 64 lowercase hexadecimal digits including leading zeroes. It MUST be nonzero and strictly less than r. A zero projection, malformed or alternate prefix, non-lowercase or wrong-width encoding, noncanonical field value, or mismatch MUST fail closed; implementations MUST NOT retry, rehash, normalize, substitute, or fall back. The envelope retains the original X_ value, and this deterministic reading into ZKA's field is not a second session context. It is the same big-endian mod-r, reject-zero seam used by §15.4.

    opened_at is a signed-wire RFC 8785/JCS JSON integer number, not a JSON string, with the inclusive range 0..253402300799. Its canonical wire token is unsigned base-10 decimal: 0, or a nonzero digit followed only by decimal digits; an exponent, decimal point, sign, leading-zero form, or any other representation is noncanonical. It maps exactly to UTC 1970-01-01T00:00:00Z at 0 and UTC 9999-12-31T23:59:59Z at 253402300799; every intermediate value maps to its exact whole-second RFC 3339 UTC form YYYY-MM-DDTHH:MM:SSZ. The upper bound is less than 2^53, so every permitted value is exactly representable as an IEEE-754 JavaScript number. Implementations MUST reject every other JSON type, every fractional, negative, out-of-range, or noncanonical token, and MUST NOT round or coerce while parsing or recomputing a received context. The four preimage members are strings, their names are fixed ASCII, and the JCS bytes above contain no insignificant whitespace. X_ is AFP's readable qualified-digest form, not a CESR encoding. A recipient MUST verify the initiator signature over the proposal's stable content (including session_context), authenticate the proposal's responder binding as §12.2.1 requires, then recompute this value from those authenticated party bindings and reject a mismatch before accepting the session. Equality proves that the responder entity AID was committed by the initiator-signed context; a different responder cannot satisfy it.

    The following vector is normative and MUST reproduce exactly:

    initiatorEntityAID = "E_i"
    responderEntityAID = "E_r"
    sessionId          = "sess-1"
    opened_at          = 1700000000
    timestamp          = "2023-11-14T22:13:20Z"
    preimage           = {"initiator_entity_aid":"E_i","responder_entity_aid":"E_r","session_id":"sess-1","timestamp":"2023-11-14T22:13:20Z"}
    session_context    = X_eddb6ad038bd9a1a79c6c1d29ebd7461da0383948b7e6c13dadd6553b97e4a7f
    zka_bundle_context = 0x2c4a3104b3f719739885aaf898b812ed3933e272a498a9cecb558f03f97e4a7b

    docs/afp_opened_at_conformance_v1.json is the normative machine-readable conformance fixture for this rule. Every conforming runtime MUST accept and reproduce both session_context and zka_bundle_context for its min, published, and max vectors and MUST reject every listed rejected value or raw JSON token without rounding, coercion, alternate timestamp formatting, or alternate field mapping.

Open-federation variant (§5.8). For an unknown-counterparty session the entity AIDs are not drawn from a pre-existing agreement; they are the attested identities established at first interaction (the ZKC verifier/holder identities each party authenticates under §5.8). Steps 2–4 are otherwise unchanged: the same KEL verification (step 2), the same channel encryption (step 3), and the same context-binding hash (step 4) over those attested AIDs — additionally binding the shared ZKC proof context so the session is anchored to the specific attestation that opened it. The binding shape is identical; only the provenance of the two AIDs differs (attested-at-first-contact vs. carried-in-agreement).

5.3 Message classes

AFP defines a deliberately small set of message classes for the tenant↔︎tenant session. All are carried inside the encrypted channel of §5.2. These are the message classes of the base work-exchange behavior; under the canonical NATS namespace of §12.4 they are addressed as leaves of the base profile's subtree, afp.{tenant}.work.* (with afp.{tenant}.session.* for the lifecycle classes). The bare names below are used for readability; §12.4 is normative for their full tenant-scoped addressing.

Class Purpose
afp.session.open / afp.session.close Establish / tear down a session per §5.2
afp.work.request An entity requests cross-tenant work. Carries a work descriptor and the requester's bilateral-agreement reference (§5.5).
afp.work.result The performing entity returns a result, carried as ZKA note(s) and/or a ZKA/Coord proof (§5.4).
afp.disclose.request / afp.disclose.response One side requests, and the other provides, a selective disclosure or compliance bundle (§5.4, §6.1).
afp.attest An entity contributes a coordination attestation (ZKA/Coord proof) for a completed multi-step workflow.

There is no afp.register, no afp.discover, no afp.delegate as a marketplace primitive. Work requests are a message inside an already-established bilateral relationship, not an open call.

5.4 Work artifacts ride on ZKA

Cross-tenant work artifacts, attestations, and value transfers are ZKA constructs; AFP carries them and defines their interaction envelope but does not redefine them.

A tenant↔︎tenant compliance requirement is an interaction-boundary concern (Safeguard 5): it may cause the requesting tenant to decline the interaction, and nothing more. It MUST NOT gate either tenant's access to its own funds or notes, and a failed bundle verification has exactly one permitted consequence — the counterparty declines — per ZKA v0.9.2-draft §5.4 and Safeguard 3.

5.5 Bilateral federation agreements

AFP does not define individual-agent reputation or a portable positive reputation score. Trust on the known-counterparty path is a bilateral federation agreement: an explicit, mutually signed record that two tenant AIDs have agreed to federate, optionally scoped by entities, work classes, and compliance requirements.

The bilateral federation agreement is the trust basis for the known-counterparty path. Two tenants with no prior agreement may instead establish trust per interaction by ZKC compliance attestation under the open-federation path of §5.8. Both paths are bilateral and private; neither creates a portable reputation score. Where this section says "the agreement," an open-federation session substitutes the per-interaction attestation and its policy block.

5.6 Internal routing boundary

When an afp.work.request arrives addressed to a tenant, routing that request to the correct entity within the receiving tenant is tenant-internal and outside AFP scope. The protocol sees tenant AIDs as session endpoints and entity AIDs as the principals named inside the encrypted channel; it does not see, and does not specify, how a tenant dispatches an inbound request across its own entities. A conforming implementation MUST keep this boundary: no AFP wire message carries a tenant's internal routing topology.

5.7 Rogue-agent threats are out of scope

A compromised or misbehaving entity inside a tenant operates with that tenant's legitimate keys and legitimate access. Protecting a tenant against its own rogue entity is a containment, capability-scoping, and observability problem handled within the tenant: a conforming tenant MUST contain, capability-scope, and observe its own entities (§1.5.1 item 7), and the mechanism is tenant-internal. ADAMAS, the reference implementation, discharges this through policy enforcement, per-entity capability scoping, and the Drasta observability entity acting on the tenant's behalf. It is explicitly not an AFP concern. AFP's guarantees — operator blindness, federation verifiability, identity continuity — MUST NOT be read as covering an internal rogue agent. This boundary is stated so that conforming implementations do not conflate the two.

5.8 Open federation — trust by ZKC attestation

Supporting open federation is OPTIONAL; a conforming deployment MAY support only the bilateral path of §5.5. Where a deployment supports open federation, the requirements in this subsection are normative (MUST/MUST NOT as stated).

Open federation is a tenant↔︎tenant session (§5.1, §5.2) between two conforming tenants that hold no prior bilateral federation agreement. In place of the agreement, trust is established per-interaction by mutual ZKC compliance attestation. This is the second of the two trust bases of §5.1; everything else about the session — its message classes (§5.3), its work-on-ZKA carriage (§5.4), its routing (§8), its session core (§12), and all six Freedom Safeguards (§9) — is unchanged.

Trust establishment. To open an open-federation session, each party in turn:

  1. Authenticates a scoped proof request. The requesting tenant issues a ZKC v0.4.1-draft Authenticated Proof Request declaring the predicates, purpose/proof context, presentation.profile = zkc:presentation:scoped:v1, and scopePolicy = protocol:afp:<profile_policy_hash>. The APR is signed by the current KEL-anchored key of the requesting entity AID. An unauthenticated request is rejected — no signature ⇒ no proof.
  2. Answers with a subject-bound ZKC compliance bundle. The responder answers with zkc:proof:v2 inside ZKA v0.9.2-draft zka:bundle:v1, wrapped in afp:envelope:v1, or declines. Every proof is atomic and its verified credential-subject AID, scoped pseudonym, and proof context MUST satisfy §5.9. A borrowed anonymous proof is not an open-federation trust basis.
  3. Retains a profile-minimized Disclosure Receipt. Each party retains a holder-custodied ZKC Disclosure Receipt containing the scoped pseudonym and scopeId, never holderCommitment, unless the holder separately and explicitly consented to the linkable v1 profile.

Because each side both requests and answers, trust is mutual: neither party is a pure verifier nor a pure holder, and each ends the exchange holding a receipt of the other's demand.

Acceptance is each party's own policy. There is no global registry of who-may-transact-with-whom, no canonical predicate set, and no canonical or required attestation authority. Each party decides, under its own acceptance policy, whether the bundle it received satisfies it (Predicate Pluralism, Safeguard 1; verifier flexibility, ZKC §1.1). Where a party's acceptance policy includes afp.telemetry.instance_attestation.v2, that policy names the (root class, root authority) pairs it accepts per §6.3.1 and the counterparty proves under one accepted pair, exactly as §14.2 requires on the introduction path; a party MAY accept an authority the other party rejects, and neither outcome is a conformance judgment about the other. A satisfied policy opens the session; an unsatisfied one declines it — and declining is the only permitted consequence of a failed attestation, of a rejected authority, or of an authority found stale or revoked (Safeguard 5; see below).

Identity binding. The session principals are the attested entity AIDs from step 1–2. For each direction the KERI-authenticated presenter AID, proof-authenticated credential-subject AID returned by the ZKC verifier, and responder AID MUST be identical; the APR signer key MUST resolve through the verifier AID's current KEL; and the scope and proof context MUST recompute under §5.9. The verifier MUST derive that subject AID from an issuer-authenticated claim in the verified proof, or from an independently verified companion subject-binding proof; it MUST NOT copy a holder-declared envelope label. This is the proof-borrowing guard: a proof produced for another holder/AID, verifier, policy, profile, or session is rejected even if its predicate result is otherwise true.

Safeguard 6 is what makes this safe, and is why the path is viable. The party demanding "prove you are compliant to transact with me" is a verifier in the ZKC sense; Safeguard 6 requires it to authenticate the demand under a stable identity and leaves the responder with portable evidence of the demand. ZKC v0.4.1-draft scoped presentation further prevents the accountability record from forcing global holder linkability.

Interaction-boundary only (Safeguard 5). A declined or failed attestation means the counterparty declines the interaction, and nothing more. It MUST NOT gate, freeze, redirect, or seize either party's funds or notes; it MUST NOT touch ZKA settlement or ZKC credential validity; and exit remains unconditional. Open federation changes who may open a session with whom and how that trust is shown — it changes nothing about value.

What open federation does NOT provide — discovery and rendezvous (§8.8). This subsection covers trust establishment once the two parties are in contact. It does not specify how two strangers find each other or how first contact is routed. A queryable directory mapping tenant AIDs to routes would itself be the coordination graph that operator-blindness (OB-2, §3, §8) exists to prevent. Until a dedicated discovery/rendezvous layer with its own privacy analysis exists (a §10 open item), open federation is usable only where first contact is established out-of-band. AFP deliberately solves the trust half and leaves the discovery half explicitly out of scope, rather than admitting a directory that would weaken OB-2.

Relationship to the Introduction Tier (§14). Open federation and the introduction tier (§14) are the two ways AFP admits a session between tenants with no pre-existing agreement; a deployment MAY offer either or both. They differ in what they leave behind: §5.8 establishes trust per interaction with no agreement (lightest; suited to one-off or low-stakes exchange), whereas §14 runs an introduction session that mints a signed provisional §5.5 agreement and grows a progressive, ZK-provable reputation envelope (heavier; suited to an ongoing relationship). Both inherit all six safeguards, are interaction-boundary-only (Safeguard 5), and share the same unresolved stranger-rendezvous problem (§10). The three trust bases are enumerated together in §12.1.

5.9 ZKC v0.4.1-draft presentation profiles and AFP identity continuity

AFP consumes ZKC v0.4.1-draft presentations through zka:bundle:v1; ZKC remains authoritative for the v1/v2 envelopes, circuits, APR signatures, pseudonym/scope derivation, and proof verification. AFP is authoritative for selecting the privacy profile a flow may consume and binding the verified ZKC result to AFP identity and session state.

AFP flow Accepted profiles Default Downgrade rule
Tenant→operator telemetry (§6) anonymous, scoped, linkable anonymous scoped→anonymous MAY be accepted when the predicate and policy window require no cross-presentation continuity
Known-counterparty bilateral (§5.5 / §12) anonymous, scoped, linkable anonymous scoped→anonymous MAY be accepted when KERI/session authentication already supplies all required continuity
Introduction (§14) scoped, linkable scoped no anonymous downgrade; the presentation becomes the credential basis of an ongoing provisional agreement
Open federation (§5.8) scoped, linkable scoped no anonymous downgrade; the presentation is the per-interaction trust basis that opens the session

In every row, zkc:presentation:linkable:v1 is the frozen v1 proof path and requires an explicit, recorded holder consent for that presentation. It is never selected by fallback, downgrade, verifier preference, or an absent APR presentation block.

Scoped policy derivation. AFP scoped requests use the signed ZKC scopePolicy string protocol:afp:<profile_policy_hash>, where profile_policy_hash is the exact session, telemetry, or introduction policy hash authenticated for the interaction. ZKC derives scopeId from the authenticated APR signer public key, that scope-policy string, and its declared rotation epoch (ZKC v0.4.1-draft §5.9.1). The holder derives the identifier itself; AFP MUST reject a verifier-supplied literal scope. The APR signing key MUST be the current KEL-anchored key for apr_verifier_aid; swapping either the AID or key fails verification.

AID, credential subject, flow purpose, and context binding. A proof accepted for an AFP responder MUST cryptographically prove the credential's subject-AID claim equals that responder's KERI entity AID. AFP then requires the KERI-authenticated envelope presenter to be the same AID. AFP 0.3.12-draft uses the v2 ZKC presentation context below. Its purpose is selected from the verifier's authenticated expected flow, never from a holder-supplied label: telemetry maps to afp:trust-flow:telemetry, known-counterparty bilateral maps to afp:trust-flow:bilateral, introduction maps to afp:trust-flow:introduction, and open federation maps to afp:trust-flow:open-federation.

The v1 record is retained byte-for-byte only to identify and audit historical AFP 0.3.10-draft artifacts:

interface AfpZkcPresentationContextV1 {
  kind: 'afp:zkc-presentation-context:v1';
  credential_subject_aid: string;       // == KERI-authenticated presenter entity AID
  session_context: string;              // exact §5.2 / §12.3 context
  apr_verifier_aid: string;             // APR signer key resolves here through its KEL
  presentation_profile: string;         // active, not merely requested, profile
  scope_policy: string | null;           // protocol:afp:<profile_policy_hash> iff scoped
}

It MUST NOT be silently translated, treated as an alias for v2, or accepted by a current AFP 0.3.12-draft flow. The current preimage is exactly:

interface AfpZkcPresentationContextV2 {
  kind: 'afp:zkc-presentation-context:v2';
  credential_subject_aid: string;       // == KERI-authenticated presenter entity AID
  session_context: string;              // exact §5.2 / §12.3 context
  apr_verifier_aid: string;             // APR signer key resolves here through its KEL
  presentation_profile: string;         // active, not merely requested, profile
  scope_policy: string | null;           // protocol:afp:<profile_policy_hash> iff scoped
  purpose: 'afp:trust-flow:telemetry' | 'afp:trust-flow:bilateral' | 'afp:trust-flow:introduction' | 'afp:trust-flow:open-federation';
}

Before hashing, the session_context member MUST be the exact canonical §5.2 X_ digest and its §5.2 ZKA field projection MUST be nonzero; the record retains the original X_ value, not that projection. A malformed, alternate-prefix, wrong-width, non-lowercase, or zero-projecting session value is rejected before the presentation hash is computed. Let h be the 32 SHA-256 digest bytes of the RFC 8785/JCS UTF-8 encoding of exactly that v2 record, let OS2IP interpret those bytes as one unsigned big-endian integer, and let r = 21888242871839275222246405745257275088548364400416034343698204186575808495617, the BN254 scalar-field modulus. The proof-context scalar is OS2IP(h) mod r. A zero result is rejected fail-closed; implementations MUST NOT retry, rehash, or substitute another value. A nonzero result is serialized as exactly 32 unsigned big-endian bytes and rendered as 0x followed by exactly 64 lowercase hexadecimal digits, including leading zeroes. The verifier MUST construct it from the KERI-authenticated responder AID, live session context, authenticated APR verifier AID, active presentation profile, derived-or-absent scope policy, and caller-authenticated expected flow. A presentation's declared flow is checked for exact equality only; it is not authority for policy selection or context construction. Unknown flows, v1 values, v2-shaped records without purpose, noncanonical field encodings, zero, and any proof whose verified context differs from this recomputation are rejected.

For a subject-binding proof profile, the credential issuer signature binds the subject-AID claim to the credential and private holder commitment; the ZKC proof binds that credential subject and the active profile handle to this context; the AFP envelope signature binds the same result to the presenter's KERI AID. The generic ZKC KYC and jurisdiction profiles do not expose or prove an AID claim by themselves and therefore cannot, alone, satisfy this AFP subject-binding contract; they require a separately specified issuer-authenticated subject claim or companion proof. This repository ships no production ZKC adapter or companion subject-binding profile: its fixed-subject callbacks are synthetic conformance fixtures only. A deployment using only the generic ZKC v0.4.1-draft profiles MUST reject, and introduction/open-federation credentialing is not operational until that deployment supplies and authenticates the separately versioned subject-binding mechanism. Consequently a proof borrowed from an unrelated holder, AID, APR signer, scope, profile, session, or trust flow cannot be repackaged: at least one issuer claim, ZKC public input, KEL key binding, or AFP context comparison fails. Relabelling both holder-visible flow fields does not help because the verifier recomputes v2 from its own expected flow and requires the lower-layer verified statement to report that exact context. A boolean envelope label that merely says “verified” is insufficient; a conforming AFP implementation MUST invoke a ZKC v0.4.1-draft verifier and consume a structured verified public statement containing the exact proof context, proof-authenticated credential-subject AID, and atomic predicate statements. The adapter MUST return rejection when it cannot prove any one of those fields. Flow-purpose binding does not replace meaning binding: AFP MUST also compare the verified atomic predicate statements against the caller-authenticated required-predicate policy for that flow (§6.3, §6.3.1, §14.4).

Version and handle exclusivity. Anonymous and scoped presentations use zkc:proof:v2; linkable presentations use the frozen zkc:proof:v1. The built-in KYC predicate is profile-versioned with that envelope: anonymous/scoped use zkc:proof:kyc:v2, while linkable uses zkc:proof:kyc:v1; a verifier selects the exact accepted predicate IDs in its caller-authenticated policy and AFP supplies no counterparty-wide catalog. Every one is carried in ZKA v0.9.2-draft zka:bundle:v1. AFP receipts, rotation presentations, and other AFP-authored artifacts expose no holder-derived handle beyond the active profile:

Active profile Permitted holder-derived fields
anonymous none in AFP-authored artifacts; presentationPseudonym == 0 and scopeId == 0 occur only as sentinels in the ZKC v2 proof envelope
scoped presentationPseudonym, scopeId
linkable holderCommitment only

Mixing handle families is a conformance failure. In particular, a scoped receipt or rotation artifact MUST NOT carry holderCommitment. An anonymous ZKC v2 proof envelope MUST carry the two zero sentinels required by ZKC, while an AFP-authored receipt or rotation artifact MUST omit all three holder-derived fields; implementations MUST NOT serialize the proof-envelope sentinels into those AFP artifacts.

Conformance. Implementations MUST include negative cases for a swapped presenter AID, holder-declared credential-subject AID, proof-authenticated credential-subject AID, scope, APR signer key or AID, session/proof context, presentation profile, trust-flow purpose, and verified predicate meaning. They MUST reject a malformed, alternate-prefix, or zero-projecting AFP session context before hashing the presentation record; a v1 context; a v2 preimage with no purpose; a context produced for any other flow; a noncanonical or zero BN254 context; a verified subject differing from either the responder or the declared subject; and a presentation whose holder-facing flow labels are both changed while the lower-layer verified statement remains bound to the original flow. They MUST also demonstrate that correctly purpose-bound presentations remain accepted; that anonymous presentations remain accepted in telemetry and known-counterparty bilateral flows where continuity is unnecessary; that an anonymous rotation presents the credential as fresh with no rebind or artifact handles; that scoped continuity requires zkc:proof:rebind:v2; and that introduction/open-federation anonymous downgrades and unconsented linkable presentations are rejected.


6. Tenant ↔︎ Operator Telemetry

This section specifies the second relationship: an operator learning that the platform is healthy without learning what tenants do. It rides on ZKC.

6.1 Model

The operator needs operational assurance — that instances are running supported, attested builds and behaving within agreed bounds. Under the operator-blindness invariant (§3), the operator obtains this assurance only through claims a tenant proves, never through observation of tenant work or the coordination graph.

Under §3.2, per-tenant transport observation is not a capability the operator is permitted to have. Therefore all per-tenant operator knowledge is ZKC-proved. Nothing in the operator-health model may depend on the operator watching individual tenant traffic.

Telemetry uses the §5.9 flow policy. zkc:presentation:anonymous:v1 is the default because the authenticated tenant→operator channel and the predicate's public tenant/operator/window inputs already provide interaction identity. An operator MAY request scoped continuity across one declared telemetry policy window; the tenant MAY downgrade that request to anonymous when the predicate does not require cross-presentation continuity. Linkable v1 is accepted only after explicit holder consent and MUST NOT be required as the ambient telemetry mode.

6.2 What the operator may learn

The operator's per-tenant knowledge is exactly the set of claims in the telemetry predicate catalog (§6.3), each delivered as a ZKC proof. Separately, the operator MAY compute aggregate operational metrics across the whole tenant base (for example, total compute consumed, total error count) provided the aggregate is computed so that no per-tenant value is recoverable from it — by aggregation with a sufficient tenant count, or by differential privacy. Aggregate metrics MUST NOT be derived in a way that reveals or narrows any individual tenant's value, and MUST NOT reveal the coordination graph.

6.3 Telemetry predicate catalog

Operator-facing claims are ZKC proofs. Each claim is one atomic predicate (Safeguard 2). AFP defines the starting predicate catalog below; each predicate circuit MUST be open-source, versioned, and published with a public review period before it is accepted by an operator (§9 Safeguard 4). A telemetry proof that uses an unregistered predicate identifier, an unsupported predicate version, or a predicate outside this catalog is not AFP-conforming.

The telemetry catalog is closed. This is consistent with Safeguard 1 (Predicate Pluralism), which protects a verifier's freedom to choose predicates in a bilateral interaction between equals, including §5.8 tenant↔︎tenant attestation where AFP defines no canonical predicate set or global registry. The tenant→operator direction is asymmetric: the operator runs infrastructure the tenant depends on, and an open catalog would let it demand arbitrary claims as a condition of service, creating the leverage §3 and §6.6 forbid. The closed catalog is a tenant protection. Root classes are extensible inside the catalog through a specification revision (§6.3.1), and any new telemetry predicate passes Safeguard 4's open-source and public-review gate. An operator MUST NOT condition service on a telemetry predicate outside this catalog.

Predicate ID Claim Issuer model Public inputs Private witness Cadence Failure signal
afp.telemetry.instance_attestation.v1 "I am a verified ADAMAS instance, build hash X" Third-party-issued: the ADAMAS publisher signs a build manifest; the tenant proves possession. Tenant AID, entity AID or instance AID, build manifest digest, publisher AID, predicate version, telemetry window, operator AID. Publisher-issued ZKC credential, holder binding key, instance signing key. At bootstrap, after build change, and at least once per operator policy window. instance_attestation_missing or instance_attestation_invalid.
afp.telemetry.instance_attestation.v2 "This machine subject runs the attested manifest under verifier-accepted root class R and authority A." Pluggable, as classified by §6.3.1: third-party-verifiable build provenance or TEE attestation, or a self-attested publisher manifest. Tenant AID; entity AID or instance AID; attested manifest digest; root class; root-authority identifier; root-material digest; attestation-evidence digest; verification-policy digest; public-sample-binding digest (the zero sentinel only where §6.3.1 permits it); predicate ID and version; attestation window; verifier AID; active presentation profile. ZKC machine build/runtime credential and holder binding key; instance signing key; attested manifest; root-specific provenance, inclusion, quote, endorsement, or publisher-signature evidence and openings; public-sample-binding witness when required. At bootstrap, after any manifest, root authority, root material, measurement, endorsement/collateral, or verification-policy change, and at least once per verifier policy window. instance_attestation_missing, instance_attestation_invalid, instance_attestation_unsupported_root, instance_attestation_untrusted_authority, instance_attestation_provenance_not_in_log, instance_attestation_measurement_mismatch, or instance_attestation_sample_binding_missing.
afp.telemetry.build_currency.v1 "I run a supported build" Third-party-issued: the publisher signs the supported-build set; the tenant proves set membership. Tenant AID, build manifest digest, supported-build-set root, publisher AID, support-policy version, telemetry window, operator AID. Membership path for the build digest and publisher-issued support credential. At bootstrap, after build change, and whenever the supported-build-set root changes. build_currency_missing, build_currency_expired, or unsupported_build.
afp.telemetry.compute_quota.v1 "My consumption is within agreed bounds" Self-attested: the tenant signs a usage-log root and proves the bound. Tenant AID, usage-log root, quota policy digest, committed usage upper bound, telemetry window, operator AID, afp:telemetry-sample-binding:v1 digest. Usage-log inclusion/range witnesses, tenant usage signing key, and sample-binding witness from §6.4. Once per quota window and after any policy-window close. compute_quota_missing, compute_quota_invalid, or compute_quota_over_bound.
afp.telemetry.audit_log_integrity.v1 "My audit log has not been truncated" Self-attested: the tenant signs the current append-only audit-log head. Tenant AID, previous accepted log head, current log head, append-only accumulator root, telemetry window, operator AID, afp:telemetry-sample-binding:v1 digest. Append-only proof from previous to current head, tenant audit signing key, and sample-binding witness from §6.4. Every audit window and after recovery from a missed window. audit_log_missing, audit_log_invalid, or audit_log_truncated.

Every operator-facing catalog proof MUST bind the tenant AID, operator AID, predicate ID, predicate version, telemetry subject, telemetry window, active ZKC presentation profile, and the exact afp:trust-flow:telemetry purpose into the proof transcript. For v2, these values map to verifier_aid = operator AID and attestation_window = telemetry window under §6.3.1. Scoped proofs additionally bind protocol:afp:<telemetry-policy-hash>, the APR signer, pseudonym, and scopeId per §5.9. This prevents replay across verifier, subject, window, policy, profile, scope, or AFP trust flow. Predicate versions are monotonic: a verifier MAY accept multiple versions during a migration window, but it MUST publish the accepted version set and MUST NOT reinterpret a v1 predicate or linkable presentation under later semantics.

Third-party-issued predicates prove what a tenant is. Self-attested predicates prove how a tenant behaves and MUST carry the public-sample binding of §6.4. build_currency is third-party-issued; compute_quota and audit_log_integrity are self-attested. Instance attestation is versioned separately: v1 has the frozen publisher-issued semantics in its catalog row, while v2 uses the root-class taxonomy and binding rules below. Both versions are usable operator-facing; counterparty-facing policy in §14 uses v2 so it can select a vendor-neutral root. The delegated, product-scoped publisher construction of §6.3.1 applies only to v2 and later versions. The v1 publisher AID is a fixed publisher field and MUST NOT be interpreted as a delegated AID, organizational root, or generic authority identifier.

6.3.1 Instance-attestation root classes

afp.telemetry.instance_attestation.v1 is frozen. Its complete public inputs, circuit, private witness, cadence, failure signals, and semantics are the v1 catalog row above. Implementations MUST NOT interpret its publisher AID as a generic authority field, an organizational root AID, or the delegated product-scoped publisher AID of the v2 construction below, and MUST NOT attach v2 semantics to a v1 proof. The freeze is semantic as well as textual: wire decoding alone cannot detect a prohibited v2 interpretation of a v1 proof, so conforming verifiers MUST apply only the v1 publisher-issued semantics.

afp.telemetry.instance_attestation.v2 fixes the generic statement. Its wire-level public statement is:

interface AfpInstanceAttestationPublicInputsV2 {
  kind: 'afp:instance-attestation-public-inputs:v2';
  tenant_aid: string;
  subject_aid: string;                       // entity AID or instance AID
  attested_manifest_digest: string;          // canonical digest, defined below
  root_class: 'root:build-provenance' | 'root:tee-attestation' | 'root:publisher-manifest';
  root_authority_identifier: string;
  root_material_digest: string;
  attestation_evidence_digest: string;
  verification_policy_digest: string;
  public_sample_binding_digest: string;      // canonical digest or exact zero sentinel
  predicate_id: 'afp.telemetry.instance_attestation.v2';
  predicate_version: '2';
  attestation_window: { starts_at: datetime; ends_at: datetime };
  verifier_aid: string;
  presentation_profile: string;              // active §5.9 profile
}

For tenant→operator telemetry, verifier_aid MUST equal the telemetry operator AID and attestation_window MUST equal the telemetry window. For §14 counterparty use, verifier_aid MUST equal the APR's apr_verifier_aid; the APR signing key MUST resolve through that AID's current KEL, and a delegated verifier AID MUST resolve through its delegation chain to the counterparty tenant AID. attestation_window MUST equal the explicit attestation_window on afp:introduction-request:v1 and MUST be bounded by the session expiry. The proof context additionally binds the APR signer, session context, scope policy, active presentation profile, and the exact §5.9 trust-flow purpose selected by the verifier. A proof made for one flow, verifier, or window MUST NOT verify in the other.

Where an authority delegates product-scoped signing (below), two obligations apply and are discharged differently. The delegated signing AID's delegation chain MUST resolve to the AID named by root_authority_identifier, and the verifier MUST perform that resolution out of circuit before acceptance — that resolution reads only published KEL events, which the verifier holds. The manifest signing key MUST be the current signing-key state of that delegated AID. The verifier recomputes root_material_digest from the signing-key state it resolves, and the circuit proves that the hidden manifest signature verifies under exactly that committed state. The circuit binds the leaf signing-key state committed by root_material_digest; the delegation chain is an out-of-circuit verifier obligation. A verifier that cannot resolve the chain to the named authority root MUST reject the proof with instance_attestation_untrusted_authority.

Canonical encoding (normative). Every non-sentinel *_digest is the ASCII string sha256: followed by exactly 64 lowercase hexadecimal characters. AFP-authored JSON artifacts use RFC 8785 JSON Canonicalization Scheme (JCS) bytes and MUST contain a kind discriminator; AFP's datetime strings first normalize to whole-second UTC YYYY-MM-DDTHH:MM:SSZ. External provenance statements, transparency-log entries/checkpoints, hardware reports/collateral, and publisher-signed manifests MUST NOT be re-encoded as AFP JSON. Their digest is SHA-256 over ASCII("AFP-External-Artifact-v2\0") || u32be(len(kind)) || UTF8(kind) || u32be(len(scheme)) || UTF8(scheme) || u64be(len(signed_bytes)) || signed_bytes, where signed_bytes are the exact scheme-native signed bytes and kind and scheme are nonempty ASCII identifiers fixed by the accepted verification policy. This wrapper domain-separates artifact types without changing the signed payload.

Publisher signing-key state (normative). For root:publisher-manifest, root_material_digest commits to the publisher's signing-key state. That state is neither an external provenance artifact nor a transparency-log checkpoint nor hardware collateral, so it is AFP-authored and therefore carries a kind under the rule above. Its preimage is the RFC 8785 JCS bytes of exactly:

interface AfpPublisherKeyState {
  kind: 'afp:publisher-key-state:v1';
  authority_aid: string;                 // == root_authority_identifier
  signing_aid: string;                   // delegated signing AID; == authority_aid when signing is not delegated
  delegation_anchor_dig: string | null;  // said of the authority-KEL event anchoring the delegated inception; null iff signing_aid == authority_aid
  key_state_event_dig: string;           // said of the establishment event, in the signing AID's KEL, fixing the state below
  key_state_sequence_number: number;     // that event's sequence number in the signing AID's KEL
  signing_public_keys: string[];         // ordered current signing keys, exactly as the establishment event lists them
  signing_threshold: string | string[] | string[][];  // that event's `kt`, copied verbatim from the establishment event
  next_key_digests: string[];            // ordered pre-rotation commitments from the same event, verbatim
  next_threshold: string | string[] | string[][];     // that event's `nt`, copied verbatim from the establishment event
}

Field encodings are KERI-native and are never re-encoded. delegation_anchor_dig and key_state_event_dig carry the KERI SAIDs of those events exactly as the KEL lists them (CESR-qualified), and each element of signing_public_keys and next_key_digests carries the corresponding key or pre-rotation commitment exactly as the establishment event lists it. signing_threshold and next_threshold carry the establishment event's kt and nt values verbatim — a hex string for a simple threshold, or the nested list of fraction strings KERI uses for a weighted multisig threshold, which is why the type is a union. These fields are KERI-native identifiers and values, they follow the _dig convention this specification already uses for KEL event digests (§4.5.1, §4.6.1, §12.2.1), and they are NOT re-encoded under the sha256:-prefixed *_digest rule above. RFC 8785 JCS then canonicalizes the record — including a nested threshold — deterministically.

root_material_digest is sha256: followed by the lowercase hex SHA-256 of those JCS bytes. A verifier MUST recompute this digest from the key state it independently resolves out of the signing AID's KEL, reached through the delegation anchors in the authority's KEL (§4.2, §4.4), and MUST reject a mismatch with instance_attestation_untrusted_authority. Where signing is not delegated the signing AID is the authority AID and the two KELs coincide. Every field is derived from published KEL events, so the record is a re-encoding of state the verifier already holds; it is never transmitted as an AFP message, adds no public input, and adds no wire field. For root:publisher-manifest, the same record MUST also appear in the circuit's private witness: the circuit reconstructs its RFC 8785 JCS bytes, opens root_material_digest as their SHA-256 digest, and uses that record's signing_public_keys and signing_threshold for publisher-signature verification. The circuit binding and the verifier's independent recomputation are both mandatory and prove different facts: the former binds the hidden signature to the public digest, while the latter binds that digest to authority-anchored public KEL state. For root:build-provenance and root:tee-attestation, root_material_digest retains the external-artifact meaning fixed in their branches below and MUST NOT use this record.

The exact absent-sample sentinel is sha256:0000000000000000000000000000000000000000000000000000000000000000, which MUST NOT be accepted as an artifact digest. The circuit decodes each real digest to 32 bytes and exposes it as two unsigned 128-bit big-endian limbs (high limb, then low limb). It maps root_class to 1, 2, or 3 in the order shown above. Each string public input maps separately to two limbs as SHA-256 over u32be(byte_length) || UTF8(NFC(string)). The attestation_window object maps to two limbs as SHA-256 over its RFC 8785 JCS bytes and exposes starts_at and ends_at additionally as unsigned 64-bit Unix seconds. Verifiers MUST recompute every mapping and reject noncanonical strings, timestamps, digests, sentinels, or limb values. This construction uses SHA-256, not H(domain, x...), and therefore adds no AFP-KDC field-domain tag.

The root class MUST be one of the three literal values above. The root-authority identifier MUST resolve, under the verifier's authenticated verification-policy digest, to trust material the verifier accepted before receiving the proof. It is not a prover-selected display name.

For an organizational attestation root authority — the case root:publisher-manifest presents, and the case any KERI-rooted authority presents under any class — the authority is the accountable party that stands behind the attestation, identified by its organizational root AID, never a product, brand, build line, or software distribution. For such an authority, a root-authority identifier naming a product rather than the party accountable for it MUST NOT be accepted. In root:tee-attestation, the identifier instead names a hardware vendor's product-root policy resolved from the verifier's pinned vendor-root configuration. In root:build-provenance, it names a transparency log's checkpoint signer under the verifier's accepted log policy. The branch rules below are authoritative for those two meanings.

An organizational authority MAY delegate product-scoped signing: the signing AID is then a delegated AID whose inception is anchored in the authority's KEL, exactly as an entity is delegated from a tenant root (§4.2), and root-material digest MUST commit to that delegated signing-key state. Rotation, compromise handling, and succession of a delegated signing key MUST run as explicit, published events — the delegated inception and each delegated rotation as events in the signing AID's KEL, each anchored by a seal in the authority's KEL (§4.2, §4.4, §4.6). It is through those anchors that the authority remains the authoritative source for the signing key's state and for the stale/revoked determination below, and this holds for any authority identified by a KERI AID without reference to any registry, advisory feed, or vendor document. For root:build-provenance and root:tee-attestation the equivalent determination is made against the log-policy and pinned vendor-root material fixed in those branches. What holds for every class, without exception, is that no authority-published feed is ever on the normative path. The binding between an authority AID and the legal entity it denotes is established out-of-band, at the moment a verifier accepts that authority's trust material into its verification policy; AFP defines no on-wire representation of that binding and no protocol step that establishes it.

root-material digest commits to the exact accepted transparency-log checkpoint, hardware endorsement/collateral set, or publisher signing-key state used for this proof. attestation-evidence digest has the branch-specific meaning fixed below and commits to evidence without exposing the raw provenance bundle, TEE quote, or signed manifest. The separately versioned ZKC machine build/runtime credential schema defines the provider-neutral claim consumed here; AFP binds that claim to these public inputs and does not redefine the credential schema or issuer-adapter stages.

The v2 circuit MUST prove all common properties: the prover holds an authentic, unexpired machine build/runtime credential and its holder binding; the credential's machine subject equals the KERI-authenticated entity or instance AID; the instance signing key is controlled by that subject; the hidden manifest opens to attested manifest digest; the hidden root evidence opens to attestation-evidence digest; the credential claim, root evidence, and manifest agree on the root class, root authority, machine subject, and manifest; and the proof transcript binds every v2 public input plus the active §5.9 presentation profile. It MUST then enforce the branch below for the declared root class. A verifier MUST reject an unknown class, a class/authority mismatch, a stale or revoked authority, a branch proof produced for another class, or a nonzero/zero public-sample-binding digest that violates that class's rule. It MUST also reject a root-material digest absent from its accepted policy for root:build-provenance and root:tee-attestation, where that digest names an externally pinned checkpoint or vendor-collateral set. For root:publisher-manifest the accepted policy binds the authority AID, not a key-state digest: root_material_digest is a per-key-state value that changes at every published rotation, and it is validated by recomputation from the resolved KEL state under the publisher-key-state rule above rather than by presence in a pre-signed list. A verifier's policy therefore does not have to be re-signed on every publisher rotation, and a policy that does additionally pin a specific publisher key-state digest pins that key state and nothing more.

Two clarifications govern that list where signing is delegated. Agreement on the root authority is agreement on the identifier value: the credential claim, root evidence, and manifest MUST agree on the root-authority identifier the proof declares, and the circuit is not required to prove the delegation chain behind it — the verifier resolves that chain out of circuit before acceptance, as stated above. For a KERI-rooted authority, staleness and revocation are resolved from its KEL: where the root-authority identifier is a KERI AID — the organizational case, and the only case root:publisher-manifest admits — a verifier MUST determine that authority's current key state, revocation, and succession from the authority's KEL and the KELs it anchors, under §4.2, §4.4, and §4.6, and MUST reject a proof whose committed signing-key state is superseded by a published rotation, whose delegated signing AID's delegation has been revoked, or whose authority root key state the verifier can no longer resolve. This parallels the kel_keystate_stale rejection of §12.3. For root:build-provenance and root:tee-attestation — whose authorities are a transparency log's checkpoint signer and a hardware vendor's product root, neither of which is required to be a KERI AID with a KEL — the same determination is made instead against the checkpoint-signer key and rotation state under the verifier's accepted log policy, and against the verifier's pinned vendor root configuration with its collateral freshness and revocation state, exactly as those branches already fix below. In every class the material is either self-certifying KERI state or material the verifier itself pinned; in no class is an authority-published feed on the normative path.

Authority-KEL integrity is not assumed. An authority is not a tenant (§2.1), so the §4.5 tenant-scoped witness duties do not attach to it of their own force. A verifier MUST therefore verify authority-KEL events — and the KEL events of any AID that authority anchors — against a witness set and threshold pinned in the authority trust material it accepted into its verification policy, applying the receipt-threshold rule of §4.5. A KEL that does not meet the pinned threshold, or that forks against events the verifier previously accepted, is treated as unresolvable authority key state and routes to exactly the consequence stated below, never to any other.

An authority MAY additionally publish an advisory or status feed covering its own key state and revocation status as a convenience; such a feed MUST NOT enumerate, rank, endorse, or report on any other authority (§1.2). No verifier is required to consult one, and no such feed is a normative path for discharging this obligation.

The consequence of any authority-related failure — an authority the verifier does not accept, an authority found stale or revoked, an unresolvable delegation, or unresolvable authority key state — is exactly the consequence of any other failure in this section and nothing more: in tenant→operator telemetry an alert under §6.6, and in every counterparty-facing use — a §5.4/§5.5 bilateral interaction, open federation (§5.8), and the introduction tier (§14.3) — declining the interaction and nothing beyond it. A rejected, stale, or revoked authority MUST NOT gate, quarantine, freeze, or otherwise act upon tenant state, a tenant's funds or notes, or any prior session, and MUST NOT force a downgrade of tenant state (Safeguards 3 and 5). A §14.7 contraction of the go-forward envelope is a form of declining to extend further credit and is unaffected by this sentence; it touches no prior session and no tenant state.

The operator-facing predicate catalog remains closed. Safeguard 1 permits each bilateral verifier to choose which of the three registered root classes and which authorities it accepts; it does not permit an implementation to invent a fourth class or accept an unregistered telemetry predicate. Extending the root-class taxonomy requires a revision to this section.

Standing up a new root authority does not. The set of authorities is open and unenumerated: any organization MAY stand as an authority under any of the three classes, on the identical terms this section states, and does so by publishing trust material a verifier can accept — not by registration with, permission from, notification to, or a grant by any party, and not by an amendment to this specification. Whether a given verifier accepts a given authority is that verifier's policy decision alone (Safeguard 1, §9). AFP maintains no authority registry and defines no procedure for admitting, ranking, suspending, or removing an authority. Naming an authority in this specification records an instance of the construction; it neither privileges that authority nor closes the set.

Shipped defaults are product configuration (normative). An implementation MAY ship a default verification policy naming authorities it recommends. Such an entry MUST be visible to, editable by, and removable by the deployment operating it; it MUST NOT be required for conformance, and an implementation MUST NOT hardcode an authority entry a deployment cannot remove. It is product configuration and not a default of this specification: this specification defines no default authority and no default (root class, root authority) pair. A shipped entry applies to an interaction only once the party operating the deployment signs it into that interaction's policy (§5.5, §14.4). The §9 Safeguard 1 and §12.8 rows state how this bears on the safeguards; this paragraph is where the obligation itself lives.

Non-normative — the first named authority. None of the following is yet operational. Pyramidal Inc.'s organizational root AID has not been incepted, and no AID value is asserted here or anywhere else in this specification. What follows records the intended instantiation of the construction above so that the normative text can stay authority-agnostic, exactly as ADAMAS is cited as the reference implementation without becoming the definition (§1.5.1, §2.1). Nothing in this note is a requirement of this specification; every requirement it alludes to is stated in the normative text above and binds there, not here.

  • Pyramidal Inc. is the first named attestation root authority. For proofs it stands behind, root_authority_identifier is intended to resolve to Pyramidal Inc.'s organizational root AID — the accountable legal entity, not the ADAMAS product.
  • ADAMAS build manifests are to be signed by a delegated, product-scoped ADAMAS publisher AID whose inception is to be anchored in Pyramidal's KEL, mirroring the tenant→entity delegation of §4.2. root_material_digest would then commit to that delegated signing-key state as afp:publisher-key-state:v1 above, with rotation, compromise handling, and succession of the publisher key running as explicit events in the publisher AID's KEL, each anchored by a seal in Pyramidal's organizational KEL.
  • The ADAMAS product line attests under root:publisher-manifest and is therefore self-attested at the platform layer, with the mandatory §6.4 public-sample binding and both halves of the disclosure duty above. Pyramidal attests only ADAMAS builds it produces; a fork or third-party distribution stands up its own authority under the same rules.
  • Once incepted, Pyramidal Inc.'s organizational root AID is intended to ship as the default entry in ADAMAS verification policies. That would be a shipped product configuration — visible, editable, and removable by the deployment operating it, per the normative paragraph above — never a default of this specification and never a requirement of it (§9, §12.8, §14.4).
  • The AID↔︎legal-entity binding would be established out-of-band when a verifier accepts Pyramidal's trust material, per the rule above.
  • Because the authority's KEL and published trust material do not yet exist, the operational-status disclosure stated normatively above applies today to any deployment selecting this root path, and §14.9 applies additionally where that deployment runs the introduction tier.
  • Pyramidal Inc. is also an AFP operator and is constrained as one by §8.6. The two roles are independent in both directions; neither transfers trust to the other.

6.4 Public-sample consistency

A self-attested claim is only as good as its resistance to a lying tenant. AFP requires that each self-attested predicate be paired with a public sample the verifier can check the signed assertion against — for example, an aggregate count or a digest the verifier independently holds — and that the proof demonstrate cryptographic consistency between the signed assertion and the sample.

For a root:publisher-manifest proof (§6.3.1), the mandatory public-sample binding is produced and signed by the tenant proving its own instance, as this section's artifact requires; it is not produced or signed by the attestation authority, and the authority's separate duty to characterize its own attestation (§6.3.1) is discharged in that authority's published trust material rather than here.

The consistency artifact is a ZKC proof over a tenant-signed sample binding:

interface AfpTelemetrySampleBinding {
  kind: 'afp:telemetry-sample-binding:v1';
  tenant_aid: string;
  predicate_id: string;                 // ZKC predicate identifier from §6.3
  predicate_version: string;
  telemetry_window: {
    starts_at: datetime;
    ends_at: datetime;
  };
  assertion_commitment: string;          // commitment to the private self-attested value
  sample_kind: 'aggregate' | 'selective-reveal' | 'external-digest';
  sample_commitment: string;             // commitment to the verifier-checkable sample
  sample_policy: {
    min_population?: number;             // REQUIRED for aggregate samples
    differential_privacy?: {
      epsilon: number;
      delta: number;
    };
    revealed_fields?: string[];          // REQUIRED for selective-reveal samples
    external_source_aid?: string;         // REQUIRED for external-digest samples
  };
  sample_context_hash: string;           // hash of cadence, operator, and predicate context
  issued_at: datetime;
  tenant_signature: string;
  zkc_proof: string;                     // proves consistency without revealing hidden values
}

The zkc_proof MUST prove all of the following:

  1. the prover controls tenant_aid and signed the binding;
  2. assertion_commitment commits to the same private value used by the self-attested telemetry predicate;
  3. sample_commitment commits to a verifier-checkable sample whose declared sample_kind and sample_policy are satisfied;
  4. the assertion is included in, bounded by, or equal to the public sample according to the predicate-specific rule; and
  5. the proof binds predicate_id, predicate_version, telemetry_window, sample_context_hash, and the operator-facing telemetry subject so the sample cannot be replayed across predicates, operators, or windows.

v2 counterparty mapping (normative). The afp:telemetry-sample-binding:v1 field names remain frozen for telemetry compatibility. For v2 sample bindings, including mandatory root:publisher-manifest bindings used in §14, telemetry_window MUST equal the v2 attestation_window; the operator component inside sample_context_hash MUST be replaced by verifier_aid; and the operator-facing telemetry subject component MUST be replaced by the exact AFP introduction session context. Requirement 5 above is evaluated with those substitutions, so the binding cannot replay across counterparty verifier, APR, session, or window. The binding's tenant signature and proof context MUST also bind afp.telemetry.instance_attestation.v2, its active §5.9 profile, and the selected root class/authority. These mappings do not change the v1 artifact's wire fields and MUST NOT be applied to frozen instance_attestation.v1.

The allowed sample forms are intentionally narrow:

Under §3, the operator has no per-tenant transport observation to use as a sample. A public sample for a self-attested predicate MUST therefore be one of the forms above; it MUST NOT be derived from operator surveillance of tenant traffic, message timing, counterparty identity, coordination volume, or any other coordination-plane observation.

If the sample binding is missing, expired, inconsistent with the predicate, or uses a sample form outside this section, the proof fails. In tenant→operator telemetry, §6.6 applies: the result is an alert only, never operator intervention in tenant state. In §14 counterparty use, the verifier declines the introduction interaction under §14.3; it MUST NOT form the provisional agreement, gate tenant state, or impose any other consequence.

6.5 Push model and cadence

Telemetry is tenant-push. A tenant publishes telemetry proofs on a telemetry subject the operator subscribes to; the tenant controls timing. The operator monitors cadence — a missing proof within an expected window is itself a meaningful operational signal — but cadence is established by the presence or absence of pushed proofs, not by transport observation of the tenant's other traffic.

6.6 Failure semantics

A failed or missing telemetry proof MUST NOT trigger any automated operator intervention in tenant state. Per Safeguard 3 and the operator-blindness invariant, the operator has no such capability and the protocol grants none. A telemetry-proof failure propagates as an alert on a separate operator-monitored subject; human investigation follows. This is the structural difference between AFP operator telemetry and a ZKC v0.4.1-draft compliance gate: a gate is allow-or-deny; AFP telemetry is observe-and-alert. AFP defines no gate over tenant state for any party.

6.7 Topology independence

The tenant↔︎operator protocol surface is identical whether the tenant runs on a managed transport or a self-hosted appliance. In every topology, the operator learns only what the tenant proves. There is no deployment in which the operator learns more by observation; §3 forbids it. The appliance case is therefore not a privileged tier for telemetry — it is the same protocol — and the managed-transport case is held to the same structural standard.


7. Deployment Topologies

7.1 Auditable implementation

Every topology in this section is conformance-relevant only to the extent that the software realizing it can be checked: operator blindness is a claim about capability (§3.2), and a capability claim is verifiable only against an implementation open to inspection. This subsection states that requirement; it applies to §7.2 and §7.3 alike. Two consequences are normative:

An implementation that cannot support inspection of its OB-relevant behavior MUST NOT claim OB as a capability (§3.2). Such an implementation falls under the honest-disclosure duty of §3.5: it MUST disclose the limitation to its tenants rather than represent OB as satisfied. Inability to support inspection is a limitation of the implementation, never a lesser standard of conformance — §3.2 admits no substitute for structural verifiability.

7.2 Appliance

An appliance is a self-hosted deployment of a conforming tenant that does not connect to an operator-run transport — not a distinct protocol target. For the reference implementation, it is offered as a hardware recommendation plus a licensed ADAMAS image and support plan. An appliance tenant runs the same AFP 0.3.12-draft interface as a managed-transport tenant. The appliance reaches the rest of the federation through the §8 routing layer; it does not connect to an operator-run transport that could observe it. Identity migration to or from an appliance is the one-move operation of §4.7.

7.3 No operator-run coordination transport

AFP does not define a deployment in which an operator runs the transport that tenant↔︎tenant coordination traffic transits. Such a deployment cannot satisfy OB-2 (§3.3) and is not a conforming topology. Operators may run telemetry-subject infrastructure (§6.5) and may participate in the §8 routing layer as one relay among several, but no operator may occupy a position from which the coordination graph is observable.


8. Metadata-Privacy Routing

This section delivers OB-2, the coordination-graph privacy of §3. It states the normative requirement (§8.1), the conformance properties any routing layer must satisfy (§8.2), the AFP Federation Mixnet (§8.3–§8.7), private route resolution (§8.8), the relationship to honest disclosure (§8.9), and residual limitations (§8.10). The §8.2 properties are the conformance test; §8.3–§8.8 define AFP's baseline construction. An implementation MAY use another construction, but it MUST satisfy §8.2.

The threat model is explicit and was decided deliberately: the OB-2 adversary includes a passive observer of the network links between relays — a transit provider, an infrastructure operator, or a national-scale link observer — not merely a curious party running one piece of infrastructure. AFP's flagship use case is cross-jurisdictional collaboration between identifiable institutions; the existence and intensity of a coordination relationship is itself commercially sensitive, sometimes materially so, and the platform cannot ask a tenant to predict in advance which relationships are sensitive. The routing layer is therefore designed to defeat statistical traffic analysis by a global passive adversary, not only single-relay correlation.

8.1 Requirement

Tenant↔︎tenant coordination traffic MUST be routed such that no single transport operator, no single relay, no colluding minority of relays, and no passive observer of the links between relays can learn the pairing (tenant A AID, tenant B AID) for any coordination session, nor reconstruct it from timing or volume correlation.

8.2 Properties the routing layer must provide (conformance test)

A conforming routing layer MUST provide:

  1. Endpoint unlinkability. A relay forwarding a message sees, at most, one hop in each direction — never both the originating and terminating tenant of a session.
  2. No cleartext relationship in addressing. Routing headers and any identifier visible to a relay MUST NOT encode the tenant pair, and MUST NOT be stable across sessions in a way that lets a relay link a tenant over time.
  3. Correlation resistance. Timing and volume patterns MUST be treated so that a relay, a colluding minority of relays, or a passive observer of inter-relay links cannot reconstruct the pairing by correlation. This is the discriminating property and the reason a mixnet, not a simpler relay, is required.
  4. No single trusted relay. The layer MUST NOT contain a relay whose compromise reveals the graph. This is the §3.2 capability standard applied to the routing layer itself: decentralization of the relay set is required, not optional.

8.3 The AFP Federation Mixnet

AFP's baseline satisfies §8.2 with a self-contained, consortium-operated mix network of the Loopix family. "Self-contained" means the mixnet is operated by and for the federation; it is not a dependency on any public, token-incentivized, third-party anonymity network. "Loopix family" means the design class characterized by the Sphinx packet format, a stratified mix topology, continuous-time (exponential-delay) mixing, and mandatory cover traffic — the class with formal, published anonymity bounds against a global passive adversary.

Reference implementation. The preliminary reference implementation is Katzenpost (katzenpost/katzenpost, AGPLv3), a Loopix-family mixnet using client-selected exponential mixing delays, a stratified topology, configurable-geometry Sphinx packets, post-quantum cryptographic agility, and a decentralized directory-authority system. Katzenpost is the reference implementation for development, not a frozen normative dependency; it is evolving toward the Echomix design and has no confirmed large-scale production deployment as of this writing. Accordingly, §8 binds to the Loopix-family architecture (§8.2 properties), not to a specific Katzenpost version. An implementation tracks a named Katzenpost release; the AFP conformance test remains §8.2.

Reference maturity rule. A deployment that uses Katzenpost MUST publish the exact release, commit, configuration profile, and any local patches in its AFP conformance statement. Echomix is tracked as the expected next-generation Katzenpost protocol, but it is not a normative AFP dependency until AFP publishes a successor mixnet profile. A deployment MAY pilot Echomix-derived behavior only if the deployed behavior still satisfies §8.2 and the conformance statement identifies the deviation from afp.mixnet.profile.ob2-baseline.v1.

Why a mixnet and not a lighter relay. Simpler designs — VPNs with rotating exits, onion-routed relays without mixing, or queue-relay systems without cover traffic — defeat single-relay and IP-level correlation but do not defeat §8.2 property 3 against a link observer: a burst of real traffic leaving tenant A still correlates, by timing and volume, with the burst arriving at tenant B, regardless of how the source address is obscured. Only mixing delay plus cover traffic breaks that correlation. This is the property that selects the Loopix family.

8.4 Topology and packet format

The AFP baseline implementation profile is afp.mixnet.profile.ob2-baseline.v1. A deployment MAY define a stricter profile, but it MUST disclose any deviation and still satisfy §8.2.

Parameter ob2-baseline-v1 value Conformance rule
Packet format Sphinx, fixed-size packets Every coordination packet has identical observable size and form.
Hop geometry 3 mix hops, one hop per layer A deployment MUST NOT use fewer than 3 hops for OB-2 claims.
Layers 3 stratified layers No operator may control all nodes in any layer or enough adjacent-layer positions to observe a full path.
Payload class 4 KiB encrypted AFP payload fragment per packet Larger AFP messages are fragmented and padded into indistinguishable packets.
Per-hop delay Exponential delay, mean 30 s The delay is independently sampled per hop by the sender.
Maximum packet TTL 15 min Expired packets are dropped without revealing whether they were real or cover packets.
Tenant cover rate At least 1 packet every 10 s per federated tenant Real traffic is slotted into the same schedule; deployments may raise but not lower the rate.
Mix-node loop cover At least 1 loop packet every 10 s per mix node per adjacent layer Loop cover is independent of tenant activity.
Directory epoch At most 24 h Tenants MUST reject expired directories unless an emergency extension is KERI-witnessed.

8.5 Mandatory cover traffic

Cover traffic is REQUIRED, not optional, and is the property on which OB-2 against a link observer depends.

8.6 Relay operation — no single trusted relay

8.7 Mix-node directory

The set of mix nodes, their layer assignment, and their public keys MUST be published as a directory that every tenant can verify without trusting any single party.

The directory document has the following verifier-facing shape:

interface AfpMixnetDirectory {
  kind: 'afp:mixnet-directory:v1';
  profile: 'afp.mixnet.profile.ob2-baseline.v1';
  epoch: number;
  valid_from: datetime;
  valid_until: datetime;
  keri_anchor_event_dig: string;
  sphinx: {
    packet_payload_bytes: 4096;
    hop_count: 3;
  };
  mixing: {
    delay_distribution: 'exponential';
    mean_delay_seconds: 30;
    packet_ttl_seconds: 900;
  };
  cover: {
    tenant_packet_interval_seconds: 10;
    mix_loop_interval_seconds: 10;
  };
  layers: Array<{
    layer_index: number;
    nodes: Array<{
      mix_node_aid: string;
      operator_aid: string;
      endpoint: string;
      sphinx_public_key: string;
      directory_signing_key: string;
      software_ref: string;
    }>;
  }>;
  quorum_signatures: Array<{
    witness_aid: string;
    signature: string;
  }>;
}

A tenant MUST reject a directory if the KERI anchor is absent, the witness quorum is below the federation threshold, the profile parameters are weaker than ob2-baseline-v1, any layer is empty, or the operator-control constraints of §8.6 are violated.

8.8 Private route resolution

§5.2 step 1 requires an initiating entity to resolve a counterparty tenant AID to a transport route. This subsection specifies how, without any party learning the pairing.

8.9 Relationship to OB and to honest disclosure

§8.3–§8.8 are the mechanism by which OB-2 (§3.1) is satisfied. OB-1 (content) does not depend on this section — it is delivered by §5.2 channel encryption in every topology. A deployment whose mixnet is not yet operational, or which cannot yet run mandatory cover traffic across a sufficient mix-node set, does not satisfy OB-2 and MUST disclose this per §3.5: such a deployment claims OB-1 and states OB-2 as not-yet-met. Because OB-2 has little value at very small federation sizes (with few tenants there is almost no coordination graph to hide) and accrues value as the federation grows, an honest-disclosure interim during early deployment is consistent with this specification.

8.10 Residual limitations

Two limitations MUST be disclosed honestly rather than overclaimed:


9. Freedom Safeguards

AFP 0.3.12-draft inherits the six Freedom Safeguards defined normatively in ZKA v0.9.2-draft §1.6. They are not re-declared here; this section states only how each bears on AFP.

# Safeguard Bearing on AFP
1 Predicate Pluralism AFP carries ZKC compliance bundles (§5.4) but privileges no issuer or predicate set. A tenant's compliance requirements in a federation agreement (§5.5) are that tenant's bilateral choice; AFP defines no canonical or default set — of issuers, of predicates, of root classes, or of root authorities (§6.3.1). An implementation MAY ship a default verification policy naming authorities it recommends; that is implementation-side product configuration, and §6.3.1 states normatively that such an entry must be visible to, editable by, and removable by the deployment operating it and is not a default of this specification. Naming an authority in this specification privileges it in no way.
2 Minimal Disclosure Every selective disclosure and every telemetry claim (§6.3) is atomic — one property per proof. AFP message classes (§5.3) carry single-predicate proofs; composite disclosure requires explicit per-predicate consent.
3 No Revocability AFP defines no gate over tenant state for any party. Operator telemetry failure is observe-and-alert, never intervene (§6.6). No AFP message can invalidate, freeze, or redirect a ZKA note.
4 Open-Source Predicates Telemetry predicate circuits (§6.3) MUST be open-source, versioned, and published with a public review period, exactly as ZKC §1.4 requires of any predicate.
5 Agent Exit Rights A tenant's withdrawal of its assets to a base layer is a ZKA operation gated by nothing in AFP. Leaving a federation, leaving a managed transport for an appliance (§4.7), and changing operator are all unconditional. AFP adds no exit gate.
6 Verifier Accountability The first safeguard to constrain the counterparty rather than the operator, and load-bearing for open federation (§5.8). A tenant that requests a compliance proof is a verifier: it MUST authenticate the request under a stable KERI identity, and the holder retains a profile-minimized Disclosure Receipt of what was demanded, by whom, and for what purpose (ZKC v0.4.1-draft §5.5–§5.9; AFP §5.9). Anonymous demands are rejected; global linkability requires explicit consent.

Operator blindness (§3) is an AFP-level invariant in the same spirit as the Safeguards: a structural guarantee, verifiable by inspection (§7.1), not a policy promise.


10. Open Items

The following seams are not defined by the current AFP interface. Implementations MUST respect the stated boundary and MUST NOT silently substitute a local mechanism as though it were AFP.


11. Canonical Specification

This document is the sole normative AFP specification. Other AFP drafts, scope notes, migration notes, and implementation documents are non-normative unless a current section explicitly incorporates them by reference. An implementation MUST NOT infer wire semantics, conformance requirements, or exceptions from those materials. If another AFP document conflicts with this specification, this specification controls.


12. Bilateral Session Core

This section group is normative and defines AFP's shared bilateral-session substrate once: two verified instances open a session, bind it to KERI identities, exchange signed atoms under bilateral policy, resolve disputes bilaterally with an optional pre-agreed arbiter, commit with both signatures and an optional DLT anchor, and inherit the Freedom Safeguards. AFP's §5 tenant↔︎tenant work exchange uses this core; PRP and VXP are profiles (§13) over it.

The core composes §4 identity, §3 operator blindness, §8 routing, and §9 safeguards into the session abstraction that profiles consume. Section 12 is the canonical profile-facing statement of shared session constructs; §5 applies them to work exchange.

12.1 The bilateral session

A bilateral session is the unit of all AFP-mediated interaction between two verified conforming instances (§1.2). Where — and only where — a party requires instance attestation, "verified" is verified under a root class and root authority that party accepts (§6.3.1); trust basis (a) below imposes no attestation requirement of itself — whether the agreement carries one is bilateral (§5.5) — and this section imposes none. It is, by construction:

A profile (§13) does not define a new session; it runs inside a bilateral session, contributing its own atom kinds and state machine while the session establishment, identity, transport, dispute, commitment, envelope, and safeguard machinery are the core's. The introduction tier (§14) is the one place a profile is permitted to drive session opening itself, and only because closing the stranger-onboarding gap requires it; the carve-out is bounded to that profile and stated normatively in §14.2.

12.2 Session establishment, lifecycle, and timeout

12.2.1 The session atom

A bilateral session is durable, queryable AKG state, represented by a core SessionAtom (afp:session:v1). A profile MAY specialize/extend this atom (PRP's prp:session:v2 and a VXP transaction context are profile specializations), but every profile session carries the core fields:

interface AfpSessionAtom {
  kind: 'afp:session:v1';
  id: string;                       // session id; component of the session context (§5.2 step 4)

  // Principals — KERI-aligned (§4). The session is between two ENTITY delegated AIDs,
  // each verified up to its TENANT root AID.
  initiator: AfpPartyRef;
  responder: AfpPartyRef;

  // Governing bilateral federation agreement (§5.5) and the profile this session runs.
  agreement_ref: string;            // AFP bilateral federation agreement id
  agreement_version: string;        // pinned at session open
  profile: string;                  // registered profile name (§13.2), e.g. 'prp' | 'vxp' | 'work'
  profile_policy_hash: string;      // hash of the profile policy block exchanged at open

  // Lifecycle (§12.2.2)
  status: AfpSessionStatus;

  // Timing / timeout (§12.2.3)
  opened_at: uint64;                // §5.2: RFC 8785/JCS JSON integer token, 0..253402300799; exact Unix seconds
  expires_at: datetime;             // hard session timeout; REQUIRED
  closed_at?: datetime;

  // Binding
  session_context: string;          // exact SHA-256 / X_ construction in §5.2
}

interface AfpPartyRef {             // the canonical session identity binding (§12.3)
  tenant_aid: string;               // §4 tenant root AID (self-certifying)
  entity_aid: string;               // §4 entity delegated AID — the acting principal
  kel_event_dig: string;            // KEL event the signing key is pinned to (key-state pinning)
  signature: string;                // KEL-anchored signature over the atom content hash
}

enum AfpSessionStatus {
  PROPOSED = 'proposed',            // open requested, not yet accepted
  OPEN     = 'open',                // mutually authenticated, channel established
  ACTIVE   = 'active',              // profile exchange in progress
  CLOSING  = 'closing',             // graceful teardown initiated
  CLOSED   = 'closed',              // completed normally
  EXPIRED  = 'expired',             // expires_at reached before close
  ABORTED  = 'aborted'              // torn down on error or by either party's exit (§12.8, Safeguard 5)
}

Both initiator and responder are REQUIRED in every afp:session:v1 wire record. They are identity bindings, not late-added fields. The party-reference objects are independently authenticated against their KELs and are not themselves members of the stable session-content hash; this retains the existing 0.3.10-draft signature preimage and avoids making either signature cover itself. Each signature covers the same stable session identity, including session_context, while §5.2 makes that signed digest a commitment to the exact two entity AIDs carried by the party bindings. A recipient establishes the binding by authenticating those references and recomputing the context; no acceptance transition may replace, omit, or reinterpret either reference.

signature has one narrowly defined proposal sentinel: in PROPOSED, initiator.signature MUST be a non-empty KEL-anchored signature and responder.signature MUST be exactly the empty string. Before producing that proposal, the initiator MUST authenticate the responder's tenant root, delegated entity AID, current kel_event_dig, and declared witness threshold through its KEL/delegation chain and place those exact identity values in responder; it then computes session_context from the two authenticated entity AIDs, ID, and opened_at, and signs the stable content containing that context. A recipient MUST first verify the initiator signature and its declared witness threshold, then verify that the unsigned responder binding exactly names its own authenticated tenant root, delegated entity AID, and current KEL event. A mismatch, an absent responder binding, or any responder proposal signature other than the exact empty string MUST be rejected. Only after those checks MAY the recipient recompute session_context from the atom's authenticated party bindings, ID, and opened_at; it MUST reject a mismatch.

On acceptance (OPEN), the responder MUST preserve every stable-content value and both unsigned party bindings byte-for-byte, replace only its empty signature with its own KEL-anchored signature over the same stable content hash, and set status to OPEN. An OPEN or later session MUST carry two non-empty, valid party signatures; an initiator confirming acceptance MUST reject a changed echoed initiator reference, responder binding, content hash, session context, or responder signature. These checks reject replay of a proposal to a different responder even where transport routing delivers it.

The profile field is recorded in the session so the transport layer and the counterparty know which atom kinds and state machine govern the exchange. afp:session:v1 carries no profile-specific fields; those live in the profile's own atoms.

12.2.2 Lifecycle

The core session lifecycle is the part of every profile's state machine that concerns the session itself, distinct from the profile's exchange state machine (which the profile defines over ACTIVE):

 propose ──▶ PROPOSED ──accept──▶ OPEN ──begin──▶ ACTIVE ──close──▶ CLOSING ──▶ CLOSED
                │                                     │                          ▲
            reject/timeout                   either-party exit (§12.8 SG-5)      │
                │                                  or error                      │
                ▼                                     ▼                  profile reports done
            ABORTED                                ABORTED ──────────────────────┘
   (EXPIRED reachable from any non-terminal state when expires_at passes)
  1. Propose / accept. The initiator proposes a session under a named profile and a pinned federation-agreement version using the responder-binding and signature invariants of §12.2.1; the responder accepts (→ OPEN) or rejects (→ ABORTED). Acceptance completes the mutual authentication and channel establishment of §5.2.
  2. Activate. On the first profile exchange message the session moves to ACTIVE. The profile's own state machine (PRP Compare→…→Commit; VXP Intent→…→Settlement) runs entirely within ACTIVE.
  3. Close. When the profile reports its exchange complete, the session moves CLOSING → CLOSED. A core CommitmentAtom (§12.6), where the profile produces one, is the artifact that justifies a clean close.
  4. Abort / expire. Either party MAY exit at any time (§12.8, Safeguard 5) — the session moves to ABORTED, leaving whatever signed atoms exist as the durable record. expires_at elapsing moves any non-terminal session to EXPIRED. No core transition can be forced by an operator (§12.8, Safeguard 3).

A profile MAY name additional terminal annotations for its exchange (e.g. PRP ESCALATED); those describe a session that closes or aborts at the core level — the core recognizes only the states above.

12.2.3 Timeout

Every session MUST carry an expires_at. A session that has not reached a terminal state by expires_at is EXPIRED; a fresh session must be proposed to continue. Profiles MAY set finer-grained per-phase deadlines (PRP evidence_deadline_seconds, VXP offer valid_until) within the federation agreement's policy block, but the core session timeout is the outer bound and is non-optional. Timeout is observe-and-resolve, never operator intervention: an expired session simply ends; nothing in tenant state is frozen or rolled back (§12.8, Safeguard 3).

12.3 Identity binding for sessions

Session identity binding is §4 applied to the session, stated once here so profiles cite it rather than re-derive it:

A profile's party reference (PRP PartyRef; VXP created_by: EntityAID plus the §1.3 derivation) MUST use AfpPartyRef; a profile carries no additional identity machinery.

12.4 NATS namespace ownership

AFP owns the federation subject tree. No profile defines a standalone namespace. The canonical tree is:

afp.{tenant}.{profile}.*

where {tenant} scopes the subtree to a tenant and {profile} is a registered profile name (§13.2). Concretely:

afp.{tenant}.work.*            # base work-exchange behavior (the §5.3 message classes)
afp.{tenant}.session.*         # core session lifecycle (propose / accept / close / status)
afp.{tenant}.prp.*             # PRP profile subtree
afp.{tenant}.vxp.*             # VXP profile subtree
afp.{tenant}.telemetry.*       # tenant↔operator telemetry (§6.5) — operator-facing, not a bilateral profile

Normative rules:

  1. A profile's subjects MUST live under afp.{tenant}.{profile}.* and nowhere else. Standalone top-level namespaces such as bare vxp.* or prp.{org}.* are non-conforming.
  2. A profile name occupies exactly one {profile} label and MUST be registered (§13.2) before its subtree is used.
  3. The subjects MUST remain metadata-private under §8: the {tenant} label and any subject string visible to a relay is carried only inside Sphinx-wrapped packets and MUST NOT be a stable cleartext identifier of the tenant pair (§8.2 property 2). The tree above is the logical namespace; its on-the-wire form is the routing layer's concern.

12.5 Bilateral dispute and arbiter model

This dispute model is part of the bilateral core and applies to every profile.

Primary — bilateral-only, no operator mediation. A dispute within a bilateral session is resolved bilaterally, between the two principals, or not in-protocol at all. There is no operator-mediated dispute: the operator of any AFP transport cannot adjudicate, freeze, reverse, or force any outcome. This is Safeguard 3 (No Revocability) made concrete at the session layer (§12.8): a core CommitmentAtom (§12.6) is valid only with both parties' KEL-anchored signatures; no admin, governance, or operator key can produce or invalidate one. When the two parties cannot agree in-protocol, the session aborts/escalates with a signed divergence record (the profile's dispute atoms) as the durable artifact, and the parties resolve off-protocol — through their own entities, then the federation agreement's governance process — and, failing that, sever the relationship. Exit is unconditional (§12.8, Safeguard 5).

Optional — a pre-agreed third-party arbiter. A federation agreement (§5.5) MAY name, in advance, a single arbiter — a third entity AID, itself a verified conforming instance (§1.2) — to break a tie. The two parties decide bilaterally what verification they require of the arbiter, exactly as for any counterparty; this section imposes no instance-attestation requirement on an arbiter and gives no attestation authority any standing over a dispute. The arbiter:

For a monetary leg (a VXP concern), an arbiter MAY act as a co-signer on a pre-agreed 2-of-3 escrow release — able to break a tie only within the funds the parties escrowed, never against base-layer custody (Safeguard 5). This is the one arbiter capability that touches value, and it is bounded by what the parties escrowed in advance.

There is no global arbiter registry, no min_tvf_level, no capability query, and no random-from-pool selection (§1.2). A profile MAY add profile-specific escalation steps (PRP's entities→governance ladder, VXP's per-leg breach conditions) on top of this model, but the bilateral-only primary path, optional pre-agreed arbiter, and no-forced-outcome guarantee are core requirements.

12.6 Commitment and optional-anchor primitive

Profiles end an interaction with a bilaterally signed commitment to an agreed outcome, chained for replay resistance, with an optional DLT anchor. The shared primitive is:

12.6.1 Bilateral commitment

A core CommitmentAtom (afp:commitment:v1) is the record that two principals agreed to an outcome:

interface AfpCommitmentAtom {
  kind: 'afp:commitment:v1';
  id: string;
  session_ref: string;                 // the afp:session:v1 this commits

  outcome_root: string;                // Poseidon2 commitment to the agreed outcome
                                       // (profile-defined: PRP reconciled-state root; VXP settled-legs root)
  profile: string;                     // which profile produced this outcome

  // Bilateral signatures — BOTH required (§12.5, Safeguard 3)
  initiator_signature: AfpCommitSignature;
  responder_signature: AfpCommitSignature;

  // Chaining — replay-resistant ordered history per AID-pair
  previous_commitment_ref?: string;
  sequence_number: number;             // monotonically increasing per AID-pair

  anchor_ref?: string;                 // afp:anchor:v1, if anchored (§12.6.2)

  // Optional pre-agreed arbiter co-signature (§12.5), present only if an arbiter acted
  arbiter_signature?: AfpCommitSignature;
}

interface AfpCommitSignature {
  signed_by: string;                   // tenant/entity AID
  kel_event_dig: string;               // pins key state at signing (§12.3)
  signature: string;                   // Ed25519, KEL-anchored
  signed_at: datetime;                  // observational metadata; not in signature payload
}

Normative properties:

A profile's commitment atom (PRP prp:commitment:v2; a VXP SettlementAtom) is a specialization of this primitive: it MAY add profile-specific fields (PRP bilateral_metrics, VXP legs[]) but MUST carry the bilateral-signature, chaining, and outcome-root machinery defined here.

12.6.2 Optional anchor

Anchoring is optional. The protocol is fully functional with bilateral KEL-anchored signatures alone, which already give a signed, ordered, replay-resistant history. For records whose disputes might later be adjudicated by a party outside the bilateral relationship (a regulator, an auditor), a commitment MAY be anchored to a DLT for an independent, tamper-evident timestamp:

interface AfpAnchorAtom {
  kind: 'afp:anchor:v1';
  id: string;
  commitment_ref: string;

  chain: string;                       // e.g. 'ethereum:arbitrum' | 'ethereum:base' | 'ethereum:optimism'
  via: 'apl';                          // the anchoring rail; the sole registered value is
                                       // APL (ADAMAS Payment Layer). This literal is a
                                       // vendor-named value in a wire format — generalizing
                                       // it is a wire change, tracked as a §10 open item.
  transaction_id: string;
  block_number: number;
  block_timestamp: datetime;

  anchored_data: {                     // hashes only — never atom content
    commitment_hash: string;
    outcome_root: string;
    parties: string[];                 // AIDs, hashed for privacy
    sequence_number: number;
  };
  inclusion_proof: string;             // either party verifies independently
}

A profile's anchor policy (PRP policy.anchor.mode, VXP channel settle_to) selects when to anchor; the anchor atom and the Ethereum-L2-baseline rule are the core's.

12.7 The federation bundle wire format — session envelope over ZKA bundle

AFP uses one bundle ownership rule:

ZKA owns the cryptographic bundle; AFP owns the session envelope.

  • The ZKA Compliance Bundle zka:bundle:v1 (ZKA v0.9.2-draft §5.4) is the cryptographic bundle: it carries the ZKA/Pay transaction proof (where present), ZKC v0.4.1-draft zkc:proof:v2 anonymous/scoped proofs or explicitly-consented linkable zkc:proof:v1 proofs, the applicable identity/presentation binding proof, and the ZKA/Coord recursive work-attestation proof. AFP does not introduce a replacement bundle version.
  • AFP owns the session envelope afp:envelope:v1: the construct that carries a zka:bundle:v1 across a tenant boundary within a bilateral session, binding it to the session context (§12.3) and routing it on the namespace of §12.4. The envelope is session-level framing; the bundle is cryptographic payload.
  • A profile MUST NOT author a separate bundle wire format. It places a ZKA bundle inside an AFP envelope; vxp:coordbundle:v1 and PRP-specific bundle wrappers are non-conforming.
interface AfpEnvelope {
  kind: 'afp:envelope:v1';
  session_ref: string;                 // the bilateral session (§12.2) this crosses within
  session_context: string;             // exact §5.2 X_<64 lowercase hex> digest (§12.3)
  profile: string;                     // profile placing the envelope (e.g. 'vxp' | 'prp' | 'work')

  bundle: ZkaBundle;                   // zka:bundle:v1; metadata.context is the §5.2 BN254 projection
  bundle_version: 'zka:bundle:v1';

  message_class: string;               // which AFP message class / subject this rides (§5.3, §12.4)
  generated_at: datetime;
  expires_at: datetime;
}

Verification is two-layer and unambiguous: AFP verifies the envelope (its canonical X_ session_context matches the live session, the envelope has not expired, and the profile/message_class are valid for the session); ZKA verifies the bundle (every proof inside zka:bundle:v1 — transaction, compliance, binding, coordination — verifies, and its required context bindings are valid). Before invoking ZKA, AFP MUST derive the exact §5.2 canonical nonzero BN254 projection from that authenticated X_ digest. The bundle metadata.context, transaction-level bundle context, binding context, and ZKA/Coord receipt context MUST equal that projected field value in every AFP flow, including pay; malformed, noncanonical, zero, out-of-field, or mismatched values are rejected before the ZKA callback. The pay profile additionally derives a distinct interaction_context from the same projected session digest and the §15.3 quote hash — computed over signature-free quote content, before either signature (§15.4) — without changing afp:envelope:v1 or replacing its original X_ value.

ZKC proof context is the sole deliberate semantic split. For a current AFP 0.3.12-draft presentation, AFP supplies ZKA v0.9.2-draft with the caller-authenticated §5.9 v2 digest and selects ZKA's fixed AFP-presentation ZKC context mode. ZKA then requires every contained ZKC envelope and its verified proof_context public input to equal that value under the fixed profile afp:zkc-presentation-context:v2. The shared-session legacy mode remains available only for explicitly selected older family tuples and uses the same §5.2 field projection as the other ZKA session members; there is no fallback, no silent conversion from v1, and no API that lets a caller pair an arbitrary alternate profile string with an alternate context. AFP, not ZKA, derives the v2 digest because AFP alone owns the authenticated presenter AID, APR verifier AID, scope policy, profile, and trust-flow purpose. The ZKA bundle kind, wire schema, and context field width do not change. No proof system lives in the AFP layer; AFP frames and binds, ZKA proves.

12.7.1 Coord v2 reconciliation and reciprocal-compute integration

For a Coord v2 exchange, AFP's signed X_<32-byte SHA-256> session_context remains the context in afp:session:v1 and afp:envelope:v1. The canonical ZKA bundle.metadata.context and CoordReceipt.context instead encode that same digest as a BN254 scalar: take the 32-byte payload of the X_ digest, interpret it as an unsigned big-endian integer, reduce it modulo the BN254 scalar-field modulus r of §15.4, and serialize the result as lowercase 0x followed by exactly 64 hexadecimal digits. The result MUST be nonzero. A malformed X_ value, invalid field encoding, or zero reduction is rejected; an implementation MUST NOT truncate, substitute, or fall back to another context. Because reduction is many-to-one, the verifier MUST independently authenticate and recompute the exact signed outer X_... context and compare its field reading to both ZKA locations; equality of field readings alone is insufficient. This is a reading of the already-signed digest, not a new AFP wire record, field, or identifier.

The complete receipt shape is the pinned ZKA v0.9.2-draft Coord v2 canonical schema at publication commit 6e484dd07c2ae907f17d33fceeb12eff98ce3cb7: 27 JSON members representing exactly 33 flattened public field elements. AFP neither restates nor extends that ABI. The schema file schemas/public-inputs/zka_coord_v2_recursive.json is pinned by SHA-256 f8e58f59d17406baab44923ed539108167e616d7ce3d87bdc9fd22a87a28e9a1; the carrying schemas/compliance-bundle-v1.schema.json is pinned by SHA-256 fbe249ee089d9d22111ac2f3c7536ee859185ff4693608b44bd101102b78e49e. A different shape, legacy five-field receipt, unknown member, noncanonical field, or opaque coord string is not Coord v2 completion evidence.

The published §5.2 vector fixes the AFP→Coord seam:

session_context       = X_eddb6ad038bd9a1a79c6c1d29ebd7461da0383948b7e6c13dadd6553b97e4a7f
coord_context_field   = 0x2c4a3104b3f719739885aaf898b812ed3933e272a498a9cecb558f03f97e4a7b

Coord v2 acceptance is verifier-owned policy, but that policy is cryptographically bound without a new AFP wire format. CoordAcceptancePolicyV1 is the canonical JSON object whose kind is afp:coord-acceptance-policy:v1 and receiptKind is zka:coord:receipt:v2; it contains afpProfile, the text and field profileId, workflowDefinitionHash, workflowInstanceId, completionStepId, verifierPolicyRoot, acceptedProfileBindings, acceptedArtifacts, the two evidence-presence booleans, and expected initial-state, output, and post-state commitments. Each profile binding contains the exact profileVersion, profileManifestHash, profileVkHash, and assuranceClass. Each artifact binding contains the exact role, circuitId, artifactEntryHash, schemaHash, target, and verificationKeyHash. Both nonempty arrays are ordered by the RFC 8785 canonical bytes of each member and contain no duplicate. All keys are serialized exactly as named here, absent members are forbidden, and the session value is:

profile_policy_hash = "D_" || lowercase_hex(SHA-256(RFC8785_JCS_UTF8(CoordAcceptancePolicyV1)))

The acceptor MUST compare that value to the live mutually signed session before calling a verifier. The verifier and durable completion store are deployment-owned capabilities bound when the receiving AFP instance is configured; neither the envelope sender nor an individual receive call may select or replace them. The acceptor supplies the exact policy, authenticated sender and recipient AIDs, session parties, both context encodings, verification time, and authorized evidence references to that configured ZKA verifier. The same verifier first authenticates the complete canonical zka:bundle:v1 — including every proof and all bundle metadata — and then verifies the Coord final proof, recomputing the workflow and policy roots and authenticating the profile manifest/VK, recursive artifact policy entry/schema/VK, output, state, and evidence. A prover-supplied policy, caller-supplied verifier, caller-supplied replay store, or successful pairing alone is insufficient.

The only supported mappings are:

AFP claim ZKA profile ID profileId field receipt assurance profile application circuit DA / external commitments
PRP reconciliation completion zka:coord:private-reconciliation:v1 0x147446fe0f34833135f89bc9d7aa756cb778f8e8fbd77e570996ff1a8df752e1 zkNative zka_coord_reconciliation DA nonzero; external zero
VXP reciprocal-compute leg zka:coord:vxp-reciprocal-compute:v1 0x1aba8f8267027cdbd3e5ac25dc6767ffaf379e08541452dd1ea6b7a6f9710607 zkNative zka_coord_reciprocal_compute DA nonzero; external zero
VXP reciprocal acknowledgement zka:coord:vxp-reciprocal-ack:v1 0x23087cc4ad08d9dbc80318a7e7eb8df9618112319bca1c10f93fcd37073ebc6d attested zka_coord_reciprocal_ack DA zero; external nonzero

Every supported mapping is version 1 and is pinned to the following immutable profileManifestHash / profileVkHash pair, in table order: PRP 0x00502511877b0c7502792ccc35e841e36d067ba98925728253bc66ccb1082768 / 0x0bd66b2eb5d199726ef90f307069782beae1afe6d9a7488ec2c1e251d7b78f63; VXP compute 0x21b14f3d82f54bfef2ae81ca254081bf0e94dd6ae7c0d7ca7a42684ef3d5a61c / 0x2073432bf50f20e8cd5127a9734419ff866ce08e0d2b6a7b8a09e6491e425319; and VXP acknowledgement 0x2b622e9a71c5e351bfef8d54e970084c483f7f1c158fe893296cdbaa117f2f1d / 0x1bce6effb5f61fce0f27a7257ff301b67538699e675d0c6617da014d6cbf29dd. The three immutable upstream profile-manifest file digests are, in the same order, 95b657020189c074976fcd49e0502d52d12445907caae7c6199292163c309560, 11a25ff351add2b60172bb7d331eef862c97458185dbfd5834951ef04c394733, and de85a9ad42e5b33de771569884b55030f14b88a9a413ff56a1ea24864242f15a; those SHA-256 file pins are not substitutes for the BN254 manifest fields. The carried final proof's artifact is the policy-pinned role-2 zka_coordination_v2_recursive artifact with verification-key field hash 0x2e72267c8a0c8750a5cfe2e2893ec689ae9b4a488938ac084aeff1b470d055a2, not one of the application circuits in the table. Its artifact-entry hash, schema hash, and target remain explicit verifier-policy selections. The conformance fixture selects ZKA's live-fixture policy-entry convention and therefore pins artifactEntryHash 0x1520451d4710b91c59f982d7c148ba89c52d7e10c842bbb30a3a25b083784fae and schema hash 0x18a3537ebe76dccbc7ca7a3f80eab71cdb3602df63bc6ad27bc4a7b5b9937c4a; deployments using a different canonical source/toolchain policy entry commit and sign its exact resulting policy coordinates instead.

The reciprocal acknowledgement is narrow: it proves only verification of the profile's signed assertion, not reciprocal computation, settlement, delivery, or an objective outcome. Its immutable canonical manifest pins the conformance-pilot producer and consumer identity keys. A pilot with different identities MUST publish a distinct versioned profile manifest and artifact and MUST NOT reuse zka:coord:vxp-reciprocal-ack:v1; the reference mapping above accepts only the pinned profile. Each VXP compute leg is independently complete. An acknowledgement gates only its successor leg, and a terminal acknowledgement remains separate evidence.

A completion envelope uses the already-registered result message class. PRP reconciliation and the VXP reciprocal-compute leg travel from the session initiator to the responder; the reciprocal acknowledgement travels from the responder to the initiator. Any other class or direction is terminal. On this acceptance path the envelope and sender objects have exactly their registered members, every member is present, duplicate JSON names and non-JSON numeric constants are rejected, and generated_at / expires_at are finite JSON numbers; an ignored extension or value normalized away during decoding cannot become an exact retransmission. The selected ZKA bundle profile is the minimal pure work-attestation form: its exact top-level members are compliance, coord_proof, and metadata, and compliance is exactly { "proofs": [] }. A transaction, binding, compliance proof, compliance aggregation, unknown member, or explicit null placeholder is rejected on this path; none may be used as a side channel for private workflow material. This selects a strict canonical subset of zka:bundle:v1 and does not define another bundle format. The bundle's metadata.generatedAt and metadata.expiresAt MUST be offset-bearing RFC 3339 timestamps, expiry MUST NOT precede generation, and expiry MUST be strictly later than the receiver's verification time. These are the pinned ZKA ordering and freshness semantics; AFP adds no future-time or clock-skew rule, and the complete metadata remains input to the configured verifier. A PRP/VXP coord_proof submitted through the generic envelope-receive path is rejected: completion is available only through the policy-, verifier-, direction-, and replay-enforcing path.

Before a PRP or VXP state machine records completion, applies a result, settles a leg, or emits a commitment, it MUST independently verify the complete bundle, final proof, and canonical receipt, revalidate that the authenticated session is still active, and then atomically and durably write workflowInstanceId -> (sessionRef, receiptCommitment, completionFingerprint, canonicalEnvelopeBytes). The final session revalidation and durable write are serialized against same-instance lifecycle transitions; an abort or expiry observed while verification was in progress records no completion. That one record is both the replay authority and the authoritative completion effect; an in-memory inbox is only an idempotent projection of it. This ordering closes the crash gap between replay reservation and effect recording: after session authentication, structural/freshness validation, and policy binding, a byte-identical retransmission can reconstruct the projection from the durable record without recontacting a verifier that is now unavailable and without applying a second effect. An unseen completion still requires both configured verifier calls. completionFingerprint hashes the exact signed outer/derived contexts, ordered parties, authenticated direction, committed policy, full 33-element receipt, selected artifact, and authorized evidence references. An exact byte retransmission is idempotent. A changed envelope, receipt, artifact, evidence reference, policy, direction, party pair, or cross-session use of a reserved workflow instance is terminal replay. A store advertised as durable MUST reject SQLite in-memory, temporary, or URI-selected memory modes.

Private reconciliation records and reciprocal data chunks remain encrypted participant-held material. AFP carries only the canonical ZKA proof and public commitments. An authorized evidence reference is verifier-local, opaque, and bound by its kind and aggregate commitment; it contains no caller-set availability flag. Before either configured verifier method may retrieve evidence, the consumer requires exactly one reference for each nonzero data-availability or external-fact commitment in the receipt and no other reference. Missing, duplicate, same-commitment/different-kind, or unrelated extra references are terminal; the verifier cannot be used as a confused deputy to fetch an unnecessary private dataset. The configured verifier MUST retrieve and authenticate each exact admitted reference. Data-availability commitments prove integrity, not operational availability. Only the configured verifier's typed temporary-unavailability signal maps to COORD_V2_DATA_UNAVAILABLE, the only retryable result, and it writes no durable completion record. A generic verifier exception, altered reference, or successfully retrieved but commitment-mismatched reference is terminal. No witness, private payload, retrieved bytes, profile bundle, proof bytes, opaque reference, or backend exception text may appear in an error.

A configured verifier preserves a canonical terminal ZKA diagnosis only by raising the typed CoordVerifierRejected signal with one exact allowlisted terminal COORD_V2_* code: version, identifier, predecessor count, inactive slot, assurance, profile policy, artifact, application proof, scope, or incomplete workflow. The consumer returns that enum value without exception text. Replay, malformed input, verifier failure, AFP identity/session binding, and temporary unavailability remain consumer-owned outcomes and are forbidden in that signal; an unsupported, mutated, textual, or otherwise untyped verifier rejection collapses to COORD_V2_VERIFIER_FAILURE. This makes codes such as COORD_V2_PREDECESSOR_COUNT and COORD_V2_INCOMPLETE_WORKFLOW reachable without trusting a backend-controlled string.

The reference returns the stable ZKA codes COORD_V2_VERSION, COORD_V2_IDENTIFIER, COORD_V2_PREDECESSOR_COUNT, COORD_V2_INACTIVE_SLOT, COORD_V2_ASSURANCE, COORD_V2_PROFILE_POLICY, COORD_V2_ARTIFACT, COORD_V2_APPLICATION_PROOF, COORD_V2_SCOPE, COORD_V2_INCOMPLETE_WORKFLOW, COORD_V2_DATA_UNAVAILABLE, COORD_V2_REPLAY, COORD_V2_MALFORMED, and COORD_V2_VERIFIER_FAILURE, plus the AFP boundary codes AFP_COORD_IDENTITY and AFP_COORD_SESSION_CONTEXT. The conformance fixture groups them into the stable cross-organization classes unknown_profile, session_or_context, identity, workflow_or_policy, manifest_vk_or_artifact, assurance, evidence_alteration, replay, and proof_failure. All but data unavailability are terminal and record no completion. A reference code and retryable flag are the entire public diagnostic; implementations MUST redact internal exception text and private material.

docs/afp_coord_v2_cross_org_conformance_v1.json is the normative machine-readable cross-organization pilot fixture. It pins mutually authenticated parties, the context KAT and source artifacts, verifier-owned policy coordinates, private-data custody and opaque evidence-reference behavior, the three accepted claims, terminal mutation/replay cases, retryable unavailability, independent verification, and redaction assertions. A conforming pilot MUST reproduce every case without importing a witness or private payload into AFP. The standard-library reference tests use a deterministic verifier double to test this AFP consumer boundary; that double is deliberately not evidence of cryptographic verification. A real pilot satisfies independent_final_proof_verification only through its deployment-bound ZKA adapter verifying the complete bundle and final proof against the pinned coordinates before the durable completion write.

ZKA bundle requirements. ZKA v0.9.2-draft §5.4 defines an optional coord_proof member in zka:bundle:v1 and permits pure work-attestation bundles where coord_proof is present and transaction is absent. The bundle's metadata.context is the shared replay/context binding and always equals the canonical nonzero BN254 projection of the AFP envelope's authenticated X_ session_context; transaction, binding, and Coord context checks retain that projected field value. Only contained ZKC proofs in the current AFP mode use the separately authenticated v2 presentation context above. For pay, §15.4 defines the additional quote-bound interaction context supplied to the ZKA payment proof or request; the pool and proof enforce its binding, and any recomputing party checks the §15.4 fold. A profile carries an AFP envelope over zka:bundle:v1, and ZKA verifies the transaction, compliance, binding, and coordination proofs that are present.

12.8 Freedom Safeguards at the session layer

The six Freedom Safeguards are defined normatively in ZKA v0.9.2-draft §1.6 and inherited by AFP at the protocol level (§9). This subsection maps them onto the bilateral session; every profile inherits them by building on the core and MUST NOT define a competing safeguards mapping.

# Safeguard (ZKA §1.6) Application to an AFP bilateral session — inherited by every profile
1 Predicate Pluralism A session privileges no issuer or predicate set. Compliance requirements are the bilateral choice recorded in the federation agreement (§5.5); the core defines no canonical or default predicate set and no canonical or default root authority (§6.3.1), and a profile MUST NOT introduce one. A default entry an implementation ships in its verification policy is product configuration, not a core or profile default; it is visible, editable, and removable by the deployment operating it (the duty is normative in §6.3.1), and it applies to an interaction only when signed into that interaction's policy (§9, §14.4).
2 Minimal Disclosure Every in-session proof is atomic — one predicate per proof (§5.4). A core CommitmentAtom commits to an outcome_root, not cleartext (§12.6.1). The session reveals only what the exchange requires; profiles enforce this structurally (PRP's Merkle-subtree pruning, VXP's value commitments) but inherit the default from the core.
3 No Revocability No operator, arbiter, or platform can force, freeze, reverse, or invalidate a session outcome. A CommitmentAtom requires both parties' KEL-anchored signatures (§12.6.1); dispute is bilateral-only with at most a pre-agreed arbiter that still needs both signatures (§12.5); timeout/expiry is observe-and-resolve, never intervention (§12.2.3). This is the session-layer statement of the §3 operator-blindness invariant.
4 Open-Source Predicates Any predicate a session carries MUST be open-source, versioned, and published with a public review period, exactly as ZKC §1.4 / ZKA §1.6 require. A profile adds no opaque predicate.
5 Agent Exit Rights Either party MAY exit a session, and the relationship, at any time (§12.2.2 → ABORTED); exit is unconditional and carries no protocol penalty. Leaving a federation, leaving a managed transport for an appliance (§4.7), and changing operator are all unconditional. A compliance-proof rejection blocks the interaction (the session declines), never exit. A profile's recourse for abandoning a confirmed commitment is only the bilaterally-agreed terms in the agreement, never an operator-enforced lock.
6 Verifier Accountability A proof or attestation request MUST be authenticated under the requesting party's stable KERI identity and the holder MUST retain a portable receipt of what was requested, for which declared purpose, and by whom (§5.8). Anonymous or unreceipted demands are rejected. A profile MUST preserve this accountability for every proof it requests.

A profile inherits this table by running over the core. A profile MAY note how it realizes a safeguard structurally (as PRP's Localize algorithm realizes Safeguard 2), but it MUST NOT weaken any safeguard, and it does not re-establish them — they hold because the session does.


13. Profiles

13.1 What an AFP profile is

An AFP profile is a named behavior over the Bilateral Session Core (§12). Both endpoints are AFP tenants (§1.2), so PRP and VXP are profiles rather than peer protocols. They are standardized behaviors layered on AFP, not independent wire standards with separate session, identity, or governance machinery.

A profile:

The test for profile vs. protocol is direct: if both endpoints are AFP tenants (§1.5.1) and the behavior is a named way of using a bilateral session, it is a profile. A tenant remains an AFP tenant whether it uses the reference implementation or another conforming implementation and whether it attests under an accepted authority or under none. A behavior whose counterparties are not AFP tenants belongs in a standalone standard, not an AFP profile.

13.2 Profile registration and extension

Profiles are registered lightly; AFP deliberately avoids a heavyweight registry that would create marketplace machinery (§1.2). A profile is defined by a small, declarative profile descriptor:

interface AfpProfileDescriptor {
  name: string;                 // the {profile} namespace label (§12.4); unique; lowercase; e.g. 'prp'
  version: string;              // profile spec version, e.g. '0.2'
  spec_ref: string;             // URI / citation of the profile specification
  atom_kinds: string[];         // the profile's atom-kind discriminators, e.g. ['prp:session:v2', …]
  policy_schema_ref: string;    // the policy block this profile adds to the federation agreement (§5.5)
  status: 'reference' | 'experimental' | 'deprecated';
}

Registration rules:

  1. A profile name occupies exactly one {profile} label in the §12.4 tree and MUST be unique across registered profiles.
  2. The registry is the set of descriptors published with the AFP specification plus any a federation agrees bilaterally to recognize. There is no global, queryable, marketplace-style profile directory (§8.8's reasoning applies: such a directory would itself leak the coordination graph). Two tenants recognize a profile by naming it in their federation agreement; the descriptor is how they agree on its atom kinds and policy schema.
  3. A profile MUST register its atom-kind discriminators so they do not collide with another profile's or with the core's afp:*:v1 kinds.
  4. Extension is by versioning the profile (a new descriptor version), not by mutating the core. A profile MUST NOT add to or alter the core's §12 machinery; if a behavior needs a core change, that is an AFP-spec change, not a profile change. This keeps the core stable and the dependency acyclic: profiles depend on the core; the core depends on no profile.
  5. A profile descriptor's status is reference (specified with AFP, the recommended set), experimental (recognized bilaterally, not yet a reference profile), or deprecated.

13.3 Published profiles

AFP publishes three reference behaviors and three experimental profiles: afp.introduction (§14), which remains experimental until its lower layers are operational (§14.9); pay v1.0 (§15), which remains experimental pending independent production-audit evidence for its Base settlement dependencies; and pay_venue v2.0 (§15.9), which is published for exact bilateral negotiation but does not qualify or activate a settlement deployment.

Profile (name) Behavior Atom kinds Spec Status
work (base) AFP's own tenant↔︎tenant work exchange — §5: a bilateral cross-tenant work request and a ZKA-carried result / attestation. The base behavior of the core, not a separate document. afp:session:v1, afp:commitment:v1 + the §5.3 message classes This spec, §5 + §12 reference
prp Pairwise Reconciliation Protocol — bilateral state reconciliation: Compare → Localize → Evidence → Resolve → Commit (+ optional Anchor). A Coord v2 completion claim is only zka:coord:private-reconciliation:v1 under the §12.7.1 policy and verification rules. prp:session:v2, prp:comparison:v2, prp:divergence:v2, prp:evidence:v2, prp:resolution:v2, prp:commitment:v2, prp:anchor:v2 PRP v0.2 reference
vxp Value eXchange Protocol — bilateral value exchange: Intent → Negotiation → Commitment → Fulfillment → Settlement, across reciprocal / compute / data / capability / money domains. Coord v2 recognizes only the reciprocal-compute and narrowly-scoped reciprocal-ack claims of §12.7.1. vxp:* discriminators over IntentAtom, NegotiationAtom, CommitmentAtom, FulfillmentAtom, SettlementAtom, DisputeAtom, ChannelAtom, SubscriptionAtom VXP v0.2 reference
afp.introduction Introduction Tier — stranger onboarding: a credential + ZKC-proof handshake that auto-negotiates a scoped, collateral-priced provisional §5.5 agreement, with reputation-driven limit growth, graduation, and safeguard-compatible downgrade/termination. afp:introduction-offer:v1, afp:introduction-request:v1, afp:credential-presentation:v1, afp:provisional-agreement:v1, afp:limit-adjustment:v1, afp:downgrade:v1, afp:termination:v1 This spec, §14 experimental
pay USD-obligation/WETH-settlement — a bilaterally authenticated USD commercial obligation and exact WETH-wei quote bound to a Base ZKA-native pool, with finality-aware receipt evidence. afp:payment-quote:v1, afp:payment-receipt:v1 This spec, §15 experimental
pay_venue Venue-neutral private settlement — a bilaterally authenticated USD obligation and exact asset-atomic quote bound to an immutable venue deployment, recipient, manifest, policy, and holder-disclosed receipt occurrence. afp:payment-quote:v2, afp:payment-receipt:v2 This spec, §15.9 experimental

PRP and VXP define their own behavior-specific machinery — PRP's O(D·log N) Merkle localization, named scope filters, and star or mesh multi-party composition; VXP's value-domain taxonomy, exchange channels, subscriptions, and per-domain breach conditions. They reference §12 for the shared session, identity, namespace, dispute, arbiter, commitment, anchor, bundle, and safeguard machinery.

Multi-party note. PRP composes N-party reconciliation from bilateral sessions in a star (a coordinating center runs one session per site) or a small mesh. This is a profile composition of core bilateral sessions, not a core N-way construct — the core is bilateral by definition (§12.1), and any multi-party behavior is a profile arranging bilateral sessions. The optional pre-agreed arbiter of §12.5 (e.g. a trial coordinating center) and PRP's star hub are frequently the same entity, but the roles are distinct: arbiter (tie-break authority granted in the agreement) vs. hub (the party that holds the converged snapshot).

13.4 Profile conformance

A conforming AFP profile:

A "profile" that defines its own session establishment, identity binding, standalone namespace, dispute model, or bundle wire format is not a conforming AFP profile; it duplicates the core.


14. Introduction Tier

This section is normative. It specifies how two tenants with no prior relationship bootstrap a working bilateral relationship automatically: a credential and ZKC-proof handshake negotiates a scoped, collateral-priced provisional federation agreement (§5.5), grows the envelope with provable interaction history, graduates to a standard agreement, and degrades or terminates safely without weakening operator blindness (§3), OB-2 routing (§8), or the Freedom Safeguards (§9 / §12.8).

The introduction tier is AFP's path from strangers to a first agreement. It is an afp.introduction.v1 profile (§13) over the Bilateral Session Core (§12), with the bounded opening rule in §12.1. Its progressive-trust model has four steps: (1) opt-in discovery and a credential handshake produce a scoped, small-limit provisional agreement; (2) first value interactions settle on the ZKA-direct rail or under collateral-anchored small-limit channels; (3) limits grow with ZK-provable interaction history; and (4) the relationship graduates to a standard §5.5 agreement and fast APL channels.

14.1 The willingness-to-federate directory

Discovery is opt-in. An instance that wishes to be approachable by strangers MAY publish an offer record to a KERI-anchored, witness-replicated directory, structurally the same object as the §8.7 mix-node directory:

interface AfpIntroductionOffer {           // afp:introduction-offer:v1
  kind: 'afp:introduction-offer:v1';
  tenant_aid: string;                       // who is open to being approached
  accepted_credential_classes: string[];    // e.g. ['zkc:proof:jurisdiction:v2',
                                            //       'zkc:proof:kyc:v2',
                                            //       'afp.telemetry.instance_attestation.v2']
  entertained_envelope: {                   // the CEILING it will negotiate within — not an offer of credit
    assets: string[];
    max_per_interaction: string;            // decimal string
    max_cumulative: string;
    collateral_required: boolean;
  };
  route_handle: string;                     // §8.8 private-route resolution handle (not a clear address)
  directory_epoch: number;                  // binds to the directory epoch (anti-replay), as §8.7
}

The directory is KERI-anchored and verified exactly as §8.7's mix-node directory: a tenant MUST reject an offer whose KERI anchor is absent, whose witness quorum is below the federation threshold (§4.5), or whose directory_epoch does not match the directory's current epoch exactly (a stale epoch is replay; a future epoch cannot yet be validated).

Directory boundary (normative). This directory is not a work marketplace (§1.2) and does not weaken OB-2 (§3, §8):

The directory's hosting, epoch rotation, and gossip operation are a federation-operational concern outside AFP scope, not a tenant-internal one — the record is witness-replicated and verifiable without trusting any single party, structurally like the §8.7 mix-node directory, so no single tenant discharges it (a companion requirement); this section specifies the record shape, the OB reconciliation, and the conformance rules only.

14.2 The afp.introduction.v1 profile and the bootstrap exception

The introduction tier is an AFP profile (§13), registered with the descriptor:

{
  name: 'afp.introduction',
  version: '1.0',
  spec_ref: 'AFP 0.3.12-draft §14',
  atom_kinds: [
    'afp:introduction-offer:v1',
    'afp:introduction-request:v1',
    'afp:credential-presentation:v1',
    'afp:provisional-agreement:v1',
    'afp:limit-adjustment:v1',
    'afp:downgrade:v1',
    'afp:termination:v1',
  ],
  policy_schema_ref: 'afp:introduction-policy:v1',
  status: 'experimental',                   // promoted to 'reference' once the lower layers are operational federation-wide (§14.9)
}

Bounded session-opening rule. Section 13.2 rule 4 forbids a profile from altering the core. Section 12.1 expressly permits afp.introduction.v1 to open for the purpose of minting a provisional agreement in place of a pre-existing one; no other profile may use that rule. Mutual authentication (§5.2 step 2), end-to-end encryption, OB-2 routing (§8), and context binding (§5.2 step 4) remain REQUIRED. Open federation (§5.8) is a separate core trust basis: it opens with no agreement through per-interaction ZKC attestation and does not mint a provisional agreement. Section 12.1 enumerates all three trust bases.

The instance-attestation credential named below is §6.3 afp.telemetry.instance_attestation.v2. Each side's introduction policy MUST name the root classes and root-authority identifiers it accepts; the proving side selects one accepted pair and proves the v2 predicate under that branch. The verifier performs the matching §6.3.1 root-specific obligations in addition to ordinary ZKC v0.4.1-draft verification, including out-of-circuit resolution of any delegated publisher signing AID to the named authority root. Neither side is required to accept any particular authority; a policy naming no authority declines v2 attestation rather than defaulting to one, and an authority an implementation ships as a default entry has this standing only once that party signs it into the policy (§14.4). A v1 proof does not satisfy a v2 request and MUST NOT be reinterpreted as carrying a generic root class.

14.3 The handshake

The handshake runs inside one §12 bilateral session (profile: 'afp.introduction') and drives it from zero trust to a signed provisional agreement:

PROPOSED ──▶ CREDENTIALING ──▶ NEGOTIATING ──▶ BONDING ──▶ ACTIVE
   │              │                  │             │          │
   └─▶ABORTED     └─▶DECLINED        └─▶DECLINED   └─▶ABORTED  ├─▶ DOWNGRADED ──▶ (ACTIVE)
       /ABORTED       /ABORTED                                 ├─▶ TERMINATED
                                                               └─▶ GRADUATED

The opener fixes the credential-validity interval used by any v2 instance-attestation APR:

interface AfpIntroductionRequest {          // afp:introduction-request:v1
  kind: 'afp:introduction-request:v1';
  initiator_tenant_aid: string;
  responder_tenant_aid: string;
  offered_credential_classes: string[];
  required_credential_classes: string[];
  proposed_envelope: object;                // the §14.4 envelope fields
  attestation_window: { starts_at: datetime; ends_at: datetime };
}

Every introduction request MUST carry attestation_window; starts_at MUST precede ends_at, and both endpoints MUST lie within the introduction session lifetime. If v2 is requested, its APR and proof MUST copy this exact window. Requiring the field universally keeps the v1 request shape deterministic even when a request does not ask for instance attestation.

Relationship to ZKM mandates. The provisional agreement produced here is a bilateral AFP policy envelope; a ZKM mandate is a unilateral, principal-issued authorization consumed by an agent. An interaction MAY be subject to both. The AFP per-interaction/cumulative limits and the ZKM cap, expiry, policy, and revocation checks are evaluated independently, and both must accept. Forming, downgrading, terminating, or graduating an AFP agreement does not issue, widen, revoke, or consume a ZKM mandate. Conversely, ZKM revocation does not mutate the AFP agreement or any underlying ZKA note.

Terminal states: DECLINED (credential or negotiation failure; no agreement is formed), ABORTED (unconditional exit, §12.8 Safeguard 5), and from ACTIVE: DOWNGRADED (the envelope contracts, then the session returns to ACTIVE under the new terms; §14.7), TERMINATED (§14.7), and GRADUATED (replaced by a standard §5.5 agreement; §14.6).

14.4 The provisional agreement and the introduction policy block

interface AfpProvisionalAgreement {         // afp:provisional-agreement:v1
  kind: 'afp:provisional-agreement:v1';
  tier: 'introduction';                     // distinguishes it from a standard §5.5 agreement

  // §5.5 agreement core: mutually signed by both tenant root (or delegated) AIDs,
  // using the §12.6.1 bilateral-signature shape.
  initiator_signature: AfpCommitSignature;  // §12.6.1: signed_by, kel_event_dig, signature, signed_at
  responder_signature: AfpCommitSignature;

  policy: AfpIntroductionPolicy;            // afp:introduction-policy:v1

  reputation_state: {                       // the running, signed basis for §14.6 growth
    settled_interactions: number;
    cumulative_settled_value: string;
    slash_events: number;
    last_commitment_ref?: string;           // chains into the §12.6.1 commitment history
  };
}

interface AfpIntroductionPolicy {           // afp:introduction-policy:v1
  permitted_entities: string[];
  permitted_assets: string[];
  max_per_interaction: string;
  max_cumulative_outstanding: string;
  bond_ref?: string;                        // escrow reference; absent only for a trustless-ZKA-direct-only envelope
  accepted_credentials: { class: string; valid_until: string }[];
  accepted_instance_attestation_roots: {
    root_class: 'root:build-provenance' | 'root:tee-attestation' | 'root:publisher-manifest';
    root_authority_identifier: string;
  }[];                                      // signed v2 verifier policy; empty only when v2 is not required
  rotation_policy: string;                  // KEL rotation expectations (§4.6)
  growth_curve: object;                     // negotiated step / cap / decay (§14.6) — not protocol-fixed
  agreement_expires_at: string;
}

A provisional agreement is a specialization of the §5.5 federation agreement, distinguished by tier: 'introduction' and a reputation state — and, for a bonded envelope, a bond reference (bond_ref is absent for a trustless-ZKA-direct-only envelope, §14.5). When accepted_credentials contains afp.telemetry.instance_attestation.v2, accepted_instance_attestation_roots MUST contain at least one pair, and the verifier MUST accept only a proof whose public root_class and root_authority_identifier exactly match one signed pair. If v2 is not required the array MUST be empty; it MUST NOT silently authorize an attestation predicate. A pair that a party's implementation ships as a default entry has no different status here: it is signed into the agreement by that party or it does not apply. An empty array means the predicate is not required — never that a shipped default applies. Like every §5.5 agreement the complete policy is private to the two tenants, bilaterally signed, not published, and not discoverable. It is referenced by the afp.work.request (§5.3) / VXP intent messages of the value interactions it scopes.

14.5 Settlement safety floor

Every value interaction under an introduction-tier agreement MUST be either:

A conforming implementation MUST NOT allow uncollateralized credit at the introduction tier. Credentials gate which counterparties and assets are eligible; the bond is what bounds loss. (Instance identity is not sybil resistance; collateral prices the risk.)

Enforcement location. The floor is enforced at the agreement / channel-policy layer: the provisional agreement's envelope populates the channel policy block (the APL channel policy block for bonded and graduated channels), and the layer that authorizes or opens a value channel enforces "ZKA-direct, or bonded ≤ collateral" before any interaction. This section defines the conformance property; the channel-policy layer implements it. A value profile (e.g. VXP) is not special-cased to carry introduction-tier validation — cross-cutting policy lives at the core/agreement layer (§13.2 rule 4).

14.6 Progressive limit growth and graduation

Mechanism (normative). A party MAY present an afp:reputation-proof — a zero-knowledge proof of N settled interactions and cumulative settled value with zero slash/default events, bound to this agreement and counterparty and non-replayable (bound to the agreement id and a monotonic counter, mirroring the §12.6.1 chaining). On verification, the presenter proposes an afp:limit-adjustment:v1 raising the envelope; both parties sign it with the §12.6.1 machinery. A rejected, stale, or slash-bearing proof yields no adjustment — never a freeze.

Curve (policy, not protocol). The growth curve — step size, caps, decay on inactivity — is a negotiated parameter in the afp:introduction-policy:v1 block. AFP fixes the predicate and the adjustment handshake; the numbers are the bilateral choice. This mirrors the house rule that the mechanism is normative and the parameters are negotiated (cf. §8.2).

Graduation. When the parties choose, they replace the provisional agreement with a standard §5.5 agreement — no introduction tier, no mandatory bond, eligible for fast APL channels. Graduation is itself a bilateral signed act; the §12.6.1 chained-commitment history carries forward as the relationship's provable basis. The reputation proof and the on-chain escrow are lower-layer constructs (§14.9); this section owns the predicate's interface and the adjustment/graduation handshake, not the circuit.

14.7 Downgrade and termination

Both are safeguard-compatible: nothing here freezes, claws back, or redirects tenant state (Safeguard 3), and exit is unconditional (Safeguard 5).

Downgrade. The go-forward envelope contracts — lower limits, higher collateral, narrower assets. Triggers include a lapsed credential validity (the policy-block expiry), a late or missed settlement, a signed dispute-divergence record (§12.5), partial collateral consumption, or reputation decay. A downgrade is realized either as a re-negotiation both parties sign (an afp:downgrade:v1 followed by a new afp:limit-adjustment:v1) or as a unilateral protective contraction: a party MAY always decline to extend further credit or offer a smaller envelope (declining an interaction is always permitted, §5.4 / Safeguard 5). A party can never unilaterally compel the counterparty to act.

Termination. Recorded as an afp:termination:v1; either party MAY terminate unconditionally and with no protocol penalty (Safeguard 5). Residual obligations settle against the pre-posted escrow only — the §12.5 arbiter-co-signed release, bounded by what was escrowed, never against base-layer custody (Safeguard 5). Any unsettled dispute leaves a signed divergence record (§12.5).

Default consequence. A default during the relationship additionally yields a portable negative-history record — a signed statement of the default, usable as anti-reputation input to the counterparty's future introductions. It is the only persistent consequence beyond loss of the bond, and it is itself only a signed record: not a freeze, not a clawback (Safeguard 3).

14.8 Composition and Freedom Safeguards

14.9 Open seams and honest disclosure

The introduction tier is the AFP-owned protocol contract; several lower layers it composes are owned and tracked elsewhere. AFP owns the handshake, the provisional-agreement object, the introduction policy block, and the conformance properties of this section. It references — and does not redefine — the following:

Construct Owner / status
Trustless ZKA-direct settlement rail ZKA (transfer + unconditional withdrawal); operational.
zka:bundle:v1 carrying the credential presentation ZKA §5.4 (credential-only bundles are supported; no payment leg required).
ZK reputation-proof circuit (§14.6) ZKA/Reputation — companion requirement; not yet operational.
On-chain bilateral escrow with mutually-chosen arbiter (§14.5, §14.7) Companion requirement (ADAMAS.Network / ZKA escrow contract); not yet operational.
Instance-attestation predicate (§14.2) §6.3 afp.telemetry.instance_attestation.v2; AFP fixes the public statement and root-specific verifier obligations. The separately versioned ZKC machine build/runtime credential schema and issuer-adapter registry entry are companion work; raw provenance bundles and TEE quotes remain provider-side. An authority's key ceremony, published trust material, and the out-of-band AID↔︎legal-entity binding (§6.3.1) are likewise companion work owned by that authority rather than by AFP; for the ADAMAS product line these are Pyramidal Inc.'s and are not yet operational.

Honest disclosure. A deployment whose selected §6.3.1 v2 root path, escrow, or ZK-reputation lower layer is not yet operational MUST disclose the introduction tier as spec-available but not fully operational rather than claim it, exactly as §3.5 and §8.9 require for OB-1/OB-2. The profile descriptor's status: experimental (§14.2) reflects this until the lower layers are operational federation-wide. Claiming a fully operational introduction tier without a realized v2 credential/circuit/verifier path, the reputation circuit, and the bonded-escrow rail in place is a conformance violation, independent of the correctness of any one available component.


15. USD Obligation / WETH Settlement Profile (pay)

This section is normative and defines the AFP pay v1.0 profile: two principals agree on a commercial obligation denominated in USD minor units and an exact settlement amount denominated in WETH wei, then bind that bilateral quote to a private ZKA/Pay transfer. The profile composes afp:session:v1, afp:envelope:v1, zka:bundle:v1, and AFP-KDC v1.0.3 without defining a new session, envelope, bundle, or key-derivation surface. AFP-KDC v1.0.3 retains the v1.0.2 Poseidon2 parameter and conformance profile.

15.1 Ownership boundaries and registration

AFP owns the principal-readable USD obligation; the exact WETH-wei quote; its source, time, expiry, slippage bound, nonce, and bilateral authentication; its binding to a live AFP session and a specific immutable ZKA pool; and the observational settlement receipt. ZKA owns notes, nullifiers, roots, proofs, conservation, pool state, pool_context, the payment proof's interaction_context, unconditional spend/withdrawal semantics, and each pool's asset-risk profile. APL owns selection of the configured :zka_direct route, construction and submission of the ZKA SDK request, finality observation, and route evidence returned to AFP.

AFP does not verify a ZKA proof, query an exchange-rate oracle, decide whether a price is fair, custody value, define a multi-asset pool, or reproduce ZKA calldata or circuit schemas. ZKA does not interpret USD, choose a pricing source, or enforce quote fairness. APL does not alter a signed quote.

The published descriptor is:

{
  name: 'pay',
  version: '1.0',
  spec_ref: 'AFP 0.3.12-draft §15',
  atom_kinds: [
    'afp:payment-quote:v1',
    'afp:payment-receipt:v1',
  ],
  policy_schema_ref: 'afp:payment-policy:v1',
  status: 'experimental'
}

experimental reflects the absence of the independent-audit evidence required to claim the launch settlement dependencies as production-ready. It does not weaken this wire contract or any conformance requirement below.

The federation agreement names the profile through this policy block:

interface AfpPaymentPolicyV1 {
  kind: 'afp:payment-policy:v1';
  deployment: {
    chain_id: 8453;
    asset: '0x4200000000000000000000000000000000000006';
    pool: string;
    pool_context: string;
    asset_risk_profile: 'zka:asset-risk:s3-hard:v1';
  };
  accepted_rate_sources: string[];
  max_quote_lifetime_seconds: number;
  max_slippage_bps: number;
  required_finality: 'safe' | 'finalized';
}

accepted_rate_sources MUST be non-empty and contain unique, non-empty names. max_quote_lifetime_seconds MUST be a positive integer; max_slippage_bps MUST be an integer in 0..10000. The deployment fields use the canonical encodings of §15.2 and identify one immutable pool. An accepted quote's deployment and rate source MUST match this policy exactly, its lifetime MUST NOT exceed the policy maximum, and its slippage bound MUST NOT exceed the policy maximum. Receipt finality MUST reach required_finality before the application represents the interaction as settled. The Bilateral Session Core's profile_policy_hash MUST equal qb64("D", SHA-256(canonical_json(AfpPaymentPolicyV1))), and a pay quote MUST be accepted only inside a live session whose profile is exactly pay and whose policy hash matches the policy used for validation.

15.2 Canonical payment quote

interface AfpPaymentQuoteV1 {
  kind: 'afp:payment-quote:v1';
  quote_id: string;
  session_ref: string;
  session_context: string;

  obligation: {
    currency: 'USD';
    amount_minor: string;
  };

  settlement: {
    chain_id: 8453;
    asset: '0x4200000000000000000000000000000000000006';
    pool: string;
    amount_wei: string;
    pool_context: string;
    asset_risk_profile: 'zka:asset-risk:s3-hard:v1';
  };

  rate: {
    source: string;
    usd_minor: string;                  // settlement-rate numerator
    weth_wei: string;                   // settlement-rate denominator
    reference_usd_minor: string;        // signed source-rate numerator
    reference_weth_wei: string;         // signed source-rate denominator
  };

  quoted_at: string;
  expires_at: string;
  max_slippage_bps: number;
  quote_nonce: string;

  initiator_signature?: AfpCommitSignature;
  responder_signature?: AfpCommitSignature;
}

obligation.amount_minor, settlement.amount_wei, and all four rate integers MUST be positive canonical unsigned base-10 integer strings: digits only, with no sign, whitespace, decimal point, exponent, or leading zero. USD is represented in ISO 4217 minor units (cents for USD) and WETH in wei. A wire implementation MUST NOT use a binary floating-point value for any monetary or rate component. The Base launch profile additionally requires settlement.amount_wei <= 2^120 - 1, matching ZKA's frozen MAX_NOTE_VALUE_WEI; quote construction and validation MUST reject a larger amount before signing or routing it.

The rate is the exact positive rational usd_minor / weth_wei. Quote construction and validation MUST use arbitrary-precision integer arithmetic without overflow or truncation and the deterministic ceiling rule:

settlement.amount_wei = ceil(
  obligation.amount_minor * rate.weth_wei / rate.usd_minor
)

Equivalently, implementations MAY compute (amount_minor * weth_wei + usd_minor - 1) // usd_minor; any different result MUST be rejected, and a result above the frozen ZKA note-value bound MUST be rejected. max_slippage_bps MUST be an integer in 0..10000. The reference rational is reference_usd_minor / reference_weth_wei; the settlement rational is usd_minor / weth_wei. Validation MUST also enforce, with arbitrary-precision integer arithmetic:

abs(usd_minor * reference_weth_wei
    - reference_usd_minor * weth_wei) * 10000
  <= max_slippage_bps * weth_wei * reference_usd_minor

This is the absolute settlement-price deviation divided by the signed reference price. It constrains pre-signing quote construction, never permits execution above the signed settlement.amount_wei, and no post-signing repricing is allowed. AFP fixes no global rate source; rate.source names the bilateral source or policy whose signed reference observation the parties accepted.

Every address in canonical quote content MUST be normalized before hashing to lowercase 0x followed by exactly 40 hexadecimal digits. Mixed-case, checksum-case, short, overlong, or otherwise non-canonical encodings MUST be rejected, not silently normalized during verification. The Base launch profile fixes chain_id to 8453 and settlement.asset to the lowercase Base WETH9 address shown above. settlement.pool, settlement.pool_context, and settlement.asset_risk_profile MUST exactly match the configured immutable deployment. pool_context MUST use the ZKA SDK's canonical nonzero BN254 field encoding: lowercase 0x followed by exactly 64 hexadecimal digits whose big-endian integer is in 1..r-1, for the modulus r in §15.4. session_context MUST be the base-defined X_ 32-byte digest (§5.2); any other form is out of profile and MUST be rejected.

quoted_at and expires_at MUST have exactly the whole-second UTC form YYYY-MM-DDTHH:MM:SSZ; fractional seconds and numeric UTC offsets are invalid. Both timestamps MUST represent valid instants, and expires_at MUST be strictly later than quoted_at. The quote is expired when the verifier's current time is greater than or equal to expires_at.

quote_nonce MUST be lowercase 0x followed by exactly 64 hexadecimal digits (32 bytes). A signing party MUST NOT sign two different quote contents with the same nonce. quote_id is a caller-supplied, non-empty stable identifier. An acceptor MUST atomically and durably reserve the exact (quote_id, quote_hash, quote_nonce, acceptance_record_hash) acceptance tuple when accepting a quote. This durable acceptance ledger is the authority consumed by route and receipt tracking code. A repeated accepted quote is a duplicate and MUST NOT execute again; the same ID with different canonical content or the same nonce with a different quote is a terminal validation failure. A rejected quote MUST NOT consume either replay key. A restart or process crash MUST NOT make an accepted replay key available again.

15.3 Quote hash and bilateral authentication

quote_content is the quote object with initiator_signature and responder_signature absent (not null). AFP uses the canonical JSON serialization already used by the Bilateral Session Core: UTF-8, lexicographically sorted object keys, and compact separators. Hashes use SHA-256 and AFP's D qb64 digest convention:

quote_hash = qb64("D", SHA-256(canonical_json(quote_content)))

Both principals MUST sign this exact quote_hash using the AfpCommitSignature shape of §12.6.1. Both signatures are required. Each signature MUST be genuine under the signer's KEL-current Ed25519 key identified by kel_event_dig; a stale, unknown, malformed, or mismatched key state MUST be rejected. AfpCommitSignature.signed_at is untrusted observational metadata: it is not covered by the Ed25519 payload, MUST NOT be used for freshness or authority, and changing it neither invalidates nor strengthens quote authentication. Freshness derives only from the signed quoted_at and expires_at fields compared with the verifier's current time. The initiator and responder AIDs MUST be the two principals of the live session, and session_ref and session_context MUST exactly match that session. The session context MUST recompute from the signed §12.2.1 session fields, and both session AfpPartyRef signatures MUST verify against KEL-current delegated identity and the declared witness thresholds before quote acceptance. An operator, arbiter, application administrator, or one party alone cannot substitute for either signature.

Signatures authenticate the exact settlement amount and all quote controls, including the rate source, pool, pool context, risk profile, timestamps, slippage bound, ID, and nonce. Any mutation after either signature was made invalidates authentication.

After both signatures verify, the acceptor MUST compute acceptance_record_hash = qb64("D", SHA-256(canonical_json(full_quote))), where full_quote includes both complete signature objects. This is distinct from the content-only quote_hash. The exact signed-record hash is stored in the durable acceptance tuple, retained by AcceptedPaymentQuote, and recomputed from its sealed canonical wire on every use. Removing, replacing, or changing either signature therefore cannot reuse a content-only acceptance reservation.

15.4 Interaction-context derivation

The ZKA payment for an accepted quote MUST bind the canonical nonzero BN254 field value derived below. The derivation is the ZKA/Pay Interaction-Context Binding Profile v1 fold — restated here normatively and in full, so this specification stays auditable standalone, and pinned to its frozen upstream definition: ZKA docs/sdd/sdd_pay_interaction_binding.md §2 at immutable revision db39afd, KAT fixture SHA-256 e0d7fd0442b3cfd14c21c949ae13bcf9fba4250b2067300f729aaad2d38a2a8d. On any divergence the pinned v1 governs. A future upstream v2 does not bind AFP; adopting one requires an explicit AFP interface revision and compatibility plan.

ZKA's active_fixed_byte operational-domain registry contains this tag as pay-interaction-context, and AFP vendors that registry at protocol-family/poseidon2-operational-domains-v1.json under the canonical family manifest. The manifest binds both registry copies and both copies of the frozen interaction-context KAT.

Both fold inputs are 32-byte digests mapped into the scalar field by plain big-endian reduction:

r = 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001

afp_quote_hash_field  = big_endian_uint256(quote_digest)   mod r
session_context_field = big_endian_uint256(session_digest) mod r

Here r is the BN254 scalar-field modulus. quote_digest is the 32 SHA-256 digest bytes the §15.3 quote_hash encodes — its qb64 payload: the digest itself, never its string encoding. session_digest is the 32-byte payload of the §5.2 X_ session context. A session context that is not exactly that base-defined digest is out of profile and MUST be rejected fail-closed, never mapped by a fallback. This mapping is a reading of the same §5.2/§12.3 session context into field form, not a parallel context: the AFP envelope retains the signed X_... value while the ZKA/Pay request consumes the derived field required by this section, just as Coord v2 consumes its §12.7.1 field reading. The quote-digest input remains unreduced on its AFP wire; an APL adapter or party recomputing the fold MUST apply the reduction above first — roughly four of five uniformly distributed digests exceed r, so skipping it is the common-case error. Truncation is non-conforming. A reduction yielding zero MUST be rejected fail-closed (probability 6/2²⁵⁶ ≈ 2⁻²⁵³; never substituted). Both inputs exist before either quote signature, so the producer MUST perform the full derivation at quote-construction time, before signature exchange and nonce reservation — a zero reduction there costs only a reissue with a fresh quote_nonce; at dispatch it is a terminal §15.5 fail-closed condition. Here r is the BN254 scalar-field modulus. quote_digest is the 32 SHA-256 digest bytes the §15.3 quote_hash encodes — its qb64 payload: the digest itself, never its string encoding. session_digest is the 32-byte payload of the §5.2 X_ session context. A session context that is not exactly that base-defined digest is out of profile and MUST be rejected fail-closed, never mapped by a fallback. This mapping is a reading of the same §5.2/§12.3 session context into field form — not a parallel context, and the §12 envelope/bundle context equality is unaffected. The wire stays unreduced: the quote digest carried to ZKA or an APL adapter is the raw 32 bytes, and any party recomputing the fold MUST apply the reduction above first — roughly four of five uniformly distributed digests exceed r, so skipping it is the common-case error. Truncation is non-conforming. A reduction yielding zero MUST be rejected fail-closed (probability 6/2²⁵⁶ ≈ 2⁻²⁵³; never substituted). Both inputs exist before either quote signature, so the producer MUST perform the full derivation at quote-construction time, before signature exchange and nonce reservation — a zero reduction there costs only a reissue with a fresh quote_nonce; at dispatch it is a terminal §15.5 fail-closed condition. Here r is the BN254 scalar-field modulus. quote_digest is the 32 SHA-256 digest bytes the §15.3 quote_hash encodes — its qb64 payload: the digest itself, never its string encoding. session_digest is the 32-byte payload of the §5.2 X_ session context. A session context that is not exactly that base-defined digest is out of profile and MUST be rejected fail-closed, never mapped by a fallback. This mapping is a reading of the same §5.2/§12.3 session context into field form, not a parallel context; §12.7 uses the identical projection for ZKA's bundle, transaction, binding, and Coord session-context slots while the AFP envelope retains the original X_ digest. The quote-digest wire stays unreduced: the value carried to ZKA or an APL adapter is the raw 32 bytes, and any party recomputing the fold MUST apply the reduction above first — roughly four of five uniformly distributed digests exceed r, so skipping it is the common-case error. Truncation is non-conforming. A reduction yielding zero MUST be rejected fail-closed (probability 6/2²⁵⁶ ≈ 2⁻²⁵³; never substituted). Both inputs exist before either quote signature, so the producer MUST perform the full derivation at quote-construction time, before signature exchange and nonce reservation — a zero reduction there costs only a reissue with a fresh quote_nonce; at dispatch it is a terminal §15.5 fail-closed condition.

The derivation is the field-count-prefixed Poseidon2 fold over the §A.3 instance's two-field hash P(a, b):

INTERACTION_CONTEXT_TAG =
  0x00007a6b612f7061792f696e746572616374696f6e2d636f6e746578742f7631
  // the UTF-8 bytes "zka/pay/interaction-context/v1",
  // right-aligned in a 32-byte word — a fixed byte tag (§A.4 carve-out)

state = P(INTERACTION_CONTEXT_TAG, Field(2))
state = P(state, afp_quote_hash_field)
state = P(state, session_context_field)
interaction_context = "0x" || lowercase_hex_64(state)

Field(2) is the field element two — the count of folded input fields, absorbed as an ordinary second sponge input to the first call, not an arity annotation. The input order is normative — quote hash first; order sensitivity is proven by the upstream asymmetry-pair vector. A zero fold output MUST be rejected fail-closed, matching the ZKA circuit's interaction_context != 0 assertion. The tag is a raw fixed byte tag consumed directly as a field element under the §A.4 carve-out for ZKA pool-adjacent fold tags; it is not a §A.3 deriveFieldTag domain, and it is currently registered in no family registry — its defining authorities are the pinned SDD §2 and the ZKA SDK's INTERACTION_CONTEXT_TAG_HEX constant. Until the coordinated family registry includes it, implementations MUST source that exact pinned value and collision-check it against the active fixed-byte registry before publication.

The application or APL adapter MUST provide the 32-byte lowercase hex field value, not a digest or a qb64 encoding, as the ZKA payment request/proof's interaction_context; the pool and proof MUST enforce the corresponding binding. The envelope retains the original session_context. Implementations MUST NOT replace, concatenate, reinterpret, or overload that session field, and MUST NOT use a raw quote hash or raw session context in place of the derived field.

Conformance vectors. An implementation MUST reproduce, byte-identically, all five vectors of the pinned upstream KAT fixture — small-ints, one-one, max-canonical, keccak-derived-quote, asymmetry-pair (published verbatim as the normative artifact docs/pay_interaction_context_kat_v1.json) — and the AFP-native end-to-end vector below, which pins the qb64→field seam the upstream vectors cannot exercise. Both of its digests exceed r, so the reduction leg is vector-pinned rather than prose-pinned. For the §15.2 canonical quote content

{"expires_at":"2026-07-11T12:05:00Z","kind":"afp:payment-quote:v1","max_slippage_bps":100,"obligation":{"amount_minor":"2500","currency":"USD"},"quote_id":"quote-2026-08-16-0002","quote_nonce":"0xabababababababababababababababababababababababababababababababab","quoted_at":"2026-07-11T12:00:00Z","rate":{"reference_usd_minor":"250000","reference_weth_wei":"1000000000000000000","source":"bilateral:base-weth-usd:v1","usd_minor":"250000","weth_wei":"1000000000000000000"},"session_context":"X_eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","session_ref":"session-0001","settlement":{"amount_wei":"10000000000000000","asset":"0x4200000000000000000000000000000000000006","asset_risk_profile":"zka:asset-risk:s3-hard:v1","chain_id":8453,"pool":"0x1234567890abcdef1234567890abcdef12345678","pool_context":"0x0101010101010101010101010101010101010101010101010101010101010101"}}

the derived values are:

quote_hash            = D_f5637ad946fbcc4ca76a7bb137a1796c1eb60c24590d773231d36f4759e8462d
afp_quote_hash_field  = 0x036df29ae103ab7c0dd91f20b01abf9a55b282b9f86e445bde69a363a9e84628
session_context_field = 0x2d5db5236a286e480dadd814e8e98d7a4e1f4dcd08092ca9df67189f2eeeeeea
interaction_context   = 0x26232f5bff7eb274b601df155b7b47abf504fc5778bdc2b35b961d14e5140d6d

Implementations MUST reproduce this vector exactly.

15.5 Acceptance and route execution

Before dispatching a route, an implementation MUST fail closed unless all of the following hold:

  1. the quote has the exact kind and canonical field encodings of §15.2;
  2. the amount satisfies the deterministic ceiling-rate calculation;
  3. the caller presents an immutable live-session capability minted by the AFP session core only after its owning instance completed the normal propose/accept-or-confirm/activate lifecycle; the detached session snapshot, identifiers, both AfpPartyRef signatures, and both quote signatures all verify KEL-current;
  4. the quote is unexpired and its ID and nonce pass atomic replay checks;
  5. chain, WETH asset, immutable pool address, pool_context, and zka:asset-risk:s3-hard:v1 match configured Base launch policy; and
  6. the derived interaction_context is supplied unchanged to the ZKA request, and no §15.4 field reduction of either fold input yielded zero — a terminal fail-closed condition at dispatch, reachable only if the mandatory quote-construction-time derivation (§15.4) was skipped.

The live-session capability is owner-bound and non-serializable. Every access MUST revalidate the exact current FSM, normal activation provenance, and expiry against the owning session manager's clock. Closing, aborting, expiring, or replacing that FSM invalidates every previously minted handle. A restarted manager MUST rehydrate the durable session/FSM state, re-establish its lifecycle provenance, and mint a new handle; it MUST NOT persist or restore the handle as standalone authority.

Unknown or unsupported chain, asset, pool, context, risk profile, signature, or finality state MUST be rejected. APL MUST submit exactly the signed settlement.amount_wei; it MUST NOT reprice, substitute an asset or pool, or route through a mutable/multi-asset settlement contract. A failed proof or submission leaves the interaction unsettled.

Successful acceptance MUST produce an immutable, non-wire AcceptedPaymentQuote capability minted only by the acceptance boundary after all checks above succeed. Its constructor MUST NOT be public, and it MUST NOT accept a caller-supplied quote hash. The minting path MUST canonically reconstruct the detached quote and recompute and verify quote_hash before retaining the quote ID, nonce, interaction context, and canonical quote wire. Receipt or route code MUST require this capability and a store-issued limited acceptance/route authority. It MUST fail closed unless the ledger contains the exact capability (quote_id, quote_hash, quote_nonce, acceptance_record_hash) tuple; verifier absence, false, or error is rejection. The tracker MUST recheck this reservation at initialization. A copied capability for the same sealed wire and exact durable tuple is equivalent; an unreserved capability is not accepted authority.

The deployment composition root MUST construct one PaymentReceiptVerificationContext that binds a store-issued limited acceptance/route authority and the deployment's finality-evidence verifier. The writer/replay store itself, an arbitrary structurally similar object, and an untrusted request-selected verifier MUST be rejected. Route evidence creation, route decoding, and tracker creation MUST use only this preconfigured context; their public request-facing calls MUST NOT accept a verifier parameter. A durable SQLite authority uses a separate connection exposing only exact-tuple verification and atomic route claim, never quote reservation; the in-memory test authority exposes the same limited operations under the store lock. This boundary prevents request data from substituting a fresh attacker ledger while leaving deployment owners free to choose their trusted composition root.

Before returning route evidence, the context MUST atomically claim one transaction_id against the exact accepted four-field tuple. The first claim wins, the same transaction is idempotent, and every different transaction for that quote MUST fail. Route decoding and tracker initialization MUST verify the exact claimed transaction. The supported reference API registers store-issued authority instances through a closure-held issued-instance set and rejects direct construction, nominal object construction, and structurally similar fakes. This is API misuse hardening inside a trusted implementation, not a sandbox boundary against arbitrary code already executing in the same interpreter.

The reference capability is process-local and MUST carry an HMAC-SHA-256 seal over its canonical accepted wire under an unpredictable key held only by the acceptance closure. Every capability use MUST compare the seal in constant time, reparse the sealed wire, recompute quote_hash and interaction_context, and match the retained quote ID, nonce, hash, and context before returning authority. This seal provides process-local tamper detection only; it is not authorization. The durable acceptance-ledger reservation above is the authorization source for untrusted wire/request inputs when accessed through the deployment-bound context.

In-process trust boundary. AFP does not claim to sandbox arbitrary code that already executes inside the trusted composition-root process. Such code can introspect Python closures, monkey-patch implementations, or access storage credentials and therefore can subvert any in-process reference guard. A deployment MUST keep request data, tenant code, plugins, and other untrusted extensions outside that authority boundary. If same-process code itself is in the adversary model, the acceptance/route authority MUST move behind a separate process, KMS/HSM-backed signer, or equivalently isolated service boundary; the in-process reference objects are not sufficient for that threat model. The capability MUST NOT serialize across a process boundary. Restart does not clear the durable replay reservation or permit the same quote to execute again: normal acceptance requires a new quote, while any implementation-specific recovery path must verify a durable accepted record without weakening replay. Long-running receipt observation should persist its route/receipt state rather than persist this capability.

15.6 Payment receipt and finality

interface AfpPaymentReceiptV1 {
  kind: 'afp:payment-receipt:v1';
  quote_ref: string;
  quote_hash: string;
  interaction_context: string;
  chain_id: number;
  pool: string;
  asset: string;
  amount_wei: string;
  transaction_id: string;
  observed_at: string;
  finality: 'pending' | 'safe' | 'finalized';
  finality_evidence: object;
}

A receipt is observational evidence, not settlement authority. quote_ref MUST equal the accepted quote's quote_id; its quote hash, interaction context, chain, pool, asset, and amount MUST exactly match the accepted quote and the immutable route evidence. transaction_id MUST be non-empty and MUST identify the submitted transaction. observed_at uses the exact whole-second UTC format of §15.2. finality_evidence MUST be a non-empty object appropriate to the deployment's documented finality policy; unknown or empty evidence is insufficient.

Receipt tracking MUST be configured with a fail-closed, deployment-specific finality-evidence verifier. For every observation, that verifier MUST validate the evidence against the configured chain and immutable deployment, the exact submitted transaction, and the claimed pending, safe, or finalized level. Verifier absence, failure, an unknown evidence shape, or evidence for another transaction, deployment, or level leaves the interaction unsettled. Retained evidence MUST be recursively immutable; serialization returns a detached JSON copy and cannot expose mutable nested aliases.

For one quote and transaction, receipt state MAY advance monotonically pending → safe → finalized. It MUST NOT regress, skip to an unknown state, change the transaction, or rewrite any quote/route binding. pending MUST NOT be represented as final settlement. A deployment MAY require safe or finalized for application completion; until that threshold is supported by matching evidence, the AFP interaction remains unsettled. Concurrent observations for one tracker MUST serialize the finality comparison and state update atomically; a lower or equal-rank observation cannot overwrite a higher state, regardless of verifier completion order.

15.7 Asset-risk and safeguard requirements

The flagship profile is WETH in one separately deployed immutable, single-asset ZKA pool declaring zka:asset-risk:s3-hard:v1. Settlement contracts MUST be single-asset and immutable. Every additional asset requires a separate pool address, pool_context, note/nullifier/root state, escrow, certification record, and explicit asset-risk profile. An application MUST NOT treat a shared or dynamically mutable asset registry as equivalent. A native stable unit is an independent future protocol and is not a dependency of AFP pay, ZKA/Pay, or the Base WETH launch.

The profile inherits all six Freedom Safeguards (§12.8). In particular, expiry, dispute, rejection, replay failure, insufficient finality, or route failure affects only acceptance of this interaction. It MUST NOT freeze, revoke, redirect, delay, seize, or otherwise gate a holder's existing ZKA notes or unconditional withdrawal (Safeguards 3 and 5). AFP applications and APL adapters MUST NOT add an administrator recovery, override, clawback, or forced-reroute path. Any compliance proof requested alongside payment remains atomic and requester-authenticated with a holder-retained receipt (Safeguards 2 and 6).

15.8 Conformance

A conforming pay v1.0 implementation:

An implementation that obtains its own price, silently canonicalizes invalid wire content, reprices after signature, permits one-party authorization, treats pending as final, reuses a mutable/multi-asset settlement contract, or adds an administrative note/withdrawal gate is not conforming.

15.9 Venue-neutral payment profile (pay_venue) v2.0

AFP publishes pay_venue as a separate experimental profile. It generalizes the signed settlement target without changing, converting, aliasing, or deprecating pay v1.0. Every Base v1 policy, quote, receipt, canonical byte string, hash, interaction-context vector, and route rule in §15.1–§15.8 remains unchanged. A peer that supports only v1 rejects v2, a peer that supports only v2 rejects v1, and negotiation MUST select exactly one profile and version without fallback.

The published descriptor is:

{
  name: 'pay_venue',
  version: '2.0',
  spec_ref: 'AFP 0.3.12-draft §15.9',
  atom_kinds: [
    'afp:payment-quote:v2',
    'afp:payment-receipt:v2',
  ],
  policy_schema_ref: 'afp:payment-policy:v2',
  status: 'experimental'
}

Publication registers this descriptor for negotiation; it does not activate a route. A v2 quote is acceptable only in a live bilateral session whose profile is exactly pay_venue and whose profile_policy_hash equals the complete v2 policy hash. Parsing the schema, receiving a manifest hash, or finding the descriptor in an implementation registry MUST NOT select, qualify, or enable a deployment.

The policy's trusted deployment is the exact closed record below:

interface VenueDeployment {
  venue_profile: 'zka:aztec:native-pay:v1';
  network_id: string;                     // canonical lowercase aztec:<reference>
  anchor_chain_id: string;                // canonical eip155:<positive-decimal>
  asset_id: string;                       // <anchor_chain_id>/erc20:<lowercase-address>
  asset_commitment: string;               // canonical nonzero BN254 scalar
  contract_class_id: string;              // canonical nonzero BN254 scalar
  contract_instance_id: string;           // canonical nonzero BN254 scalar
  settlement_portal_id: string;           // nonzero lowercase 20-byte address
  manifest_hash: string;                  // sha256:<64 lowercase hex>
  fee_route: 'aztec-protocol-fee-v1';
  receipt_profile: 'zka:aztec:pay-receipt:v1';
  nullifier_semantics: 'aztec-app-siloed-v1';
  max_note_value: '340282366920938463463374607431768211455';
  asset_risk_profile: 'zka:asset-risk:s3-hard:v1';
}

interface AfpPaymentPolicyV2 {
  kind: 'afp:payment-policy:v2';
  deployment: VenueDeployment;
  accepted_rate_sources: string[];
  max_quote_lifetime_seconds: number;
  max_slippage_bps: number;
  required_finality: 'safe' | 'finalized';
}

The deployment owner MUST obtain this tuple from trusted tenant-scoped configuration and independently verify the immutable manifest bytes, class and instance, constructor-fixed portal and asset, asset commitment, chain identities, receipt verifier, and asset-risk evidence. A quote-supplied manifest_hash is only an exact comparison value. It is never trusted configuration, an attestation, or evidence of production eligibility. Unknown venue identifiers and any missing, extra, noncanonical, stale, or mismatched field fail closed.

network_id and anchor_chain_id use canonical restricted CAIP-2 spelling; asset_id uses the corresponding restricted CAIP-19 ERC-20 spelling. The three field identifiers use lowercase 0x plus exactly 64 hexadecimal digits representing an integer in 1..r-1, where r is the §15.4 BN254 modulus. The portal is lowercase 0x plus exactly 40 hexadecimal digits and is nonzero. max_note_value is exactly u128::MAX. The hard-S3 declaration requires an immutable ownerless one-asset deployment and does not inherit evidence from Base or from another asset.

The exact signed settlement, rate, and quote records are:

interface VenueSettlement {
  venue_profile: 'zka:aztec:native-pay:v1';
  network_id: string;
  anchor_chain_id: string;
  asset_id: string;
  contract_instance_id: string;
  settlement_portal_id: string;
  manifest_hash: string;
  recipient_id: string;                   // canonical nonzero BN254 scalar
  amount_atomic: string;                  // positive decimal, at most u128::MAX
  asset_risk_profile: 'zka:asset-risk:s3-hard:v1';
}

interface AfpPaymentQuoteV2 {
  kind: 'afp:payment-quote:v2';
  quote_id: string;
  session_ref: string;
  session_context: string;
  policy_hash: string;
  obligation: { currency: 'USD'; amount_minor: string };
  settlement: VenueSettlement;
  rate: {
    source: string;
    usd_minor: string;
    asset_atomic: string;
    reference_usd_minor: string;
    reference_asset_atomic: string;
  };
  quoted_at: string;
  expires_at: string;
  max_slippage_bps: number;
  quote_nonce: string;
  initiator_signature?: AfpCommitSignature;
  responder_signature?: AfpCommitSignature;
}

Every object is closed: an implementation MUST reject an unknown, missing, or differently encoded member rather than normalize it. All monetary and rate values are positive canonical unsigned decimal strings. recipient_id identifies the private venue recipient, not an L1 withdrawal beneficiary. The deterministic arithmetic is amount_atomic = ceil(obligation.amount_minor * rate.asset_atomic / rate.usd_minor) using arbitrary-precision integers, with the §15.2 cross-multiplied slippage rule applied to the asset-atomic denominators. Fees are not subtracted from or added to the signed amount. Every deployment, asset, recipient, amount, portal, manifest, or policy change requires a newly signed quote.

policy_hash is qb64("D", SHA-256(canonical_json(AfpPaymentPolicyV2))) over the complete policy, including every deployment and finality field. Quote hashing, two-party KEL-current signatures, the complete signed-record acceptance hash, durable quote-ID/nonce reservation, and the §15.4 interaction-context fold otherwise use the unchanged v1 algorithms. quote_content includes policy_hash and every settlement field and omits only the two signature members. Acceptance MUST reparse the exact v2 wire, require the matching live pay_venue session and policy hash, verify both principals, and reject policy substitution. Route execution requires the same sealed, store-reserved capability boundary as §15.5 and MUST submit the exact signed target and amount.

The v2 route evidence contains exactly quote_ref, quote_hash, interaction_context, the complete nested VenueSettlement, and transaction_id. The observational receipt is:

interface AfpPaymentReceiptV2 {
  kind: 'afp:payment-receipt:v2';
  quote_ref: string;
  quote_hash: string;
  interaction_context: string;
  settlement: VenueSettlement;
  transaction_id: string;
  output_note_hash: string;                // canonical BN254 scalar; zero allowed
  private_event_log_position: number;      // integer in 0..2^53-1
  observed_at: string;
  finality: 'pending' | 'safe' | 'finalized';
  finality_evidence: object;
}

The receipt MUST echo every settlement field exactly. A deployment-selected verifier MUST authenticate holder-authorized disclosure of the exact recipient output note, private event occurrence, contract instance, receipt-profile domain, interaction context, transaction, amount, asset, recipient, network, and claimed finality. A matching transaction identifier, public Base batch receipt, unconstrained log, arbitrary non-empty evidence object, or note hash alone is insufficient. Finality advances monotonically as in §15.6, and the first verified (output_note_hash, private_event_log_position) occurrence is immutable across advancement.

The published docs/payment_venue_v2_conformance.json artifact pins the full canonical policy and quote bytes, policy and quote hashes, interaction context, and exact accepted target. Conformance requires those vectors, the unchanged Base vectors, cross-version rejection, exact policy/deployment/receipt matching, replay rejection, and occurrence preservation.

The experimental status is load-bearing. Publication grants no V6 qualification, manifest approval, independent audit, holder backend, asset eligibility, APL route qualification, deployment, fund movement, or production activation. Each remains an independent fail-closed gate owned by its deployment and protocol boundary. Direct L1 withdrawal remains unconditional; a later bridge hop requires a separately accepted route and MUST NOT block or reverse withdrawal. A conforming implementation MUST keep pay_venue disabled until all deployment-specific gates are satisfied and MUST disclose it as published-but-not-activated when they are not.


Appendix A: Key Derivation Core (KDC)

Module: Key Derivation Core KDC Version: 1.0.3 Status: Stable Owning specification: AFP (this document) Referenced normatively by: ZKA v0.9.2-draft (§2.1–2.2), ZKC v0.4.1-draft (§1.3, §2.1, §5.9), ZKM v0.1.0-draft (§2), AFP 0.3.12-draft (§2.2, §4.3, §5.9)

This appendix specifies the Key Derivation Core (KDC) — the master-seed derivation, hash-and-domain-separation scheme, domain-tag registry, and per-entity sub-derivation shared by ZKA, ZKC, ZKM, and AFP. It is a self-contained module: it is published as part of the AFP specification for packaging reasons (AFP is the identity protocol of the family and carries the most complete derivation model), but it depends on no other section of this specification and on no construct specific to AFP, ZKA, ZKC, or ZKM. Sibling protocols reference this appendix without thereby depending on the remainder of AFP.

KDC carries its own version line, independent of the AFP specification version. The active module is AFP-KDC v1.0.3, and a dependent specification cites that version rather than an AFP version. Sections A.3–A.7 and their normative artifacts completely specify its field-domain derivation, Poseidon2 instance, sponge, hierarchy, and conformance vectors. AFP-KDC v1.0.3 reuses the v1.0.2 Poseidon2 parameter and conformance artifacts and defines the external fixed-byte ownership boundary; it changes no KDC-owned tag, derivation, value, vector, hash, proof, circuit, verification key, or wire record.

A.1 Scope

In scope (normative for KDC):

Out of scope (owned by the referencing specifications):

KDC defines derivation. It does not know what the derived keys are used for.

A.2 Master Seed

A master seed is a 256-bit (32-byte) secret. It is the root of a key hierarchy and MUST be generated from a cryptographically secure random source and stored securely.

A master seed may be:

The two cases are interchangeable as far as §A.6 is concerned: §A.6 takes a 256-bit seed and does not distinguish how that seed was obtained. This is what makes a sub-seed M' a drop-in master seed for the key-derivation functions.

A.3 Hash Function and Domain Separation

KDC uses Poseidon2 over the BN254 scalar field for all key-derivation hashing. Poseidon2 is the hash provided by Noir's standard library (std::hash::poseidon2) and is optimized for UltraHonk constraints, so derivations are efficiently provable inside circuits — required because the ZKA identity-binding circuit (ZKA §5.2) re-derives keys from a seed in-circuit.

The BN254 scalar field modulus is

r = 21888242871839275222246405745257275088548364400416034343698204186575808495617

All KDC hashes use domain separation via a leading domain-tag field element:

H(domain, x...) = Poseidon2( deriveFieldTag(domain), x... )

deriveFieldTag is the one field-domain algorithm for both KDC derivation tags and protocol-owned operational tags:

deriveFieldTag(tag) =
  LEInteger(Blake2b-512(UTF8("ZKA-Domain-") || UTF8(tag))) mod r

The 64-byte digest is interpreted as a little-endian non-negative integer before reduction. Implementations MUST NOT substitute direct UTF-8-to-integer encoding, big-endian digest interpretation, a different Blake2 variant, or an unprefixed hash. The byte prefix ZKA-Domain- is frozen compatibility material; it does not assign ownership of an operational tag to ZKA.

encodeFixedTag(tag) is a separate non-field operation. Its input domain is a non-empty string whose UTF-8 encoding is 1–32 bytes and whose first encoded byte is non-zero; implementations MUST reject a leading NUL byte so left-zero-padding remains injective. After validation, left-zero-pad the encoding to exactly 32 bytes. It is used for the §A.5 HKDF salt. A field tag MUST NOT be substituted as HKDF salt. A fixed byte tag MUST NOT be interpreted as a circuit field separator, with exactly one exception: the fixed-byte fold-tag class carved out in §A.4, whose externally-owned tags are consumed directly as field-element domain separators by their owning specification.

A 256-bit seed supplied as a hash input is interpreted as a big-endian integer and reduced modulo r before use as a field element.

Behavioral-identity requirement. Every consumer MUST implement the exact §A.3 permutation, sponge, tag derivation, and §A.6 hierarchy and reproduce all normative vectors. The pinned field constants, seeds-to-keys outputs, proofs, verification keys, hashes, and HKDF vectors are immutable within AFP-KDC v1.0.3; its Poseidon2 parameters and conformance vectors remain the v1.0.2 artifacts.

A.3.1 Poseidon2 Instance

KDC uses exactly one Poseidon2 instance. Any other parameterization is non-conformant even if it is also called “Poseidon2 over BN254.”

Parameter Normative value
Field BN254 scalar field Fr with modulus r above
State width t = 4
Rate 3, lanes 0–2
Capacity 1, lane 3
S-box x^5
Full rounds 8, split 4 before and 4 after the partial rounds
Partial rounds 56
External matrix [[5,7,1,3],[4,6,1,1],[1,3,5,7],[1,1,4,6]]
Internal matrix all-ones plus the pinned diagonal-minus-one values

Treat the state as a column vector and reduce every operation modulo r. Apply the external matrix once before round 0. In rounds 0–3, add all four round constants, apply x^5 to all lanes, then apply the external matrix. In rounds 4–59, add the lane-0 round constant, apply x^5 only to lane 0, then apply the internal matrix. In rounds 60–63, repeat the full-round operation.

docs/kdc_v1_0_2_poseidon2_parameters.json is the normative, self-contained parameter artifact. It fixes all 64 four-lane constant rows, the four internal diagonal-minus-one values, both matrices, and operation order. Its canonical constant encoding concatenates every constant row in round/lane order, followed by the four internal diagonal-minus-one values, with each field encoded as canonical 32-byte big-endian. The encoding contains 260 fields and 8,320 bytes and MUST have SHA-256:

8cb18652f4afeac5b6819b156c13fbbc327deacba568b9d8d961ab134123214c

An implementation that imports, embeds, or loads an explicit parameter table MUST verify this digest before use. An opaque backend intrinsic MUST be immutably identified and reproduce every §A.3.3 KAT.

A.3.2 Sponge Mode

Let P(x[0], …, x[n-1]) be the raw length-bound sponge. Its input is a sequence of canonical Fr elements with 0 <= n <= 2^32 - 1. Initialize [0, 0, 0, n * 2^64]; the length IV is in capacity lane 3. For each complete three-element chunk, add it to rate lanes 0–2 and permute. Add any one- or two-element remainder to the corresponding leading rate lanes. Apply one final permutation if and only if n = 0 or n mod 3 != 0. Return lane 0. There is no delimiter or padding field, and a positive multiple of three receives no additional trailing permutation.

Absorption is additive. P accepts canonical field elements rather than arbitrary integers or bytes. KDC semantic domain separation is explicit and is not supplied merely by arity:

H(domain, x...) = P(deriveFieldTag(domain), x...)

A.3.3 Conformance Known-Answer Tests

For raw inputs consisting of the field elements 1, 2, 3, ... in order, a conforming implementation MUST reproduce:

n Input P output
0 [] 0x18dfb8dc9b82229cff974efefc8df78b1ce96d9d844236b496785c698bc6732e
1 [1] 0x168758332d5b3e2d13be8048c8011b454590e06c44bce7f702f09103eef5a373
2 [1,2] 0x038682aa1cb5ae4e0a3f13da432a95c77c5c111f6f030faf9cad641ce1ed7383
3 [1,2,3] 0x23864adb160dddf590f1d3303683ebcb914f828e2635f6e85a32f0a1aecd3dd8
4 [1,2,3,4] 0x130bf204a32cac1f0ace56c78b731aa3809f06df2731ebcf6b3464a15788b1b9
6 [1,2,3,4,5,6] 0x07f57fcda925c06dc0a311f3f17fa0218e079b514552744a25ba8a74ee8c9e7a
7 [1,2,3,4,5,6,7] 0x16f929bc0d216df4b05bdc44222463edf2b9791bd949ab926eebda06a502d238

n = 0, 3, 6 exercise the conditional trailing-permutation branch in both directions. n = 2, 3, 4 cover the H2/H3/H4 raw-wrapper arities used by consuming runtimes.

docs/kdc_v1_0_2_conformance_vectors.json is the normative machine-readable package. Its rawSponge section contains the table above. Its kdc section covers seed values 0, 1, r-1, r, r+1, the high bit, and an asymmetric 32-byte seed, and pins all six §A.6 outputs. The r/zero and r+1/one aliases are intentional consequences of the normative big-endian reduction.

Every conforming runtime MUST reproduce every shared vector applicable to its published interface. Solidity conformance is limited to the raw arities its published interface implements; it MUST NOT claim unsupported higher arities. Noir, Rust, ZKC, ZKM, and Aztec runners MUST execute every applicable vector rather than merely copying the expected values.

A.4 Domain-Tag Registry

The following are the complete KDC-owned derivation tags in v1.0.3:

Domain tag Used in Purpose
zka/spending §A.6 Derive spending key from seed
zka/viewing §A.6 Derive viewing key from spending key
zka/proof §A.6 Derive proof key from spending key
zka/address §A.6 Derive address from viewing key
zkc/credential §A.6 Derive credential binding key from seed
zkc/binding §A.6 Derive credential commitment from credential binding key

The zka/ and zkc/ prefixes in these six frozen tag strings do not imply ownership by ZKA or ZKC. KDC owns all six derivation tags.

Ownership and collision rule. A tag string is owned by the specification that defines its operation, irrespective of its prefix. A referencing specification MAY define additional operational tags. Every such tag:

Fixed-byte fold-tag carve-out. The ZKA zka/pay/* and zka/coord/* tag families are raw fixed byte tags: each tag's UTF-8 bytes right-aligned in a 32-byte word and consumed directly as a field element, never derived through deriveFieldTag. Like ZKC's frozen aggregation literals below, they are deliberate compatibility constants outside the deriveFieldTag regime and MUST NOT be reinterpreted through it. They are owned by ZKA as the specification defining their folds. A new tag in this class MUST NOT collide with any registered tag string, and collision checking for the class is against ZKA's protocol-family/poseidon2-operational-domains-v1.json (active_fixed_byte rows) in addition to the 27-tag snapshot below. ZKA's registry contains zka/pay/interaction-context/v1 as the pay-interaction-context row, and AFP vendors that registry under the canonical family manifest. The manifest binds both registry copies and both copies of the frozen interaction-context KAT. This ownership boundary changes no KDC-owned tag, derivation, value, or vector.

The active machine-readable interoperability snapshot docs/kdc_v1_0_3_domain_registry.json pins all 27 currently tagged field constants: six KDC derivation tags, four ZKA operational tags, eight ZKC operational tags, and nine ZKM operational tags. It is a KAT and collision registry, not a transfer of tag-string ownership to KDC. A new tag MUST be checked against the complete snapshot before publication. ZKC's frozen aggregation seed/verify literals are not string tags; they remain explicit compatibility constants outside this registry and MUST NOT be reinterpreted through deriveFieldTag. Replacing them with tagged constants requires a versioned circuit/VK migration. docs/kdc_v1_0_2_domain_registry.json remains the unchanged cryptographic/profile predecessor, and docs/kdc_v1_0_1_domain_registry.json remains historical evidence; every tag and field_hex value is identical across all three registry versions.

ZKA, ZKC, and ZKM operational field tags — the deriveFieldTag class — all use the same §A.3 derivation; ZKA's pay/coord fixed-byte fold tags sit outside that class under the carve-out above. The v1.0.3 registry is the active 27-tag KAT and collision snapshot; v1.0.2 is the unchanged cryptographic/profile predecessor, and the preserved v1.0.1 artifact records the version in which those values were introduced.

A.5 Per-Entity Sub-Derivation

A tenant with master seed M derives a distinct sub-seed M' for each entity:

M' = HKDF(M, "entity/{name}")

KDC is the authoritative definition of this sub-derivation, which supplies the per-entity seed used by the AFP entity model (§4.3).

Concrete construction. The sub-derivation uses HKDF per RFC 5869 with SHA-256:

M' = HKDF-SHA256(
        IKM  = M,                            // 32-byte tenant master seed
        salt = encodeFixedTag("kdc/entity-subseed"), // fixed 32-byte domain salt
        info = utf8("entity/" || name),       // entity name, UTF-8
        L    = 32                             // output length in bytes
     )

The output M' is a 32-byte value and is itself a valid §A.2 master seed: it is supplied directly to the §A.6 derivation functions. name is the entity's name as used in its AFP delegated-AID label (AFP §4.2).

Rationale — two primitives by design. KDC deliberately uses HKDF-SHA256 for the seed-tree derivation (M → M') and Poseidon2/BN254 for the key derivation (M'/seed → keys, §A.6). The seed-tree derivation is never performed inside a zero-knowledge circuit — it is wallet- and key-management-layer work — so the well-audited RFC 5869 standard KDF is the appropriate, conservative choice. The key derivation of §A.6 is performed in-circuit (ZKA §5.2 re-derives keys from a seed as circuit constraints), so it must use the circuit-efficient Poseidon2. The boundary between the two primitives is exactly the boundary between non-circuit and circuit derivation. An implementation MUST NOT substitute one primitive for the other.

Implementation note. HKDF-SHA256 is fixed and normative. docs/kdc_v1_0_0_test_vectors.json pins the required sub-seed vectors, and encodeFixedTag("kdc/entity-subseed") is exactly the byte encoding used by those vectors.

A.6 Key-Derivation Functions

Given a 256-bit seed (a standalone master seed per §A.2, or a per-entity sub-seed M' per §A.5), KDC derives the key hierarchy:

                    +-----------------+
                    |   Seed (256b)   |   standalone master seed,
                    +--------+--------+   or per-entity M' (§A.5)
                             |
         +-------------------+-------------------+
         |                   |                   |
         v                   v                   v
  +--------------+   +--------------+   +--------------+
  | Spending key |   | Viewing key  |   |  Proof key   |
  |     (sk)     |   |     (vk)     |   |     (pk)     |
  +--------------+   +--------------+   +--------------+
         |                   |
         |                   v
         |            +--------------+
         |            |   Address    |
         |            |    (addr)    |
         |            +--------------+
         v
  +-----------------------+
  | Credential binding    |
  | key (cbk)             |
  +-----------+-----------+
              |
              v
  +-----------------------+
  | Credential commitment |
  | (holderCommitment)    |
  +-----------------------+
interface KeyHierarchy {
  seed:                 Uint8Array;  // 256 bits — store securely
  spendingKey:          Hash;        // sk
  viewingKey:           Hash;        // vk
  proofKey:             Hash;        // pk
  address:              Hash;        // addr
  credentialBindingKey: Hash;        // cbk
  credentialCommitment: Hash;        // public — holderCommitment in ZKC binding
}

// KDC v1.0.2 key derivation. All hashes use the exact §A.3 Poseidon2 sponge
// and §A.3 domain separation.
function kdcDeriveKeys(seed: Uint8Array): KeyHierarchy {
  const sk   = H("zka/spending",  seed);   // from seed
  const vk   = H("zka/viewing",   sk);     // from sk
  const pk   = H("zka/proof",     sk);     // from sk
  const addr = H("zka/address",   vk);     // from vk
  const cbk  = H("zkc/credential", seed);  // from seed — NOT from sk
  const cc   = H("zkc/binding",   cbk);    // credential commitment

  return { seed, spendingKey: sk, viewingKey: vk, proofKey: pk,
           address: addr, credentialBindingKey: cbk,
           credentialCommitment: cc };
}

Two derivation facts are normative and load-bearing for the referencing specifications:

  1. cbk derives from the seed, not from sk. The credential binding key is H("zkc/credential", seed), a sibling of the spending key, not a descendant of it. This is what lets a holder prove credential ownership without exposing spending authority.
  2. Address and credential commitment are derived along separate paths. address = H("zka/address", H("zka/viewing", H("zka/spending", seed))) and credentialCommitment = H("zkc/binding", H("zkc/credential", seed)) share only the seed. Given one, the other cannot be computed or linked without the seed. ZKA's identity-binding circuit (ZKA §5.2) proves the link in zero knowledge precisely because the link is otherwise unrecoverable. A KDC implementation MUST preserve this separation.

The names of the derived keys are KDC's; how each key is used — sk to spend ZKA notes, vk to decrypt incoming notes, cbk to bind ZKC credentials — is specified by the referencing protocols, not here.

A.7 Conformance and Versioning

A conforming AFP-KDC v1.0.3 implementation:

Test vectors. docs/kdc_v1_0_0_test_vectors.json remains normative for HKDF entity sub-seeds. docs/kdc_v1_0_3_domain_registry.json is normative for field tags, the external fixed-byte ownership contract, and the complete cross-protocol collision scan. The unchanged docs/kdc_v1_0_2_poseidon2_parameters.json and docs/kdc_v1_0_2_conformance_vectors.json remain normative for Poseidon2 and the complete KDC hierarchy. All four artifacts are required for v1.0.3 conformance; the v1.0.2 registry remains the unchanged cryptographic/profile predecessor.

Versioning. AFP-KDC v1.0.3 immutably pins the derivation surface specified by §A.3–§A.6 and its normative artifacts. It requires the v1.0.3 domain registry plus the unchanged v1.0.2 Poseidon2 parameters and conformance vectors. Any change that alters a derived field, seed, key, proof, verification key, or hash requires a major KDC version bump and an explicit compatibility and migration plan across every consumer. A referencing specification cites a specific AFP-KDC version and may assume its derived values are immutable.


License

This specification is released under the Apache 2.0 License.


AFP: Verifiable federation between tenants — with nothing to trust.