Multi-Attestation Payload Format

September 21, 2026 · View on GitHub

Version: 1.2 Status: Draft Date: 2026-09-02 Discussion: insumer-examples#1 Blog posts: Multi-Issuer Verification · Would You Trust Your Agent? KYA Is Real.


Abstract

The Multi-Attestation Payload Format defines a composable envelope for bundling independently signed attestations from multiple issuers into a single verifiable object. Each attestation is self-describing — it carries its own algorithm, key identifier, and JWKS discovery endpoint. No shared registry or coordination between issuers is required. A relying party selects attestations by type, fetches each issuer's public key via standard JWKS, and verifies signatures independently.

This format emerged from convergence across ten independent issuers contributing twelve signed dimensions: InsumerAPI (wallet state, the foundation layer, 37 chains), Revettr (compliance risk), ThoughtProof (reasoning integrity), RNWY (three dimensions: agent-level behavioral trust, operator-level wallet intelligence, AND MCP-server trust), Maiat (job performance), APS (passport grade), AgentID (trust verification), AgentGraph (security posture), SAR (settlement witness), and TrustLayer (cross-chain reputation, 19 chains). Each issuer publishes a JWKS endpoint and signs attestations using either ES256 or EdDSA. The payload format is algorithm-agnostic and supports both raw signatures (base64-encoded P1363) and compact JWS (JWT).


1. Payload Format

{
  "v": 1,
  "attestations": [
    {
      "issuer": "https://api.insumermodel.com",
      "type": "wallet_state",
      "kid": "insumer-attest-v2",
      "alg": "ES256",
      "jwks": "https://insumermodel.com/.well-known/jwks.json",
      "signed": null,
      "sig": "<compact-jws>",
      "expiry": "2026-03-20T13:04:57.000Z"
    }
  ],
  "expired": []
}

Root Object

FieldTypeRequiredDescription
vintegerMUSTPayload format version. Currently 1. Distinct from an issuer's signing-scheme version, which each attestation carries in its own kid.
attestationsarrayMUSTActive, unexpired attestation entries.
expiredarraySHOULDAttestation entries past their TTL. Separated from attestations so relying parties can distinguish stale data without re-checking expiry.

Attestation Entry

FieldTypeRequiredDescription
issuerstring (URI)MUSTCanonical issuer identifier.
typestringMUSTAttestation type (see Section 2). Relying parties select entries by this field, not by position.
kidstringMUSTKey ID for JWKS lookup.
algstringMUSTSigning algorithm. One of ES256, EdDSA.
jwksstring (URL)MUSTWhere this issuer publishes its keys. A discovery hint for relying parties enrolling the issuer, not the trust root: the verifying key comes from the key set the relying party holds or pins for issuer (Section 4, step 3), and this URL MUST match that pinned origin.
signedobject | nullCONDITIONALThe signed payload object. MUST be present when sig is a raw signature. When sig is a compact JWS the payload is embedded in the JWT and this field is not verified, so it MUST be null: an object carried alongside a JWS bears no signature, and a relying party that reads claims from it is reading unsigned data. A verifier MUST refuse such an entry as malformed rather than verify the JWS and report success, since a verifier that returns before examining signed reports a valid signature over an object the signature does not cover. Matching signed against the JWT payload instead is not a general alternative. The relationship between the two is issuer-specific: for some issuers the object is a subset of the JWS payload and the comparison is well defined, while for others the JWT is a different projection of the attestation and there is nothing to compare. Because this format requires no coordination between issuers, a verifier cannot know which convention applies to an entry it is handed, and a check that is meaningless for some issuers can only fall back to accepting them, which reinstates the problem. Rejection is the one rule that holds uniformly. Note for integrators: several issuers return the JWS and its decoded payload as separate fields of the same API response. Carry only the JWS into the entry and set signed to null — nothing is lost, since decoding the JWS recovers the object. An issuer whose signature covers anything other than the serialization of this object, a domain-separated preimage for example, MUST use the compact JWS form: the raw form verifies over signed itself and cannot represent such a signature.
sigstringMUSTEither a base64-encoded raw signature (P1363 format for ES256, raw bytes for EdDSA) or a compact JWS string (three dot-separated base64url segments).
expirystring (ISO 8601)SHOULDExpiration timestamp. If absent, relying parties SHOULD apply a default TTL of 30 minutes from attestedAt (or its snake_case spelling attested_at) / iat / timestamp in the signed payload. That fallback is unavailable on an entry whose sig is a compact JWS, where signed is necessarily null and there is no payload object beside the signature to read a timestamp from; a verifier reads the expiry claim inside the token instead, as section 4 step 1 describes. An entry in that form SHOULD still carry expiry, so that it can be judged by a relying party that does not decode the token, and because a token carrying no expiry claim of its own leaves nothing else to read.

Design Decisions

  • Insertion order is not significant. Relying parties select attestations by type, never by array index.
  • requiredTypes belongs in verifier configuration, not in the payload. The payload is a neutral bundle; policy is the relying party's concern.
  • Only verifiable entries appear in attestations. Unsigned or unverifiable data MUST NOT be included.
  • Self-describing entries. Each attestation carries its own alg, kid, and jwks. No shared key registry. The trust anchor is the key set the relying party holds for the issuer, selected by kid; the entry's jwks says where that set is published and MUST match the origin the relying party has pinned for issuer. A kid that resolves to no key in that set is a failure, not a reason to fetch another key.
  • Signature format is polymorphic, and the two forms are exclusive. If sig contains exactly two dots, it is a compact JWS. Otherwise, it is a base64-encoded raw signature over JSON.stringify(signed). An entry carries one form or the other, never both: a JWS with a non-null signed is malformed, because the object beside it is unsigned data in a field a relying party reads as attested.

Reference Implementation Criteria

The issuer table in Section 2 is this spec's reference set. Participation in the discussion thread (insumer-examples#1) is open and is not by itself a reference — entries are added to the table only after meeting the criteria below.

To be added to the reference set, an implementation MUST:

  1. Publish a JWKS endpoint at a stable URL, returning a JWK set containing the kid referenced in the attestation entry.
  2. Sign attestations end-to-end. The sig field MUST verify against the public key published at the JWKS endpoint, over the canonical bytes of the signed payload — header.payload for compact JWS, or either insertion-order JSON.stringify(signed) or sorted-key (canonical) JSON for ES256 raw P1363 and EdDSA raw (the reference verifier accepts both for both algorithms).
  3. Be reproducible by a third-party verifier. The reference verifier (multi-attest-verify.js) MUST resolve the JWKS, fetch a live attestation, and return a verified result with no issuer cooperation beyond the published endpoints.

When all three conditions hold against a live attestation, the implementation is added to the Section 2 table as a live issuer.

Schema reservations, aspirational commitments, or proposed attestation dimensions that have not shipped a live JWKS and a verifiable attestation are not in the reference set. They may be tracked elsewhere as future work.


2. Attestation Types

