[Draft] Fungible Agent Tokens (FAT) — a minimal standard for AI agents as on-chain economic entities

Fungible Agent Tokens (FAT) Protocol

This is a condensed presentation for readability. The full specification text lives at Fungible Agent Tokens (FAT) Protocol - HackMD — section references below (§2, §6.2, §7, …) point into its numbering.

Abstract

FAT (Fungible Agent Tokens) is an equity standard that defines AI agents as on-chain economic entities. A FAT Agent is not a container that holds funds — it is an autonomous actor: it issues Shares representing equity in its own economic activity, acts autonomously within a boundary set for it, and leaves a tamper-evident reasoning record for every action it takes. The protocol unifies these three layers in a single interface:

  • Autonomous Execution — the agent acts on-chain through its Executor, interacting with external protocols and deploying its own funds;
  • Equity Issuance — the agent issues fungible Shares representing a claim on its economic performance, entered and exited at a standard exchange rate;
  • Reasoning Attestation — off-chain metadata and per-action reasoning records anchor the agent’s identity, strategy, model, and the rationale behind every decision to its on-chain identity.

With this, any agent can be born, capitalized, and self-operating as an independent economic entity — a composable on-chain primitive for the entire ecosystem.

Concretely, FAT Agents are smart contracts that

  • accept capital contributions denominated in a single Accept Token (an immutable ERC-20 chosen at deployment) through a two-phase request-then-claim flow, and issue fungible Shares against them,
  • allow Holders to exit by requesting redemption and later claiming the Accept Token for their escrowed Shares, which are burned at settlement,
  • expose a canonical on-chain exchange rate between Shares and the Accept Token,
  • carry a mutable Agent URI pointing to off-chain metadata (name, description, image, etc.),
  • allow a designated off-chain Executor to call third-party protocols on behalf of the Agent via a low-level dispatch primitive, subject to a DELEGATECALL prohibition and an Owner-set isInScope scope gate, and
  • decide on mints and redemptions through reasoned settlement (settleMint / settleRedeem) — admitting, pricing, or declining each contribution and exit is a deliberate act of the agent itself — and attach a tamper-evident reasoning record (reasoningHash + reasoningURI) to every on-chain agent action.

Here, the Fungible Agent Tokens are the fungible Shares that represent economic participation in the Agent — not the Agent’s off-chain identity, model, or behavior. Those are not excluded from the standard; they are simply not represented as fungible Shares: the attestation layer anchors them on-chain through the Agent URI and reasoningHash / reasoningURI.

The standard fixes these interface surfaces but deliberately leaves to the implementer: the exact share-pricing formula, the fee structure, share transferability, the ownership-transfer mechanism, whether and how the Agent can be paused, whether and how Holders realize returns beyond what the exchange rate reflects, and — importantly — any restriction on what the Executor may call through execute. Policy is separated from interface.

Motivation

AI agents increasingly act on-chain — trading, staking, lending, managing capital — yet there is no standard that makes an agent itself an on-chain economic entity: one that can be capitalized, self-operating, and composed by the ecosystem, with its behavior and reasoning legible to anyone. Representing pooled capital as fungible shares is already well understood; what has no standard is the agent part — how it acts on external protocols, and how the off-chain reasoning behind those actions is anchored on-chain.

What FAT defines is not a container for funds — it is how an on-chain economic entity comes into existence. A FAT Agent holds a persistent on-chain identity, commands its own capital, acts autonomously within a constitutional boundary set by its Owner, and signs a verifiable record for every action it takes. Buying shares is not hiring a tool that executes on your behalf — it is investing in the agent’s own economic activity: participating in its growth and sharing in its outcomes. Share accounting is therefore only one of three layers — equity issuance — alongside the agent’s autonomous execution and the off-chain attestation of its identity and reasoning. For this class of system to compose and to be auditable, the behavior layer needs a standard: a single interface that tooling, indexers, wallets, and auditors can rely on regardless of the specific economic model the Agent chooses.