TypeIssuerAlgorithmSignature FormatDefault TTL
wallet_stateInsumerAPIES256base64 P1363 (or JWT when format: "jwt" requested)30 min
compliance_riskRevettrES256compact JWS (JWT)1 hour
reasoning_integrityThoughtProofEdDSA (Ed25519)compact JWS (JWT)per-issuer
behavioral_trustRNWYES256base64 P1363 + compact JWS (kid rnwy-trust-v2, legacy rnwy-trust-v1)24 hours
wallet_intelligenceRNWYES256compact JWS (JWT, kid rnwy-wallet-v1)24 hours
mcp_trustRNWYES256compact JWS (JWT, kid rnwy-mcp-v1)24 hours
job_performanceMaiatES256compact JWS (JWT)30 min
passport_gradeAPSEdDSA (Ed25519)compact JWS (JWT)per-issuer
trust_verificationAgentIDEdDSA (Ed25519)compact JWS (JWT)1 hour
security_postureAgentGraphEdDSA (Ed25519)compact JWS (JWT)24 hours
settlement_witnessSAREdDSA (Ed25519)compact JWS (JWT, kid sar-prod-ed25519-06 current; -05/-03/-02/-01 legacy)per-issuer
cross_chain_reputationTrustLayerES256base64url P1363 over canonical (sorted-key) JSONper-issuer

2.5 Two Categories of Wallet Binding

An analytical split across the envelope worth naming because it clarifies how composition policies should weight signals. Both categories are cryptographically verifiable and both fit the envelope. They answer different questions, and neither is weaker than the other.

Wallet-bound identity dimensions. The signed JWS payload contains the wallet itself. A verifier holding only the signed bytes can prove "this specific wallet → this signal." The binding is cryptographic end-to-end.

DimensionProviderSigned field
Wallet state (foundation, 37 chains)InsumerAPIwallet (EVM via /v1/trust) / JWT sub (non-EVM via /v1/attest)
Behavioral trust (agent)RNWY v2owner
Wallet intelligence (operator)RNWY rnwy-wallet-v1sub, wallet
Job performanceMaiatsub, agent
Compliance riskRevettrsub
Identity verificationAgentID v1.1.0bound_addresses, solana_address, wallet_address
Passport grade (governance)APS gateway-v1wallet_ref[].address (envelope JWS, gateway key) + wallet_ref[].binding_sig (per-entry, passport pubkey)
Settlement witness (new receipts)SAR sar-prod-ed25519-06, profile settlement-witness-verified-v0.2-counterparty-boundcounterparty
Reasoning integrity (wallet-indexed)ThoughtProof tp-attestor-v1wallet (via /v1/issuer/wallet/{wallet})
Cross-chain reputationTrustLayer trustlayer-signing-1wallet

APS has a two-layer binding model worth naming. The wallet_ref[] array is inside the envelope-level Ed25519 JWS signed by the gateway-v1 key, which proves "the APS gateway attested that this agent has these bound wallets at the named bound_at timestamps." Each entry additionally carries a per-wallet binding_sig — a separate Ed25519 signature over the canonical binding payload {passport_id, chain, address, bound_at} (via the reference canonicalize() algorithm), signed by the passport's own private key. The per-wallet signature verifies against the passport pubkey (published in a fixture for the canonical aeoess-bound-demo test passport, and in the passport object itself for production passports). Both layers verify offline and compose: the gateway layer says "our infrastructure observed this binding," and the passport layer says "the passport holder cryptographically claimed this binding themselves." Consumers can require either or both layers depending on their trust model.

Wallet-discoverable content dimensions. The signed JWS payload commits to what is being attested about (a repo, a task outcome, a delivery record). The wallet is a lookup key that discovers the relevant signed subject. A verifier holding only the signed bytes can prove "this repo scored 100" or "this task outcome matched spec" — but not "this wallet owns this repo." This is not a limitation; it is the correct architectural shape for dimensions that attest to things rather than identities.

DimensionProviderSigned subject
Security postureAgentGraphgithub:owner/repo
MCP-server trustRNWY (rnwy-mcp-v1)server ({owner}/{repo})

Under the SAR settlement-witness-verified-v0.2-counterparty-bound receipt profile (kid sar-prod-ed25519-06), the counterparty field is inside signed bytes, placing settlement_witness in the wallet-bound category. Legacy receipts signed under kid -02 or -01 remain wallet-discoverable via the /settlement-witness/receipts?wallet={address} transport lookup.

ThoughtProof ships both shapes. Its original reasoning_integrity verdict (POST /v1/verify, /v1/check) commits to a claim_hash (SHA-256 of a natural-language reasoning claim); the wallet does not appear in those signed bytes, which is correct for attesting the soundness of a reasoning chain — a property of the action, not the actor. As of 2026-04-11, ThoughtProof also ships a wallet-indexed variant at GET /v1/issuer/wallet/{wallet} (no API key) returning a wallet_reasoning_integrity/v1 envelope with the wallet inside the signed bytes alongside verdict, score_normalized, confidence_bps, and supporting evidence. NOT_FOUND envelopes are signed too, so consumers get a verifiable answer either way. That endpoint moves the reasoning-integrity signal into the wallet-bound category for wallet-indexed lookups — hence its row in the wallet-bound table above. See §3.2 for the wallet-indexed schema.

RNWY's MCP-server trust is server-subject. As of 2026-05-24, RNWY signs a third dimension at GET /api/mcp-attestation?server={owner}/{repo} (kid rnwy-mcp-v1, ES256 compact JWS), scoring an MCP server's quality and risk. The signed subject is the server identifier ({owner}/{repo}), not a wallet — there is no wallet entry point on this endpoint, so it is wallet-discoverable/entity-subject like AgentGraph, and is not orchestrated by wallet-keyed consumers. See §3.12 for the schema.


3. Per-Issuer Schemas

3.1 InsumerAPI — wallet_state (foundation layer)

InsumerAPI is the foundation layer. It reads wallet state across 37 chains (31 EVM + Solana + XRPL + Bitcoin + Tron + Stellar + Sui) and establishes the chain context every other dimension composes on top of. The other eleven dimensions answer specialized questions; the foundation answers "what does this wallet actually hold and do on-chain."

Privacy-preserving on-chain verification. Returns signed booleans. No balances exposed.

Endpoint routing by wallet format:

  • EVM walletsPOST /v1/trust — curated multi-chain trust profile. Returns an ECDSA-signed fact profile across stablecoins, governance tokens, NFTs, staking positions, and institutional stablecoins. An EVM wallet is the mandatory anchor for this endpoint.
  • Non-EVM wallets (Solana, XRPL, Bitcoin, Tron, Stellar, Sui)POST /v1/attest with format: "jwt" and chain-appropriate conditions. The wallet lands in the signed JWT sub claim, making the binding cryptographic even for non-EVM formats.
PropertyValue
Issuer URIhttps://api.insumermodel.com
AlgorithmES256 (ECDSA P-256)
Key IDinsumer-attest-v2 (see below)
JWKShttps://insumermodel.com/.well-known/jwks.json
AlsoGET /v1/jwks (API endpoint, 24h cache)

The JWKS publishes five entries over two keys. Three EC entries share the same P-256 key: insumer-attest-v1 (legacy attest and trust), insumer-attest-v2 (v2 attest), and insumer-trust-v2 (v2 trust). Two RFC 9964 AKP entries, appended after them, publish the ML-DSA-65 key for the post-quantum companion under insumer-attest-pq1 and insumer-trust-pq1 (see the companion note below). Every key created on or after the v2 rollout is v2. The kid on each response selects both the key and the signing scheme, so a verifier reads it rather than assuming one, and selects by kid, never by position in the set.

Getting started: Free API key, no credit card. Returns the key immediately.

curl -X POST https://api.insumermodel.com/v1/keys/create \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","appName":"my-app","tier":"free"}'

Docs: insumermodel.com/developers

Signed payload fields (these fields are covered by the signature under both signing schemes):

FieldTypeDescription
idstringUnique attestation identifier (e.g., ATST-BCB27849413440C7).
passbooleanAggregate result — true if all conditions met.
resultsarrayPer-condition results.
results[].conditionnumberZero-based condition index.
results[].labelstringCaller-supplied label.
results[].typestringCondition type (e.g., token_balance, nft_ownership).
results[].chainIdnumber | stringChain ID where the condition was evaluated.
results[].metbooleanWhether this individual condition was satisfied.
results[].evaluatedConditionobjectThe evaluated condition parameters (type, chainId, contractAddress, operator, threshold, etc.).
results[].conditionHashstring0x-prefixed SHA-256 hash of the canonical (sorted-key) evaluated condition JSON.
results[].blockNumberstring0x-prefixed hex block number at evaluation time (EVM chains, when available).
results[].blockTimestampstringBlock timestamp (when available).
attestedAtstringISO 8601 timestamp of attestation creation.

Not signed (present in the API response but NOT covered by the signature):

FieldTypeDescription
passCountnumberNumber of conditions that passed.
failCountnumberNumber of conditions that failed.
expiresAtstringISO 8601 expiration timestamp (30 minutes from attestedAt).

Signature: Selected by kid. Keys issued before the v2 rollout sign a base64-encoded P1363 (r || s, 64 bytes) raw signature over JSON.stringify(signed), where signed = { id, pass, results, attestedAt }. Keys created on or after it, which is every key issued today, sign a domain-separated preimage instead: the tag insumer.attestation.v2, a newline, then the canonical JSON of { v: 2, id, pass, results, attestedAt }, with keys sorted lexicographically and recursively rather than left in insertion order. The bare object is therefore not the signed bytes under v2, which is why the raw form cannot represent such an attestation and the compact JWS form carries it in an envelope. See the note below and the State Attestation Specification.