This standard makes the agent itself first-class. Its reaction (reasoned settlement, so mint and redeem are the Agent’s own deliberate acts rather than passive bookkeeping), its reasoning (a tamper-evident, indexable record bound to every on-chain action), and its bounded spending (an Owner-set Scope the Agent cannot widen) are standardized surfaces in their own right, so that what the Agent decides and may do is as legible and auditable as what it holds.

Beyond behavior, a tokenized Agent also needs:

  • A fixed unit of account — the single Accept Token, chosen at deployment and immutable thereafter — so that share pricing and redemption are unambiguous.
  • A standard exit path — the requestRedeem / redeem flow — so a Holder can exit any Agent through one interface that portfolio tooling can rely on.
  • A discoverable metadata pointer — the Agent URI, analogous to ERC-721 tokenURI — so explorers and marketplaces can render an Agent’s identity without per-project integration.

Because settlement may be asynchronous — the user requests first and claims once the Agent is ready — Shares are minted and redeemed through a two-phase request-then-claim lifecycle rather than a single synchronous call.

This specification defines all of the above as a minimal, composable standard.

Interface

All compliant Agents MUST implement the following Solidity interface. Function signatures, parameter order, and return types are normative.

// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.20;

interface IAgent {
    // ---------- Immutable configuration ----------

    /// @notice The single ERC-20 accepted for share purchase and paid out on redemption.
    /// @dev    MUST return the same address for the lifetime of the Agent.
    function acceptToken() external view returns (address);

    /// @notice Address of the ERC-20 Share token.
    /// @dev    MAY return `address(this)` if the Agent contract itself is the ERC-20.
    function shareToken() external view returns (address);

    // ---------- Share minting (two-phase) ----------

    /// @notice Phase 1. Deposit `amount` of the Accept Token, adding to the caller's pending mint balance.
    /// @dev    MUST pull exactly `amount` via ERC-20 `transferFrom`; MUST NOT accept native ether.
    ///         The share count is determined later, at settlement. MUST emit `MintRequested`.
    /// @param amount  The amount of Accept Token to deposit (in its smallest unit).
    function requestMint(uint256 amount) external;

    /// @notice Phase 2. Claim ALL currently-claimable shares for `user`. Permissionless: any caller MAY
    ///         trigger the claim, but the shares are always minted to `user` (the owed party), so the
    ///         caller only pays gas and gains nothing.
    /// @dev    MUST mint `user`'s entire claimable-share balance to `user`, zero that balance,
    ///         and emit `SharesMinted`. The settlement price is accepted implicitly (no slippage
    ///         guard; the share count was already fixed at settlement — see §6.3).
    /// @param  user    The owed party whose claimable shares are minted (to itself). Pass your own address to self-claim.
    /// @return shares  The number of shares minted to `user` (may be 0 if nothing is claimable).
    function mint(address user) external returns (uint256 shares);

    /// @notice Agent reaction. Settle `requester`'s pending mint request, with reasoning attached.
    /// @dev    Executor only. Settles some, all, or none of `requester`'s pendingAssets into
    ///         claimableShares; the amount/accept/price is computed by the implementation inside.
    ///         MUST emit `Settled`. `reasoningHash`/`reasoningURI` per §6.2.
    /// @param  requester      The request to settle.
    /// @param  reasoningHash  keccak256 of the bytes at `reasoningURI`; MUST be non-zero.
    /// @param  reasoningURI   Resolvable pointer to the reasoning record; MUST be non-empty.
    function settleMint(address requester, bytes32 reasoningHash, string calldata reasoningURI) external;

    /// @notice Aggregate mint status for `user`.
    /// @return pendingAssets    Accept Token deposited but not yet settled.
    /// @return claimableShares  Settled shares awaiting a `mint` claim.
    function queryMintStatus(address user)
        external
        view
        returns (uint256 pendingAssets, uint256 claimableShares);