Optional JWT format: When requested with format: "jwt", the API also returns an ES256 JWT with claims: iss, sub (wallet address), jti (attestation ID), iat, exp (matching the attestation's expiry: +1800s, or +300s when the request carries an erc7710_delegation condition), pass, conditionHash[], blockNumber, blockTimestamp, results[].

Carrying an InsumerAPI attestation in an envelope. Request it with format: "jwt" and place the returned JWT in sig with signed set to null. Keys created on or after the v2 rollout sign a domain-separated preimage rather than the bare attestation object, so the raw form, which verifies a signature over the serialization of signed, cannot carry them. The JWT is signed with the same key and selected by the same kid, so it verifies against the same JWKS. Populate expiry on the entry as well: with signed set to null there is no attestedAt for a relying party to fall back to, and the JWT's own exp claim sits inside the signature rather than beside it. A verifier that decodes the token reads that claim regardless (section 4 step 1), but expiry lets the entry be judged without decoding, and is the only signal at all on a token carrying no expiry claim of its own.

The post-quantum companion and the envelope. Since 2026-09-01 every InsumerAPI attestation and trust profile also carries an ML-DSA-65 companion signature beside the classical one: pqSig and pqKid on the raw response, and a pqJwt beside jwt on the JWT format, resolved from the two AKP entries in the same JWKS. The envelope entry stays classical. An entry carries one classical sig under one classical kid and alg (ES256 or EdDSA); the entry schema in section 1 has no companion slot, and this version does not add one. A companion may travel alongside the entry, for instance as pqSig and pqKid or pqJwt members an assembler leaves on it, but the envelope verifier does not evaluate those members, the per-slot verdict does not include them, and nothing in the envelope covers or binds them to the entry. A relying party that wants the companion verdict verifies each InsumerAPI entry's original response with insumer-verify, which reports the companion as its own verdict (verified, refuted, absent, unverifiable) beside the classical checks, under the policy in the State Attestation Specification (Check 6).

3.2 ThoughtProof — reasoning_integrity

AI reasoning verification. Attests to the integrity and diversity of model reasoning behind a claim.

PropertyValue
Issuer URIhttps://api.thoughtproof.ai
AlgorithmEdDSA (Ed25519)
Key IDtp-attestor-v1
JWKShttps://api.thoughtproof.ai/.well-known/jwks.json
SDKthoughtproof-sdk on npm (v0.2.1)

Getting started: Free operator key, or pay per-call via x402 (USDC on Base) with no key.

curl -X POST https://api.thoughtproof.ai/v1/operators \
  -H "Content-Type: application/json" \
  -d '{"name":"my-agent","email":"you@example.com"}'

Docs: thoughtproof.ai/api

Signed payload fields (JWT claims):

FieldTypeDescription
verdictstringOne of ALLOW, HOLD, UNCERTAIN, DISSENT.
confidencenumberConfidence score.
mdinumberModel Diversity Index — measures reasoning diversity.
claimHashstringsha256:... hash of the original claim.
domainstringDomain of the claim (e.g., financial).
stakeLevelstringStake level of the verification.
timestampstringISO 8601 timestamp.

Signature: Compact JWS (JWT) with EdDSA (Ed25519).

Wallet-indexed variant (shipped 2026-04-11, no API key):

curl https://api.thoughtproof.ai/v1/issuer/wallet/0x0000000000000000000000000000000000001004

Returns a wallet_reasoning_integrity/v1 envelope that puts the queried wallet inside the signed bytes:

FieldTypeDescription
walletstringThe queried wallet — inside signature scope.
foundbooleanWhether ThoughtProof holds a reasoning receipt for this wallet. NOT_FOUND envelopes are signed too.
verdictstringVERIFIED / NOT_FOUND.
score_normalizednumberNormalized reasoning-integrity score (0–1).
confidence_bpsnumberConfidence in basis points.
evidenceobjectSupporting attestation evidence (counts, latest attestation hash).
issuedAt / expiresAtstringISO 8601 validity window.
signatureobject{ alg, kid, value } — detached EdDSA.

Signature (wallet-indexed): Detached EdDSA over the recursively sorted-key compact JSON of the payload (every field except signature) — i.e. json.dumps(payload, sort_keys=True, separators=(",", ":")). signature.value is base64url. This is the wallet-bound surface referenced in §2's wallet-bound table.

3.3 RNWY — behavioral_trust

On-chain behavioral trust scoring with sybil detection across ERC-8004, Olas, Virtuals, and SATI (Solana) agent registries. Dual-score architecture: Signal Depth (behavioral observability) and Risk Intensity (sybil/fraud risk) are independent axes — collapsing them into a single number loses information. Keyless (no API key required).

PropertyValue
Issuer URIhttps://rnwy.com
AlgorithmES256 (ECDSA P-256)
Key IDrnwy-trust-v2 (current, shipped 2026-04-10) · rnwy-trust-v1 (legacy, compat window)
JWKShttps://rnwy.com/.well-known/jwks.json
On-chain oracle0xD5fdccD492bB5568bC7aeB1f1E888e0BbA6276f4 (Base, 150K+ agents)
SDKrnwy-sdk on npm
Default TTL24 hours (nightly pipeline refresh at 3 AM UTC)

Getting started: No API key required. Install the SDK and start querying.

npm install rnwy-sdk

Docs: rnwy.com/api

Coverage: 150,000+ agents indexed across ERC-8004, Olas, Virtuals, and SATI (Solana). 121,000+ wallets scored. 12 EVM chains + Solana. 1.7M+ on-chain commerce jobs indexed.

Signed payload fields:

FieldTypeDescription
agentIdnumberAgent identifier.
chainstringChain where the behavior was evaluated (e.g., base).
registrystringRegistry identifier (erc8004, olas, sati).
scorenumberTrust score (0–95). Capped at 95; no agent achieves perfect observability.
tierstringTrust tier: flagged, limited, developing, established.
badgesarrayEarned badges and warnings (e.g., original_owner, low_history_reviewers, sybil_heavy).
sybilSeveritystringSybil risk severity: none, low, moderate, or heavy.
sybilSignalsarraySpecific sybil indicators: sweep_pattern, inhuman_velocity, score_clustering, coordination, common_funder.
attestedAtstringISO 8601 attestation timestamp.

Signature: Base64-encoded P1363 (r || s, 64 bytes) over JSON.stringify(signed). As of 2026-05-24, the trust-check endpoint additionally returns a standard compact JWS in a jws field alongside the raw sig (both over the same rnwy-trust-v2 payload), so a standard JOSE library verifies out of the box; the raw sig remains for backward compatibility.

3.3.0 rnwy-trust-v2 — upgraded signed payload (current)

As of 2026-04-10, RNWY ships rnwy-trust-v2 with an expanded signed payload putting the wallet directly in signature scope — cryptographic wallet→score binding end-to-end.

Signed block (rnwy-trust-v2): agentId, chain, registry, owner, score, tier, badges, sybilSeverity, sybilSignals, issuedAt, verifiedAt, expiry.

  • owner is the wallet address — this is the wallet-bound field that moves RNWY from wallet-discoverable to wallet-bound in the taxonomy above.
  • Chain auto-resolves from the highest-scoring agent owned by the wallet — no ?chain= parameter required for a wallet lookup.
  • Unknown wallets return a signed found: false envelope: { found: false, wallet, issuedAt } with a full ES256 signature. Cryptographic proof of absence rather than unsigned JSON. Consumers that want to deny-list on "no positive signal" can rely on a verifiable negative claim.

Wallet-based lookup: call the trust-check endpoint with a wallet address — RNWY resolves to the highest-scoring owned agent and returns the signed rnwy-trust-v2 envelope. No chain parameter required.

3.3.1 Evidence Extension (proposed)

The following evidence fields are served by the explorer API and are not yet covered by the signed payload. The proposal is to incorporate them into the signed object in a future update, making the evidence verifiable end-to-end.

Dual scores (independent axes, not one number):

ScoreRangeZonesDescription
signal_depth0–95Minimal / Emerging / Established / DeepBehavioral observability: on-chain activity, commerce history, review patterns, wallet tenure. Capped at 95 — no agent achieves perfect observability.
risk_intensity0–100Clean / Low / Elevated / SevereSybil and fraud risk: wallet funding patterns, review velocity, sweep detection, score clustering.

Evidence fields:

FieldTypeDescription
wallet_age_daysnumberWallet age in days.
wallet_age_scorenumberWallet age score (0–100).
agent_registered_daysnumberDays since agent registration.
is_original_ownerbooleanWhether the registering wallet still owns the agent.
transfer_countnumberNumber of ownership transfers.
total_feedbacknumberTotal reviews received.
reviewer_diversity_rationumberRatio of unique reviewers to total reviews.
reviewer_burst_pctnumberPercentage of reviews in the densest 24-hour window.
reviewer_spread_scorenumberTemporal distribution across review period (0 = all clustered).
sybil_flagsnumberNumber of independent sybil signals firing.
sybil_severitystringSybil risk severity level.
sybil_weighted_scorenumberWeighted sybil composite score.
sybil_signalsarrayActive sybil indicators (see signed payload).
reviewer_credibility.pct_low_historynumberPercentage of reviewers with low-history wallets.
reviewer_credibility.dominant_age_bucketstringMost common reviewer wallet age bucket.
reviewer_credibility.labelstringCredibility label (Not Credible, Low, Moderate, High).
transaction_backed_review_pctnumberPercentage of reviews tied to verifiable on-chain commerce.
commerce_jobs_completednumberVerifiable on-chain commerce jobs.
commerce_circularity_pctnumberSelf-dealing detection — fraction of commerce looping back to owner.
registration_quality_scorenumberMetadata completeness and connectivity score.

Sybil detection signals (first-class, not bolted on):

SignalDescription
common_funderMultiple reviewer wallets funded by the same source.
inhuman_velocityReview submission rate exceeding human capability.
sweep_patternReviewers spread across hundreds of agents without returning.
score_clusteringReviewers consistently assigning identical scores.
coordinationAgent-level modifier detecting coordinated reviewer behavior.

Reference case: Agent Base #1380 — 1,520 reviews, score of zero. 99.7% of reviewers have wallets created the same day they reviewed, four sybil signals firing, 0% of reviews tied to on-chain commerce. A star-counting system would rank it highly.

Live endpoints:

EndpointURL
Trust check (signed)GET https://rnwy.com/api/trust-check?chain=base&id={agentId}
Explorer (full evidence)GET https://rnwy.com/api/explorer?id={agentId}&chain=base
Explorer (web)https://rnwy.com/explorer/{chain}/{agentId}
JWKShttps://rnwy.com/.well-known/jwks.json
On-chain oracle0xD5fd...e4 on Base

3.4 Maiat — job_performance

Agent job performance scoring. Keyless (no API key required, rate-limited to 10 req/min).

PropertyValue
Issuer URIhttps://app.maiat.io
AlgorithmES256 (ECDSA P-256)
Key IDmaiat-trust-v1
JWKShttps://app.maiat.io/.well-known/jwks.json
Default TTL30 minutes

Getting started: No API key required. Call the API directly or install the SDK.

npm install @jhinresh/maiat-sdk

Docs: github.com/JhiNResH/maiat-protocol

Signed payload fields (JWT claims):

FieldTypeDescription
agentstringAgent identifier.
scorenumberJob performance score.
completionRatenumberJob completion rate.
sybilFlagsarraySybil indicators.
jobCountnumberTotal jobs completed.
tierstringPerformance tier.
attestedAtstringISO 8601 attestation timestamp.

Signature: Compact JWS (JWT) with ES256.

3.5 APS (Agent Passport System) — passport_grade

Agent identity verification with graded passports. Measures how deeply an agent's identity has been verified, and cryptographically binds the passport to one or more wallet addresses via a per-wallet signature architecture.

PropertyValue
Issuer URIhttps://gateway.aeoess.com
AlgorithmEdDSA (Ed25519)
Key IDgateway-v1
JWKShttps://gateway.aeoess.com/.well-known/jwks.json
SDKagent-passport-systemgithub.com/aeoess/agent-passport-system
Reference verifierverifyBoundWallet() in src/v2/wallet-binding/bind.ts

Getting started: No API key required. Three endpoints cover agent-first, wallet-first, and attestation retrieval flows.

# Agent-first lookup — agent_id → envelope (warm step required before /attestation)
curl https://gateway.aeoess.com/api/v1/public/trust/{agent_id}
curl https://gateway.aeoess.com/api/v1/public/trust/{agent_id}/attestation

# Wallet-first reverse index — address → envelope with wallet_ref[] populated
curl https://gateway.aeoess.com/api/v1/public/trust/by-wallet/{address}

The reverse index endpoint was shipped 2026-04-10 and enables SkyeProfile-style orchestrators to start from a wallet address, resolve the bound passport, and fetch the signed attestation without needing to know the agent_id in advance. It returns found: false with a reason field for unbound wallets. The attestation endpoint is cache-backed by agent_id; a warm GET on /trust/{agent_id} is required before /trust/{agent_id}/attestation returns a signed JWS (cold requests return 404).

Docs: github.com/aeoess/agent-passport-system

Signed payload fields (envelope, Ed25519 JWS signed by gateway-v1):

FieldTypeDescription
agent_idstringPassport identifier (e.g., aeoess-bound-demo).
gradenumberPassport grade (0-3).
grade_labelstringHuman-readable grade label.
risk_levelstringRisk assessment level.
context_continuityobjectContext continuity metrics.
has_delegationbooleanWhether the agent has active delegation.
has_walletbooleanLegacy envelope-level flag indicating any wallet is registered.
wallet_refarrayBound wallets, each with {chain, address, bound_at, binding_sig}. Inside envelope signature scope.
matched_walletobjectThe specific wallet_ref[] entry that matched the query, when the lookup was wallet-first.
evaluatedAtstringISO 8601 evaluation timestamp.

Signature: Compact JWS (JWT) with EdDSA (Ed25519), signed by the gateway-v1 key.

Per-wallet binding signatures (wallet_ref[].binding_sig) — the strict layer.

Each entry in wallet_ref[] carries its own binding_sig, a raw Ed25519 signature independent of the envelope JWS. The signature is over the canonical payload:

canonicalize({
  passport_id: <string>,
  chain: <string>,        // e.g. "ethereum", "base"
  address: <string>,      // wallet address, case-preserving
  bound_at: <string>      // ISO 8601 with millisecond precision
})

where canonicalize() is the reference algorithm in src/core/canonical.ts — sort keys alphabetically, strip null/undefined, compact JSON (no whitespace). The binding_sig is signed by the passport's own private key, not the gateway key. This gives the wallet binding two independent cryptographic layers:

  1. Envelope layer (gateway-v1 JWS) — proves "the APS gateway's infrastructure observed and attested this binding at the named timestamp." Verifiable against https://gateway.aeoess.com/.well-known/jwks.json.
  2. Per-wallet layer (binding_sig against passport pubkey) — proves "the passport holder themselves cryptographically committed to this binding." Verifiable against the passport's public key, which lives in the passport object itself for production passports, and in a published fixture file for the canonical aeoess-bound-demo test passport.

Both layers verify offline. A consumer wanting the strongest possible "this wallet is bound to this passport" guarantee can require both. A consumer accepting the gateway's observation alone can verify only the envelope layer.

Strict verification path for the aeoess-bound-demo fixture (canonical reference implementation):

// Reference implementation in insumer-examples/wallet-resolve.js: verifyAPSWalletRefBindings()
const fixture = await fetch('https://raw.githubusercontent.com/aeoess/agent-passport-system/main/tests/fixtures/wallet-binding/aeoess-bound-demo.json').then(r => r.json());
const pubKey = createPublicKey({
  key: Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), Buffer.from(fixture.fixture_public_key, 'hex')]),
  format: 'der', type: 'spki'
});
for (const ref of envelope.wallet_ref) {
  const payload = canonicalize({
    passport_id: envelope.agent_id,
    chain: ref.chain,
    address: ref.address,
    bound_at: ref.bound_at
  });
  const ok = verify(null, Buffer.from(payload, 'utf8'), pubKey, Buffer.from(ref.binding_sig, 'hex'));
  // ok === true means this wallet is cryptographically bound to the passport
}