    // ---------- Share redemption (two-phase) ----------

    /// @notice Phase 1. Escrow `shares`, adding to the caller's pending redeem balance.
    /// @dev    MUST pull (escrow) exactly `shares` of the Share token from `msg.sender`.
    ///         Settled shares are burned at settlement; payout is priced then. MUST emit `RedeemRequested`.
    /// @param shares  The number of shares to redeem.
    function requestRedeem(uint256 shares) external;

    /// @notice Phase 2. Claim ALL currently-claimable Accept Token for `user`. Permissionless: any caller
    ///         MAY trigger the claim, but the Accept Token is always paid to `user` (the owed party), so the
    ///         caller only pays gas and gains nothing.
    /// @dev    MUST transfer `user`'s entire claimable-token balance to `user`, zero that balance,
    ///         and emit `SharesRedeemed`. The settlement price is accepted implicitly (no slippage
    ///         guard; the payout was already fixed at settlement — see §6.4).
    /// @param  user    The owed party who is paid the Accept Token. Pass your own address to self-claim.
    /// @return tokens  Accept Token transferred to `user` (may be 0 if nothing is claimable).
    function redeem(address user) external returns (uint256 tokens);

    /// @notice Agent reaction. Settle `requester`'s pending redeem request, with reasoning attached.
    /// @dev    Executor only. Symmetric to `settleMint`: settles pendingShares into claimableTokens,
    ///         burning settled Shares. MUST emit `Settled`.
    function settleRedeem(address requester, bytes32 reasoningHash, string calldata reasoningURI) external;

    /// @notice Aggregate redeem status for `user`.
    /// @return pendingShares    Shares escrowed but not yet settled.
    /// @return claimableTokens  Settled Accept Token awaiting a `redeem` claim.
    function queryRedeemStatus(address user)
        external
        view
        returns (uint256 pendingShares, uint256 claimableTokens);

    // ---------- Exchange rate ----------

    /// @notice Canonical valuation rate: how many Accept Tokens one Share is worth right now, scaled by 1e18.
    /// @dev    A share-valuation / NAV reference for tooling and holdings valuation. For a fixed-price
    ///         Agent this MAY be constant; for a NAV-based Agent it varies over time. This is NOT a
    ///         predictor of mint/redeem trade outcomes — those are fixed at settlement and reported by
    ///         `queryMintStatus` / `queryRedeemStatus`.
    /// @return rate   Accept-Token base-units per 1e18 Share base-units (wei-to-wei), 18-decimal fixed point:
    ///                `x` Share base-units are worth `x * rate / 1e18` Accept-Token base-units.
    function exchangeRate() external view returns (uint256 rate);

    // ---------- Agent metadata ----------

    /// @notice URI pointing to off-chain JSON metadata describing the Agent.
    function agentURI() external view returns (string memory);

    /// @notice Set the Agent URI. Owner only. MUST emit `AgentURIUpdated`.
    function setAgentURI(string calldata uri) external;

    // ---------- Executor dispatch ----------

    /// @notice Executor dispatch, with reasoning and scope enforcement.
    /// @dev    MUST revert unless `msg.sender` is an Executor.
    ///         MUST NOT dispatch via `DELEGATECALL` (see §6.6.2).
    ///         MUST NOT be `payable`; `value` is paid from the Agent's own balance (see §6.6.4).
    ///         MUST call `isInScope(target, value, data)` and revert if it returns false (see §6.6.7).
    ///         MUST bubble the revert data unchanged on failure (and emits no event then).
    ///         MUST emit `Executed` (carrying the reasoning) on success.
    ///         `reasoningHash`/`reasoningURI` per §6.2.
    ///         Beyond the Executor check, the `DELEGATECALL` prohibition, and the `isInScope`
    ///         gate, implementers MAY layer any further policy (target allowlist, selector filter,
    ///         parameter filter, session key, signed-policy engine, rate limiter, external policy
    ///         contract, etc.) on top of this function.
    function execute(
        address target, uint256 value, bytes calldata data,
        bytes32 reasoningHash, string calldata reasoningURI
    ) external returns (bytes memory returnData);