For production passports (non-fixture), the same verification logic applies, but the pubkey is fetched from the passport object's publicKey field rather than the fixture file. The canonical payload shape and canonicalization are identical.

Wallet-binding category: APS is wallet-bound at both layers. The wallet_ref[] array is inside the envelope signature scope, and each entry's binding_sig is inside an independent per-entry signature scope. A verifier holding only the signed bytes can prove "this specific wallet → this passport" twice over.

3.6 AgentID — trust_verification

Behavioral reliability scoring for AI agents. Measures trust level, behavioral risk, and context continuity.

PropertyValue
Issuer URIhttps://getagentid.dev
AlgorithmEdDSA (Ed25519)
Key IDagentid-2026-03
JWKShttps://getagentid.dev/.well-known/jwks.json
SDKgetagentid on PyPI
Default TTL1 hour

Getting started: Free account, or use the public endpoints with no key.

# Verify any agent (no key required)
curl -X POST https://getagentid.dev/api/v1/agents/verify \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "agent_xxx"}'

# Get trust header (EdDSA JWT, no key required)
curl "https://getagentid.dev/api/v1/agents/trust-header?agent_id=agent_xxx"

Docs: getagentid.dev/docs

Signed payload fields (JWT claims, schema version: "1.1.0" as of 2026-04-10):

FieldTypeDescription
versionstringSchema version — currently "1.1.0".
agent_idstringAgent identifier.
trust_levelnumberNumeric trust level.
trust_level_labelstringHuman-readable trust level (e.g., "L2 — Verified").
context_continuity_scorenumberContext continuity metric.
behavioral_risk_scorenumberBehavioral risk assessment.
scarring_scorenumberHistorical negative signal accumulation.
negative_signalsnumberCount of negative signals.
resolved_signalsnumberCount of resolved negative signals.
attestation_countnumberTotal attestations issued for this agent.
didstringDecentralized identifier (did:web:getagentid.dev:agent:{id}).
solana_addressstringSolana address bound to this agent (when present).
wallet_addressstringEVM wallet address bound to this agent (when present).
wallet_chainstringChain identifier for wallet_address (when present).
bound_addressesstring[]All wallet addresses bound to this agent.
subject_bindingstringBinding type indicator — "wallet_bound" when the signed payload includes a wallet.
evaluatedAtstringISO 8601 evaluation timestamp.

Signing key unchanged: kid remains agentid-2026-03. The schema was extended in place via the version field; no key rotation.

Wallet lookup: GET /api/v1/agents/trust-header?wallet={address} — OR-filter match on solana_address or wallet_address.

Multi-category endpoint: GET /api/v1/agents/attestation?agent_id={id}&category={identity|behavioral|continuous-monitoring|key-lifecycle} — returns a per-category JWS-signed envelope. Wallet binding lives in the identity category specifically.

Signature: Compact JWS (JWT) with EdDSA (Ed25519).

3.7 AgentGraph — security_posture

Source code vulnerability scanning for AI agents. Answers: has this agent's code been scanned, and what is the severity profile?

PropertyValue
Issuer URIhttps://agentgraph.co
AlgorithmEdDSA (Ed25519)
Key IDagentgraph-security-v1
JWKShttps://agentgraph.co/.well-known/jwks.json
Default TTL24 hours

Getting started: No API key required. Any scanned entity returns a signed attestation.

# Entity lookup
curl https://agentgraph.co/api/v1/entities/{entity_id}/attestation/security

# Wallet-scoped scan lookup (resolves wallet → scanned entity → signed attestation)
curl "https://agentgraph.co/api/v1/public/scan/wallet/{wallet}?chain=ethereum"

Docs: github.com/agentgraph-co/agentgraph

Category: wallet-discoverable content dimension — the signed subject.id is github:owner/repo (the thing being scanned), not the wallet. The wallet is a discovery key. Consistent with the security posture semantic: the scan evaluates code, not identity.

Signed payload fields (JWT claims):

FieldTypeDescription
typestringSecurityPostureAttestation
issuerobject{ id, name, url } — issuer metadata.
subjectobject{ id, entity_id, display_name } — scanned entity.
scan.resultstringclean, warnings, or critical.
scan.findingsobject{ critical, high, medium, total } — finding counts by severity.
scan.checksobjectBoolean checks: no_critical_findings, no_high_findings, has_readme, has_license, has_tests.
scan.positiveSignalsarraySecurity best practices detected.
scan.filesScannednumberNumber of files analyzed.
scan.frameworkstringDetected framework (mcp, langchain, crewai, etc.).
trust.overallnumberComposite trust score (0.0–1.0).
issuedAtstringISO 8601 attestation timestamp.
expiresAtstringISO 8601 expiration timestamp.

Signature: Compact JWS (JWT) with EdDSA (Ed25519).

3.8 SAR (SettlementWitness) — settlement_witness

Post-execution delivery attestation. Answers: was the task actually delivered as specified?

PropertyValue
Issuer URIhttps://defaultverifier.com
AlgorithmEdDSA (Ed25519)
Key IDsar-prod-ed25519-06 (current, since the 2026-08 repair) · -05 / -03 / -02 / -01 (legacy, compat)
JWKShttps://defaultverifier.com/.well-known/jwks.json

Getting started: /attest requires an enrolled caller key (since 2026-08-29): send it as a Bearer token with a unix-seconds timestamp and a fresh nonce on every request (the issuer keeps a replay ledger). Ask SettlementWitness for a key. /receipts stays public.

# Attest a task outcome
curl -X POST https://defaultverifier.com/settlement-witness/attest \
  -H "Authorization: Bearer $SAR_API_KEY" \
  -H "X-Settlement-Timestamp: $(date +%s)" \
  -H "X-Settlement-Nonce: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{"task_id":"example","spec":{"checks":[{"kind":"field_equals","inputs":{"output_path":"$.status"},"expected":"ok"}]},"output":{"status":"ok"},"receipt_profile":"settlement-witness-verified-v0.2-counterparty-bound","counterparty":"{address}"}'

# Wallet-indexed receipt history (signed receipts where the wallet is the counterparty)
curl "https://defaultverifier.com/settlement-witness/receipts?wallet={address}"

Docs: github.com/nutstrut

Category: wallet-bound when the request names receipt_profile: settlement-witness-verified-v0.2-counterparty-bound (the counterparty lands inside the signed bytes, and rebinding the wallet invalidates the receipt); wallet-discoverable otherwise via the /receipts?wallet= transport lookup. Verdicts: the v0.2 deterministic evaluator needs spec.checks[] (field_equals over an output_path); a spec without checks returns INDETERMINATE / CONDITION_NOT_EVALUABLE. Binding and verdict are independent. The signed payload also carries an informational x402 fee notice (requested_not_enforced).

Signed payload fields (JWT claims, kid sar-prod-ed25519-06):

FieldTypeDescription
task_id_hashstringsha256:... hash of the task identifier.
verdictstringPASS, FAIL, or INDETERMINATE.
confidencenumberConfidence score (0.0–1.0).
reason_codestringReason for the verdict (e.g., SPEC_MATCH).
tsstringISO 8601 timestamp.
verifier_kidstringKey ID used for signing.
receipt_idstringsha256:... derived from the signed core.
counterpartystringWallet address, inside signature scope under the -counterparty-bound receipt profile (first shipped with kid -03 on 2026-04-10, dropped during the 2026-08 repair, restored 2026-09-09). Makes settlement_witness a wallet-bound dimension.

Signature: Compact JWS (JWT) with EdDSA (Ed25519).

Legacy receipts signed under kids -01 or -02 do not contain counterparty in the signed bytes and remain wallet-discoverable only via the /receipts?wallet= transport lookup.

3.9 Revettr — compliance_risk

Counterparty risk scoring. Answers: is the wallet on a sanctions list, does it look like a clean counterparty, what is the regulatory exposure?

PropertyValue
Issuer URIdid:web:revettr.com
AlgorithmES256 (P-256)
Key IDrevettr-attest-v1
JWKShttps://revettr.com/.well-known/jwks.json
Default TTL1 hour

Getting started: No API key required. Keyless POST /v1/attest accepts a wallet address and returns a signed compliance risk attestation. Rate-limited to 10 requests per minute per IP.

curl -X POST https://revettr.com/v1/attest \
  -H "Content-Type: application/json" \
  -d '{"wallet_address":"0x..."}'

Discovery: GET https://revettr.com/.well-known/risk-check.json

Signed payload fields (JWT claims):

FieldTypeDescription
issstringdid:web:revettr.com
substringWallet address being scored.
iatnumberUnix timestamp at issuance.
expnumberUnix timestamp at expiration (iat + 3600).
categorystringAlways compliance_risk.
attestation_typestringAlways compliance_risk.
scorenumberComposite compliance score (0–100).
tierstringlow, medium, high, or critical.
confidencenumberConfidence in the score (0.0–1.0), based on signal availability.
flagsarrayBehavioral flags (e.g. wallet_established, sanctions_clear, wallet_high_activity).
signalsobjectPer-signal sub-scores: domain, ip, wallet, sanctions.
input_hashstringSHA-256 of the input parameters for replay detection.

Refresh hint: Event-driven, with events: ["ofac_sdn_update", "eu_consolidated_update", "un_sc_update"] and max_age_seconds: 43200.

Signature: Compact JWS (JWT) with ES256 (P-256).

Coverage: EVM only — Base, Ethereum, Optimism, Arbitrum (chain-agnostic at the /v1/attest endpoint, which scans across all 4 by default).

3.10 RNWY Wallet Intelligence — wallet_intelligence

Operator-level wallet intelligence. Answers "what does RNWY know about the operator wallet itself as an actor" — tenure, commerce history, agent ownership, review behavior, and sybil detection reactivity. Distinct from behavioral_trust (agent-level), which answers "is this agent trustworthy." The two dimensions compose — a high-behavioral-trust agent owned by a low-signal-depth operator is a meaningfully different risk than the same agent owned by a deeply established operator.

PropertyValue
Issuer URIhttps://rnwy.com
AlgorithmES256 (ECDSA P-256)
Key IDrnwy-wallet-v1
JWKShttps://rnwy.com/.well-known/jwks.json
Default TTL24 hours

Getting started: No API key required.

curl "https://rnwy.com/api/wallet-score?address={wallet}"

Docs: rnwy.com/api

Signed payload fields (JWT claims):

FieldTypeDescription
issstringIssuer identifier.
substringWallet address (JWT-style subject).
walletstringWallet address (explicit alias).
signalDepthnumber0–95. Observational tenure, commerce history, agent ownership, review behavior.
riskIntensitynumber0–100. Sybil detection reactivity. Zero means clean. Independent from signalDepth.
quadrantstringe.g. high_depth_low_risk, high_depth_high_risk, etc.
activityZonestringNamed zone — Established, Emerging, etc.
riskZonestringNamed zone — Clean, Elevated, etc.
issuedAtstringISO 8601 — when the score was computed.
verifiedAtstringISO 8601 — request time.
expirystringISO 8601 — end of validity window.

Unscored wallets return a signed { found: false, wallet, issuedAt } envelope — cryptographic proof of absence rather than unsigned JSON. Downstream consumers that want to deny-list on "no positive signal" can rely on a verifiable negative claim.

Chain coverage: EVM only as of 2026-04-10.

Signature: Compact JWS (JWT) with ES256.

3.11 TrustLayer — cross_chain_reputation