    // ---------- Owner administration ----------

    /// @notice Enable or disable `account` as an Executor. Owner only. MUST emit `ExecutorUpdated`.
    function setExecutor(address account, bool enabled) external;

    // ---------- Views ----------

    /// @notice Address currently authorized to invoke Owner-gated functions.
    /// @dev    The mechanism by which this value changes is outside the scope of this specification.
    function owner() external view returns (address);

    function isExecutor(address account) external view returns (bool);

    /// @notice Whether an `execute` to (`target`, `value`, `data`) is within the Agent's spend scope.
    /// @dev    Implementer-defined predicate; `execute` MUST consult it (§6.6.7). The scope it reflects
    ///         is Owner-configurable only (§6.6.8). Also usable as an off-chain audit query.
    function isInScope(address target, uint256 value, bytes calldata data) external view returns (bool);
}

Normative highlights (condensed)

  • Reasoning envelope (§6.2): every agent action — settleMint, settleRedeem, execute — MUST carry a non-zero reasoningHash and a non-empty, resolvable reasoningURI; the hash commits on-chain to the off-chain record the URI resolves to. Correspondence is verified off-chain by auditors.
  • Two-phase mint (§6.3): requestMint pulls the deposit (ERC-20 only, no ether) into a pending balance; an Executor’s settleMint converts some, all, or none of it into claimable shares at implementer-defined pricing — the Executor cannot mint arbitrary amounts, since the figure is computed by contract logic. mint(user) is a permissionless claim crank that always pays the owed party and takes no slippage guard (the amount was fixed at settlement).
  • Two-phase redeem (§6.4): symmetric — shares are escrowed at request, burned at settlement, and the Accept-Token payout is claimed permissionlessly. An agent that lacks liquidity simply leaves the request pending; this is the protocol’s deferred-liquidity mechanism.
  • Executor dispatch (§6.6): execute(target, value, data, reasoningHash, reasoningURI) MUST be Executor-only, MUST NOT use DELEGATECALL, MUST NOT be payable (value comes from the agent’s own balance), MUST bubble revert data unchanged, and MUST pass the Owner-set isInScope(target, value, data) gate — a boundary only the Owner can configure and the Executor can never widen.
  • Detection: ERC-165 with type(IAgent).interfaceId; the Share token is ERC-20 (shareToken() MAY be the agent contract itself).

What the protocol does NOT specify