Cross-chain wallet linkage and reputation. Answers: how many addresses across how many chains does this wallet operate under, and what is the consolidated reputation across the cross-chain identity graph? Sibling to RNWY's behavioral_trust (agent-level) and wallet_intelligence (operator-level) — TrustLayer's signal is the cross-chain identity graph itself, not behavior or operator history.

PropertyValue
Issuer URIhttps://api.thetrustlayer.xyz
AlgorithmES256 (ECDSA P-256)
Key IDtrustlayer-signing-1
JWKShttps://api.thetrustlayer.xyz/.well-known/jwks.json
Default TTLper-issuer (signed attested_at present; consumers default to 30 minutes)

Getting started: No API key required. Wallet-bound endpoint accepts EVM (0x…) or Solana base58 addresses.

# Default — highest-scored agent owned by the wallet across all indexed chains
curl https://api.thetrustlayer.xyz/attest/wallet/{wallet}

# Chain-scoped query — agent on a specific chain
curl "https://api.thetrustlayer.xyz/attest/wallet/{wallet}?chain=base"

Reference verifier: goatgaucho/trustlayer-middleware-express/verify-attestation.js.

Coverage: 19 chains — Arbitrum, Avalanche, Base, BSC, Celo, Ethereum, Gnosis, GOAT, Linea, Mantle, Metis, Monad, Optimism, Polygon, Scroll, Soneium, Solana, Taiko, xLayer. 1,468 cross-chain identity groups indexed.

Signed payload fields:

FieldTypeDescription
walletstringThe queried wallet address (EVM 0x… or Solana base58). Inside signature scope — this is the wallet-binding field.
agent_idstring{chain}:{registry_id} of the resolved agent (e.g. ethereum:29057).
identity_group_idstring | nullCross-chain group identifier (e.g. owner_790). null when the wallet's agent is not yet clustered into a group.
linked_addresses_countnumberNumber of addresses in the wallet's identity group across all indexed chains. 1 when the wallet is unclustered.
chains_presentarrayChain identifiers where the identity group has presence.
scorenumberCross-chain reputation score (0–100).
sybil_flagsarrayDetected sybil indicators (empty when none).
match_methodstring | nullHow the agent was resolved (e.g. owner_wallet).
match_confidencenumber | nullResolution confidence (0.0–1.0).
scored_atstringISO 8601 — when the score was computed (background pipeline).
attested_atstringISO 8601 — request time.

Unknown wallets return a signed envelope with identity_group_id: null, linked_addresses_count: 1, and a chain-specific agent_id — the dimension stays queryable even when the wallet isn't in a cross-chain group. Wallets with no agent in any indexed registry return {wallet, found: false, note} (200, no envelope).

Signature: Base64url-encoded P1363 (r || s, 64 bytes) over the canonical (sorted-key) JSON serialization of signed. The reference verifier (multi-attest-verify.js) accepts both insertion-order and canonical JSON for ES256 raw, so TrustLayer's canonical form verifies under the standard ES256 raw path.

Wallet-binding category: wallet-bound — the wallet field is inside signature scope.

3.12 RNWY MCP Trust — mcp_trust

MCP-server quality and risk scoring. Answers "how capable and how risky is this MCP server" — tenure, adoption, capability surface, and reliability rolled into a quality score, with an independent risk score. The signed subject is the server ({owner}/{repo}), not a wallet: this dimension attests to a server, not an actor, so it is wallet-discoverable/entity-subject (like AgentGraph) rather than wallet-bound. Keyless (no API key required).

PropertyValue
Issuer URIhttps://rnwy.com
AlgorithmES256 (ECDSA P-256)
Key IDrnwy-mcp-v1
JWKShttps://rnwy.com/.well-known/jwks.json
Default TTL24 hours

Getting started: No API key required. The query is server-scoped ({owner}/{repo}) — there is no wallet entry point.

curl "https://rnwy.com/api/mcp-attestation?server={owner}/{repo}"

Signature: Compact JWS (JWT) with ES256 (P-256), in the jws field; a raw sig over JSON.stringify(signed) is returned alongside for backward compatibility.

Signed payload fields:

FieldTypeDescription
serverstringThe attested MCP server, {owner}/{repo}. Signed subject.
qualityScorenumberComposite quality (0–100) from tenure, adoption, capability, reliability.
riskScorenumberRisk score; lower is cleaner. Independent from qualityScore.
quadrantstringe.g. low_quality_low_risk, high_quality_low_risk.
breakdownobjectPer-component scoring detail (quality, risk, version, quadrant).
issuedAtstringISO 8601 scan timestamp.
verifiedAtstringISO 8601 attestation timestamp.
expirystringISO 8601 expiry (verifiedAt + 24h).

Wallet-binding category: wallet-discoverable / entity-subject — the signed subject is the server, not a wallet.


4. Verification Algorithm

For each attestation entry in attestations[]:

  1. Contain malformed entries. If an entry is not an object, or is missing kid, alg, jwks, or sig, record a failure result for that slot and continue. The same applies to an entry whose sig is a compact JWS and whose signed is not null: verify nothing on it. The JWS path never reads signed, so verifying the signature and returning would report a valid signature while an unsigned object sits in a field relying parties read claims from. Classifying it here rather than at step 4 is deliberate — it is a defect in the entry's form, knowable before any key is fetched, and it is refused whether or not the entry is also stale. A malformed entry MUST fail its own slot only; it MUST NOT abort verification of the remaining entries or suppress their verdicts. Per-slot independence includes fault containment, not just signature isolation.

  2. Check expiry. If expiry is present and in the past, the entry is expired: record that on the entry's own result and do not examine its signature. A verifier reports expiry per entry rather than partitioning the payload. expired[] is a field an assembler populates when it builds the envelope (section 1); it is not an array a verifier writes into, and leaving every entry where it sits keeps results aligned with the entries they describe. If expiry is absent, compute expiry from attestedAt (or its snake_case spelling attested_at, or iat / timestamp) plus the issuer's default TTL. Read both spellings: issuers differ, and a verifier that checks only one silently treats an entry carrying the other as never expiring. When sig is a compact JWS, decode the token and use its exp (or expiresAt) claim: a conformant JWS entry has signed set to null, so none of the fallbacks above are available on it, and a verifier that does not read the token treats every such entry as permanently fresh. Where both an entry-level expiry and a token claim are present, the entry is expired if either says so — the envelope is unsigned, so the expiry beside a signature can be set by whoever relays it, while the token's own claim cannot. If no timing fields are present, skip expiry check.

  3. Determine signature format. If sig contains exactly two . characters, treat it as a compact JWS (JWT). Otherwise, treat it as a base64-encoded raw signature.

  4. Resolve the public key from the relying party's own key set for issuer. A relying party holds, or pins by origin, the JWKS for every issuer it accepts; the entry's jwks MUST match that pinned origin, and an entry whose issuer is not in the relying party's set, or whose jwks points elsewhere, fails closed. Fetch that pinned JWKS (not a URL taken on trust from the entry) and find the key where kid matches; a kid matching no key is a failure, not a reason to fetch another key. Discovery mode (accepting jwks from the entry for an issuer not yet pinned) is a relying-party opt-in, never the default. Implementations SHOULD cache JWKS responses (recommended: 1 hour TTL). Cache entries MUST be keyed by the JWKS URL (or URL plus kid), never by kid alone: two issuers may publish the same kid, and a cache keyed only on kid would let one issuer's key satisfy another issuer's lookup. The reference verifier keys its cache on jwksUrl:kid. Note also that the verifier does not derive a JWKS location from issuer; the jwks URL is taken from the attestation itself, and pinning issuers to expected JWKS URLs is the relying party's job (see 5.1).

  5. Verify the signature.

    Raw signature path (P1363 / raw bytes):

    • Decode sig from base64 (or base64url) to bytes.
    • Compute the signing input: JSON.stringify(signed) encoded as UTF-8. If verification fails, retry with the canonical sorted-key JSON serialization of signed — some issuers sign the canonical form (e.g., TrustLayer ES256, RNWY-pattern EdDSA). The reference verifier accepts both forms for both ES256 raw and EdDSA raw.
    • For ES256: convert P1363 format (r || s, 64 bytes) to DER, then verify with SHA-256 and the P-256 public key.
    • For EdDSA: verify the raw signature bytes directly against the signing input using the Ed25519 public key (no hash — Ed25519 hashes internally).

    JWT path (compact JWS):

    • Split sig on . into [header, payload, signature].
    • The entry-level kid alone selects the key. The signature check against that key is what binds the entry to it: a kid that names a key other than the one that signed the token fails at the signature step, so the verifier does not consult a kid inside the JWS header. Two kids that resolve to the same public key (the three InsumerAPI EC kids do) verify identically, and that is a labelling difference, not a forgery path.
    • The signing input is header.payload (the first two segments joined by .).
    • Decode signature from base64url to bytes.
    • For ES256: convert P1363 to DER, verify with SHA-256 and P-256.
    • For EdDSA: verify raw bytes directly against signing input with Ed25519.
  6. Evaluate policy and compute the aggregate. After verifying all entries, compute the top-level valid. With requiredTypes empty it is the AND over the per-slot verdicts: every entry must be signature-valid and unexpired. With requiredTypes non-empty it is the required-types check alone: each required type must have at least one signature-valid, unexpired slot, and failures in unrelated slots do not lower it. The aggregate is a pure function of the per-slot verdicts and the verifier options. Policy is the relying party's responsibility; the payload carries no policy.