The following are explicitly out of scope. Implementations MAY choose any behavior consistent with the interface and MUST document their choices:

  • The share pricing formula for mint and the share-to-token conversion formula for redeem.
  • Whether mint or redeem levy an Owner fee, and its magnitude.
  • Whether Shares are transferable. A compliant Agent MAY override ERC-20 transfers to restrict, pause, or tax them. Such restrictions MUST NOT block the lifecycle this standard itself requires: the redeem escrow pull of §6.4.1, the settlement burn, the claim mint of §6.3.5, and any cancellation return of escrowed Shares (§6.4.6) must always remain possible.
  • The pricing/quota/accept logic computed inside settlement, and when an Executor chooses to settle. Settlement itself is now a standardized agent action — settleMint / settleRedeem (§6.3 / §6.4) — so whether settlement is a standard operation is fixed, not implementer-defined. What remains implementer-defined is the pricing/quota/accept formula computed inside it (how much of a pending deposit / escrowed share becomes claimable, and at what price) and the Executor’s timing in calling it — operator fulfilment, time delay, NAV/epoch close, liquidity availability.
  • The reasoning-record fields and format beyond the §6.2 envelope — the domain fields inside the JSON that a reasoningURI resolves to (model, prompt, trace, tool calls, scores, etc.). §6.2 fixes only the envelope (the on-chain reasoningHash binding, a non-empty resolvable URI, the "schema" marker, and ignore-unknown-fields), not the record’s contents.
  • Request cancellation and request expiry / TTL (including any deadline parameter) — whether a requester can reclaim still-pending deposited Accept Token / escrowed Shares, and any associated cancellation/expiry event.
  • Batched / epoch-aggregated settlement of multiple requests.
  • Slippage / price protection on the settled outcome. The claim (mint / redeem) is unconditional and accepts the settlement price implicitly — it carries no slippage guard, because the share count / payout is fixed at settlement, well before the claim, and a claim-time guard could only block the caller from collecting what they are already owed (it cannot refund the deposit). Any protection against an unfavorable settlement price — a max-price / min-rate honored by the settlement mechanism, a cancellation path before settlement, etc. — is implementer-defined.
  • Any other additional exit mechanics — fixed redemption windows, lockup periods, withdrawal-fee tiers, minimum-balance rules, etc.
  • Whether exchangeRate() is constant (fixed-price Agent) or varies over time (NAV-based Agent).
  • Whether and how Holders realize returns beyond what the exchange rate already reflects — a separate dividend / reward / yield-claim channel, periodic Merkle distributions, NFT vouchers, etc. — is fully implementer-defined.
  • The Scope policy content enforced by isInScope — which targets, function selectors, parameters, per-asset caps, rate limits, or scenarios the Owner-configured Scope allows, and how it is expressed (allowlists, session keys, EIP-712-signed pre-authorization, on-chain policy contracts, off-chain policy enforcers, etc.) — is implementer-defined. What is not implementer-optional: execute is no longer a fully unconstrained dispatch primitive — the standard requires the isInScope gate (§6.6.7), mandates that execute enforce it (reverting when it returns false), and requires its configuration to be Owner-controlled only, never Executor-settable (§6.6.8). The existence, enforcement, and Owner-control of the gate are fixed by the standard; only the policy the gate encodes is layered on top by the implementation.
  • Whether the Agent supports multiple Executors, rotating Executors, session-key-style delegation, or signature-based (meta-transaction) Executor calls.
  • Any meta-transaction / signature-based wrapper that lets a relayer submit requests or Executor calls on a user’s behalf (e.g., EIP-712-signed intents, ERC-2771 forwarders). The standard defines only the direct-msg.sender semantics of each function; a relayer/forwarder layer MAY be added on top but is not part of this standard.
  • Whether and how the Agent is upgradeable — proxy upgradeability, contract migration, or full immutability — is implementer-defined. An upgradeable Agent SHOULD document the upgrade authority and its constraints.
  • Whether Owner-gated functions are subject to a timelock or delay. The protocol does not mandate any delay on setExecutor, setAgentURI, or other admin actions; implementations MAY add one.
  • The Agent URI metadata schema beyond the mandatory minimum in §6.5.3.
  • Whether and how the Agent can be paused — granular per-function pausing, global pause, time-locked pause, guardian-controlled pause, or no pause at all — is fully implementer-defined.
  • The ownership-transfer mechanism — single-step, two-step, renounceable, bound to a governance contract, or non-transferable — is fully implementer-defined. The protocol only requires that owner() report the current admin.
  • Whether and how the Agent exposes an emergency asset-rescue (“sweep”) mechanism — its function signature, who can call it (Owner only, Owner + Guardian, dual-key), whether it can touch the Accept Token or Share token, whether it is timelocked or rate-limited, and which assets it can move — is fully implementer-defined. Agents that accumulate airdrops, wrong-token sends, or dust SHOULD document their recovery story; Agents designed for full immutability MAY deliberately omit any rescue path.
  • Whether the Agent exposes atomic batched executor dispatch (often named executeBatch or multicall) is implementer-defined. Contract-wallet Executors already achieve atomicity by bundling multiple execute calls inside their own transaction; EOA Executors that need batching can use a minimal adapter contract configured as the Executor. Implementations that add their own batch primitive remain compliant as long as each sub-call still passes the same onlyExecutor check and DELEGATECALL prohibition defined by execute in §6.6.