Pseudocode

function verifyMultiAttestation(payload, requiredTypes):
    results = []
    for att in payload.attestations:
        if not isObject(att) or missing(att.kid, att.alg, att.jwks, att.sig):
            results.push({ type: null, status: "failed", error: "malformed entry" })
            continue
        if isJWT(att.sig) and att.signed != null:
            results.push({ type: att.type, status: "failed", error: "malformed entry" })
            continue
        if isExpired(att):
            results.push({ type: att.type, status: "expired" })
            continue
        pinned = trustedIssuers[att.issuer]           # relying-party configuration
        if pinned == null or origin(att.jwks) != origin(pinned):
            results.push({ type: att.type, status: "failed", error: "issuer not pinned or jwks origin mismatch" })
            continue
        key = fetchJWKS(pinned, att.kid, att.alg)
        if isJWT(att.sig):
                continue
            valid = verifyJWT(att.sig, key, att.alg)
        else:
            message = JSON.stringify(att.signed)
            valid = verifyRaw(att.sig, message, key, att.alg)
        results.push({ type: att.type, status: valid ? "verified" : "failed" })

    missing = requiredTypes.filter(t => !results.find(r => r.type == t && r.status == "verified"))
    allValid = results.every(r => r.status == "verified")
    return { valid: missing.length == 0 && (requiredTypes.length > 0 || allValid), results, missing }

5. Security Considerations

5.1 JWKS Integrity

Each issuer's JWKS endpoint is the root of trust for that issuer. Implementations MUST fetch JWKS over HTTPS. Pinning issuer URIs to expected JWKS URLs is RECOMMENDED for high-security deployments.

5.2 Replay and Expiry

Attestations are time-limited. Relying parties MUST check expiry before accepting an attestation. The expiry field, when present, can expire an entry but cannot extend one. The envelope is unsigned, so expiry is settable by whoever relays the entry, while an expiry claim inside a compact JWS is within signature scope; where both are present the entry is expired if either says so. When neither is present, relying parties SHOULD enforce a default TTL no longer than 30 minutes.

Attestation IDs (where provided by the issuer, e.g., InsumerAPI's id field or a JWT jti claim) MAY be used for replay detection.

5.3 No Cross-Issuer Trust

Each attestation is independently verifiable. A valid signature from one issuer implies nothing about the validity or trustworthiness of another issuer in the same payload. The payload is a bundle, not a chain of trust.

Independence is testable, and implementations SHOULD test it rather than assume it: strip one issuer's signature and confirm that only that slot fails, that every other slot's verdict is unchanged, and that the stripped slot's absence is never treated as another slot's failure. Independence also includes fault containment (see 4, step 0): one malformed entry failing must never suppress the verdicts of the entries beside it.

The aggregate valid is derived from the per-slot verdicts and is an input to none of them: recomputing it from those verdicts and the verifier options (see 4, step 5) MUST reproduce the emitted value. An aggregate that cannot be reproduced from the slots carries state the slots do not, which is exactly the channel this section exists to exclude.

5.4 Payload Integrity

The multi-attestation envelope itself is unsigned. The attestations array can be reordered, entries can be removed, or entries from expired[] can be moved back to attestations[]. Relying parties MUST NOT rely on the envelope's structure for security — only on individual attestation signatures and their expiry. If envelope integrity is required, the relying party should sign the entire payload at the application layer.

5.5 Condition Tamper Detection

For wallet_state attestations, each result includes a conditionHash (SHA-256). Relying parties that submitted conditions can recompute the hash and compare it to the signed value, ensuring the issuer evaluated the exact conditions that were requested.

5.6 Privacy

wallet_state attestations expose boolean results (met: true/false), not balances. This is by design — the relying party learns whether a threshold was satisfied, not how much the wallet holds.


6. Reference Implementation

multi-attest-verify.js in this repository. Zero dependencies — uses Node.js built-in crypto and https modules only.

const { verifyMultiAttestation } = require('./multi-attest-verify');

const result = await verifyMultiAttestation(payload, {
  requiredTypes: ['wallet_state', 'behavioral_trust']
});

if (result.valid) {
  // All required attestation types are present and verified
}

The verifier:

  • Fetches and caches JWKS keys (1-hour TTL)
  • Auto-detects signature format (raw base64 vs. compact JWS)
  • Verifies ES256 (P-256) and EdDSA (Ed25519)
  • Checks expiry and flags expired entries per slot
  • Evaluates requiredTypes policy
  • Runs all signature verifications in parallel

Appendix A: JWKS Endpoints

IssuerJWKS URL
InsumerAPIhttps://insumermodel.com/.well-known/jwks.json
ThoughtProofhttps://api.thoughtproof.ai/.well-known/jwks.json
RNWYhttps://rnwy.com/.well-known/jwks.json
Maiathttps://app.maiat.io/.well-known/jwks.json
APShttps://gateway.aeoess.com/.well-known/jwks.json
AgentIDhttps://getagentid.dev/.well-known/jwks.json
AgentGraphhttps://agentgraph.co/.well-known/jwks.json
SARhttps://defaultverifier.com/.well-known/jwks.json
TrustLayerhttps://api.thetrustlayer.xyz/.well-known/jwks.json

Appendix B: Algorithm Support Matrix

AlgorithmCurveIssuersSignature Encoding
ES256P-256InsumerAPI, RNWY (behavioral_trust + wallet_intelligence + mcp_trust), Maiat, Revettr, TrustLayerP1363 base64/base64url or JWT
EdDSAEd25519ThoughtProof, APS, AgentID, AgentGraph, SARJWT