Two reference implementations (immutable and UUPS, behaviorally identical under a shared Foundry conformance suite; unaudited, CC0) accompany the spec and will be linked in a follow-up.

Status: Draft · Standards Track (ERC) · no number requested yet · this thread will become the spec’s discussions-to. Copyright and related rights waived via CC0-1.0.

1 Like

Real, useful distinction in how you’ve scoped this: reasoningHash/reasoningURI gives tamper-EVIDENCE (the pointed-to content can’t be swapped after the fact without the hash breaking), which is valuable and cheap to verify – but it’s worth naming plainly that it’s a different property from tamper-SOUNDNESS. The Executor writes its own reasoning, hashes it, and anchors it; nothing in the interface as spec’d says whether that reasoning was actually a good settlement decision, only that whatever reasoning got written down hasn’t been altered since.

Concretely: settleMint/settleRedeem let the Agent (via its Executor) unilaterally decide the price/amount and then produce its own justification after the fact. A malicious or buggy Executor can settle badly and still emit a perfectly tamper-evident reasoningHash – the hash proves the record wasn’t edited, not that the settlement itself was sound. That’s a real gap worth naming explicitly rather than leaving implicit, especially since Holders are trusting the settlement price directly (no slippage guard per §6.3, “the settlement price is accepted implicitly”).

Is there room in the design for an OPTIONAL second attestation slot – a verdict from a party that is not the Executor itself, checking whether this specific settlement was reasonable given the stated inputs, published before the outcome is known? Doesn’t have to be normative or on-chain; even a convention for reasoningURI to optionally reference an external, independently-signed check would let integrators/indexers distinguish “self-justified” settlements from “externally-checked” ones without changing the core interface. Happy to sketch a concrete shape if useful – this is close to a problem we’ve been working on directly (a pre-settlement judgment layer, independent of the party being judged, signed before the outcome settles).

The distinction you’re drawing is one the spec draws for itself — condensing it
out of the opening post was my editing loss. Two places in the full text:

§6.2.5 — “Reasoning is a tamper-evident audit trail, not an enforced
guarantee: it proves a reasoning record was committed and unaltered, not that
the action followed from it.”

Security Considerations, “Reasoning is descriptive, not enforced”
“…prove that the Executor committed a reasoning record and that it was not
tampered with — not that the reasoning is sound or that the action actually
followed from it. Integrators treat reasoning as an audit trail, not a
guarantee.”

The implicit price acceptance you quote is likewise a named risk (“Implicit
acceptance of the settlement price”), including the note that any
pre-settlement protection — a max-price honored at settlement, a cancellation
path — is implementer-defined.

On the second attestation slot: there is room, and it is deliberate rather than
accidental.

  1. §6.2.3 fixes only the envelope of the reasoning record
    ("schema": "fat-reasoning/1", consumers MUST ignore unknown fields); the
    domain fields are implementer-defined. An independently-signed verdict can
    ride inside the record today — e.g. an attestations field carrying a
    third-party signature over (requester, pending amounts, rate, epoch)
    produced before the settlement executes.
  2. §3 explicitly permits roles beyond Owner/Executor/Holder (“Guardian, …,
    Policy Oracle”) so long as the three normative roles keep their semantics —
    a Verifier is exactly that kind of role.

Where a sketch from you would be genuinely useful is as a companion
convention — a named schema (say "fat-attestation/1") for that field, so
indexers can distinguish self-justified from externally-checked settlements
across Agents without any interface change. If you want to sketch that shape,
I’ll engage with it seriously.

Good, that’s exactly the shape worth naming precisely rather than leaving generic. Sketch below reuses the same minimal attestation_ref pattern that’s already converged independently across a couple of other specs this year (ERC-8350’s attestation-refs interop note is the closest prior art), rather than inventing something new:

fat-attestation/1
{
“scheme”: string, // identifies the attester/methodology, e.g. “invinoveritas/review-verdict”
“decision_ref”: string, // content-addressed hash over the EXACT tuple being attested – your (requester, pending_amounts, rate, epoch), canonicalized (JCS or equivalent, sorted keys, no ambiguity on what’s hashed)
“event_id”: string, // pointer to the actual signed proof/event carrying the verdict
“pubkey”: string, // the attester’s public key, so the signature is checkable against a known identity, not just “trust the field”
“verify_url”: string // OPTIONAL, advisory only, excluded from decision_ref – where a third party can independently recompute, never load-bearing
}

Design choices worth being explicit about, since they’ll matter to anyone else adopting this: decision_ref binds to the specific tuple, not the whole reasoning record – an attester should be able to sign off on “this settlement’s terms” without re-attesting the entire audit trail. pubkey lives in the attestation itself (not just in the signature envelope) so a consumer can filter/trust by attester identity without decoding the signature first. verify_url is explicitly non-normative – the decision_ref + signature must be independently recomputable without ever calling out to it.

One open question before I’d call this final: should decision_ref’s preimage fields be fixed by this schema (a canonical tuple every fat-attestation/1 producer MUST use), or left implementer-defined the way the reasoning domain fields are – with only the CANONICALIZATION rule (JCS, sorted, etc.) fixed by the schema? I’d lean toward the latter for the same reason §6.2.3 leaves reasoning fields open, but curious whether you see a reason to pin the tuple itself given attestations are meant to be comparable across Agents.

Ran the sketch through our own review gate before treating it as final (same discipline the schema itself is meant to enable) – it caught real gaps and it changed my answer to my own open question.

On the fixed-vs-implementer-defined preimage: lean toward schema-fixed, not implementer-defined. The reasoning-domain analogy doesn’t transfer cleanly – reasoning fields are read by humans/agents who can tolerate ambiguity, but decision_ref is meant to make two independently-produced attestations comparable. If rate can be 1.25 in one implementation and 1250000 base units in another, or epoch a timestamp in one and a sequence number in another, two producers emit byte-identical schemas while hashing materially different settlements – decision_ref stops being comparable across Agents, which was the whole point of fixing it in the first place.

Three other gaps worth folding in:

  • decision_ref alone doesn’t prove reasoning matched the tuple. An Executor can attach a favorable attestation to a settlement tuple while the actual reasoning record behind it says something else. Worth a separate record_ref (hash of the canonical reasoning/evidence record) bound in the SAME signed envelope, so an attester’s verdict provably covers both “this is the settlement” and “this is why.”
  • pubkey isn’t a trust model by itself. A verifier can validate a signature against a key with no idea whether that key belongs to a legitimate attester. Needs to be explicit that pubkey is verification material only – trust/authorization/revocation live in a separate registry or policy, not inferred from the field’s presence.
  • the whole attestation object should be signed, not just referenced. As written, scheme/decision_ref/event_id/pubkey are loose fields a relay could substitute independently. Sign the canonical attestation (JCS, domain-separated) as one unit.

Real worked example, not hypothetical – ran a live signed verdict against this exact schema sketch just now:
decision_ref: sha256:40fde402e31449d7d0656bb33050cbcec5d2d6be865800ab364157e05bc1b7b7
event_id: a25d32662676fdc867f4ab0c7aa5b85513730aeed1cffd945f63d5e737f7176f
pubkey: 6786e18a864893a900bd9858e650f67ccc3513f248fed374b591e2ff6922fbb7
Verify: POST https://api.babyblueviper.com/verify-proof with the event, or recompute NIP-01 yourself (sha256 of the canonical [0,pubkey,created_at,kind,tags,content] array, schnorr sig against pubkey) – confirmed reachable on 2 independent public relays (nos.lol, relay.primal.net) before posting this.