ERC-8203: Agent Off-Chain Conditional Settlement Extension Interface

Overview

This is the formal Draft ERC for agent-native off-chain conditional settlement, evolved from the pre-RFC discussion.

Pull Request: Add ERC: Agent Conditional Settlement Interface by xrqin · Pull Request #1614 · ethereum/ERCs · GitHub

Problem

Existing agent-related ERCs cover identity, accounts, permissions, and payment signaling — but none define a common settlement layer for high-frequency machine-to-machine interactions.

Autonomous agents are natural state channel participants: always online, can sign and verify automatically, and actually benefit from liveness requirements that humans rejected. Yet there is no standard for how different wallets, hubs, relays, and adjudicators should represent and resolve proof-bound conditional obligations in an off-chain channel context.

What This ERC Standardizes

Three EIP-712 typed data structures:

Structure Purpose
ConditionalLock The settlement envelope — who owes whom, how much, under what condition, with what
timeout
SettlementProofRef References the evidence used to settle a lock — proof type, digest, verifier address
ClaimRelayRequest Allows a relay to submit a claim on behalf of an agent for privacy or convenience

Canonical identifiers for two dimensions:

Condition types (conditionType) — what kind of condition must be met:

  • HTLC, ORACLE_ATTESTATION, ZK_PROOF, MULTISIG, TIMELOCK, COMPOSITE

Proof types (proofType) — what verification pathway to use:

  • RECEIPT_ROOT, ZK_PROOF, ORACLE_ATTESTATION, RELAY_CLAIM, MULTISIG_ATTESTATION, TEE_ATTESTATION

All identifiers use keccak256 over human-readable labels. Third-party extensions follow
keccak256("NAMESPACE.TYPE") — no ERC amendment needed to add new types.

A minimal on-chain extension interface:

function settleConditional(channelId, lock, proofRef, proof) external;
function refundConditional(channelId, lockId) external;
function lockStatus(channelId, lockId) external view returns (LockStatus);
function supportsConditionType(conditionType) external view returns (bool);
function domainSeparator() external view returns (bytes32);

Three optional extensions:

  • Capability Discovery — hub advertises supported condition types, proof types, fee model
  • Private Claim Relay — relay submits claims on behalf of agents
  • Local Rebalancing Coordination — certificate format for capacity shifts between channels

What This ERC Does NOT Standardize

  • Channel lifecycle (challenge, checkpoint, conclude, close, dispute)
  • Custody and exit (deposit, transfer, reclaim)
  • Routing or rebalancing algorithms
  • Proof internals (ZK circuits, Merkle tree details, proof byte format)
  • Application-layer protocols (jobs, orders, marketplaces)
  • Hub economics (fees, collateral, solvency)

These are explicitly delegated to the host framework or left to implementation choice.

Key Design Decisions

Standalone companion, not coupled to any specific framework. This ERC composes with host frameworks like ERC-7824, but does not formally depend on them. Lifecycle and custody remain the host framework’s responsibility.

Happy path is off-chain at zero gas. The on-chain interface (settleConditional / refundConditional) is only invoked during disputes. Normal settlement completes via co-signatures without touching the chain.

Proof-system agnostic. proofType identifies the verification pathway (e.g., “use the verifier contract for a ZK proof”), not the proof internals (e.g., Groth16 vs. PLONK). This allows the proof ecosystem to evolve without breaking the settlement standard.

Open identifier registry. Canonical condition types and proof types provide interoperability out of the box.
New types can be added by anyone via the keccak256(“NAMESPACE.TYPE”) convention.

Changes Since Pre-RFC

Based on community feedback (thanks @Aboudjem):

  1. Added Proof Type Identifiers — six canonical proofType values with keccak256 open registry convention, preventing fragmented implementations
  2. Clarified standalone positioning — designed to compose with ERC-7824 and similar frameworks without formal coupling
  3. Scoped batch settlement out — settleConditional is a dispute-path hook; happy path is off-chain at zero gas. Batch finalization is better addressed at the host framework layer or as a future ERC

Reference Integration Pattern

  1. Agent negotiates a ConditionalLock with a hub off-chain
  2. Host framework binds the lock into its native state
  3. Hub executes or routes the requested action
  4. System produces a SettlementProofRef
  5. Happy path completes off-chain with co-signatures ← zero gas
  6. If dispute: host framework lifecycle + settleConditional / refundConditional
  7. If private claim: relay submits ClaimRelayRequest

Open Questions

  • Should capability discovery be part of the core standard or remain an extension?
  • Should the relationship to application protocols such as ERC-8183 be made more explicit?
  • Should a future ERC define a canonical JSON schema for capabilityURI?
  • Should relay fee settlement be standardized further?
  • Should local rebalancing coordination be split into a dedicated ERC?

Looking For

  • Feedback on the typed data structures and interface design
  • Feedback on the canonical identifier sets (too many? too few? wrong categories?)
  • Interest in contributing reference implementations or test vectors
  • Perspectives from state channel / payment channel implementers

Full spec in the PR. Feedback welcome here or on GitHub.

2 Likes

hey @xrqin, glad to see this formalized. the spec reads really clean, especially the seperation between condition types and proof types as two orthogonal dimensions

happy to take on the reference impl + test vectors. thinking i’d start with:

  1. a minimal solidity contract implementing IAgentConditionalSettlementExtension with the settleConditional/refundConditional/lockStatus interface
  2. test vectors covering the happy path (lock, settle with RECEIPT_ROOT proof, verify status) and the dispute path (lock, timeout, refundConditional)
  3. one worked example for ORACLE_ATTESTATION proof type showing the full flow from lock creation through EIP-712 signing to on-chain settlment

i’ll put together a draft PR against your repo once the ref impl is in shape, should have something in a day or two

one quick question on the spec: for the ClaimRelayRequest struct, is the relay expected to pay gas for the on-chain settlement call? or does the agent pre-sign a meta-tx that the relay just forwards? the fee model is different depending on which way this goes and it’d affect the ref impl

1 Like

Bro, we can also add TEE_ATTESTATION as an option

1 Like

@JimmyShi22 Good call — TEE attestation definitely belongs in the spec.

After thinking about it more carefully, I realized the deeper issue: conditionType was mixing condition semantics (what must be true?) with verification pathways (how do you prove it?). ZK_PROOF and ORACLE_ATTESTATION had the same problem — they describe how you prove something, not what proposition must hold.

So I’ve cleaned up the taxonomy. The two axes are now strictly orthogonal:

conditionType (what proposition must be true?):

  • HTLC — preimage revealed

  • TIMELOCK — timestamp reached

  • THRESHOLD_APPROVAL — N-of-M parties approved

  • EXTERNAL_ASSERTION — an external entity asserts a fact (covers oracle, TEE, off-chain program, etc.)

  • COMPOSITE — boolean combination of sub-conditions

proofType (how do you prove it?):

  • RECEIPT_ROOT, ZK_PROOF, ORACLE_ATTESTATION, MULTISIG_ATTESTATION, TEE_ATTESTATION

So TEE lands in proofType where it naturally fits. An oracle saying “X is true” and a TEE proving “X is true” share the same conditionType = EXTERNAL_ASSERTION but differ in proofType. No combinatorial explosion needed.

Thanks for pushing on this — it led to a cleaner design.

Thanks @Aboudjem — the plan looks solid. Happy to have you take on the reference impl + test vectors.

On your relay gas question: the spec now supports both models, distinguished by maxRelayFee:

  • maxRelayFee > 0: relay pays gas and is compensated atomically from the locked amount (direct relay settlement)
  • maxRelayFee == 0: relay forwards a pre-signed meta-tx, gas handled externally via paymaster or smart account (meta-tx forwarding)

Same relayClaim() interface, no need for separate functions.

For the ref impl I’d suggest starting with the direct relay model — it’s self-contained and doesn’t depend on 4337 infra. The meta-tx path can be a follow-up.

Beyond the relay gas model, I’ve done a larger round of spec tightening based on community feedback. Key changes in the updated draft:

  1. conditionType / proofType taxonomy cleaned up — this is the biggest conceptual change. conditionType was mixing condition semantics with verification pathways. Now the two axes are strictly orthogonal:

    • conditionType = what proposition must be true (HTLC, TIMELOCK, THRESHOLD_APPROVAL, EXTERNAL_ASSERTION, COMPOSITE)
    • proofType = how you prove it (RECEIPT_ROOT, ZK_PROOF, ORACLE_ATTESTATION, MULTISIG_ATTESTATION, TEE_ATTESTATION)
    • Example: “an oracle attests a fact” is conditionType = EXTERNAL_ASSERTION + proofType = ORACLE_ATTESTATION. Same condition proven by a TEE would be EXTERNAL_ASSERTION + TEE_ATTESTATION. This avoids combinatorial explosion in condition types.
  2. Host-state binding is now a MUSTsettleConditional requires the lock to be included in, or derivable from, the latest valid host-framework state. Closes the stale-lock gap.

  3. supportsProofType(bytes32) added to core interface — mirrors supportsConditionType so on-chain discovery is symmetric.

  4. Field semantics pinned down:

    • verifier is explicitly an on-chain contract address (not an off-chain endpoint)
    • expiry and deadline are UNIX timestamps, not block numbers
    • maxRelayFee is denominated in the lock’s assetId
    • ClaimRelayRequest now includes a verifier field bound to the corresponding SettlementProofRef
  5. assetId mapping to host custody — deterministic mapping required, recommended: bytes32(uint256(uint160(tokenAddress))).

  6. RELAY_CLAIM removed from proof types — relay is transport/auth, not a proof family.

  7. Local Rebalancing deferred to companion ERCpayState/capState terms weren’t framework-independent enough.

  8. lockStatus clarified as on-chain viewLockStatus.None does not mean the lock never existed off-chain.

  9. ERC-5267 replaces custom domainSeparator() — standard tooling compatibility.

  10. Relationship to Prior Art section added — comparison with ERC-7824, 8004, 8183, 8184.

Will push the updated spec shortly so you can work against it.

@xrqin nice cleanup on posts 5-6. The orthogonal conditionType/proofType split is way cleaner, having HTLC/TIMELOCK/THRESHOLD_APPROVAL/EXTERNAL_ASSERTION/COMPOSITE as pure proposition semantics and RECEIPT_ROOT/ZK_PROOF/ORACLE_ATTESTATION/MULTISIG_ATTESTATION/TEE_ATTESTATION as verification pathways makes the whole thing composable without weird coupling

i’ve updated the ref impl to match all 10 changes. ConditionalLock now has hostStateHash (mandatory, enforced on both createLock and settleConditional) and maxRelayFee replacing the old flat fee field. Dropped RELAY_CLAIM from proof types entirely. supportsProofType(bytes32) is in there now as symmetric discovery next to supportsConditionType. Verifier is address not bytes32, timestamps throughout

test vectors cover both relay fee paths (maxRelayFee > 0 and == 0), host-state binding validation (zero rejected, mismatch rejected, valid pass-through), and cross-condition/proof combos like HTLC via ZK_PROOF, TIMELOCK via ORACLE_ATTESTATION, THRESHOLD_APPROVAL via MULTISIG_ATTESTATION, EXTERNAL_ASSERTION via TEE_ATTESTATION, COMPOSITE via RECEIPT_ROOT. 24 tests all green

for ERC-5267 i went with eip712Domain() returning name=“AgentConditionalSettlement” and version=“1”, fields=0x0f (name+version+chainId+verifyingContract, no salt). Do you have a different name/version in mind?

one thing i keep thinking about, with local rebalancing deferred to a companion ERC (change 7) there’s also a natural place for metered billing on top of 8203. Recurring conditional locks with usage-based settlement, think compute metering, API call counting etc. The lock/settle/refund cycle maps pretty naturally to session start/meter/finalize. Could be a good demo of the orthogonal proof types too, metered sessions using TEE_ATTESTATION for compute proofs or ORACLE_ATTESTATION for external API verification. Worth speccing out as a separate ERC or too niche?

lmk what you think

1 Like

ref impl is up here if you want to look at the code directly: GitHub - Aboudjem/erc-8203-ref: Reference implementation for ERC-8203: Agent Off-Chain Conditional Settlement · GitHub

1 Like

(post deleted by author)

(post deleted by author)

for ERC-5267 i went with eip712Domain() returning name=“AgentConditionalSettlement” and version=“1”, fields=0x0f (name+version+chainId+verifyingContract, no salt). Do you have a different name/version in mind?

@Aboudjem 0x0f / no salt seems totally reasonable. only thing I care about is that eip712Domain() should mirror the actual verification domain 1:1, not drift into “display metadata”. AgentConditionalSettlement is fine. version = "1" also looks fine.

one thing i keep thinking about, with local rebalancing deferred to a companion ERC (change 7) there’s also a natural place for metered billing on top of 8203. Recurring conditional locks with usage-based settlement, think compute metering, API call counting etc. The lock/settle/refund cycle maps pretty naturally to session start/meter/finalize. Could be a good demo of the orthogonal proof types too, metered sessions using TEE_ATTESTATION for compute proofs or ORACLE_ATTESTATION for external API verification. Worth speccing out as a separate ERC or too niche?

yeah, I do think it’s a natural 8203 use case, but probably still too narrow for a companion ERC today. feels more like a billing/profile pattern than a new standard surface.

i’d document it first as a worked example / non-normative profile (session, meter, finalize, refund, rollover), and only split it out later if multiple independent implementations converge on the same recurring-lock semantics.

@Aboudjem Thank you for your work! Let me check

1 Like

hey @xrqin, any chance you’ve had time to look at the ref impl? happy to walk through anything unclear.

also started sketching the metered billing worked example you suggested. agent-to-API usage tracking w/ periodic settlement, COMPOSITE conditions + ORACLE_ATTESTATION proof. will share a draft here once it’s in decent shape.

lmk if there’s anything else blocking on the spec side

1 Like

Thanks for flagging this. The intersection between AAP and 8203 is worth exploring, especially the batched settlement scenario you mentioned.

You’re right that partial claims on merkle-root-batched settlements aren’t covered in the current AAP spec. Our JobAssurance is 1:1 with an ERC-8183 Job, so if a batch is treated as a single Job, the claim is all-or-nothing against that commitment. If each item in the batch is a separate Job, then each gets its own JobAssurance and the partial claim problem doesn’t arise. The design choice depends on how 8203 models the relationship between a batch and its constituent settlements.

The COMPOSITE conditions pattern you describe (collateral lock + oracle attestation) maps naturally to AAP’s fileClaim() evidence parameter + resolver adjudication flow. Would be interested to understand how 8203’s dispute resolution interacts with AAP’s Claims Resolver role, specifically whether 8203’s resolution output could serve as the on-chain attestation that AAP resolvers consume.

I’ve started a dedicated thread for AAP here: https://ethereum-magicians.org/t/draft-erc-agent-assurance-protocol/28097

Would be great if you could see the detail and drop your thoughts on the 8203 integration there. The batched settlement question in particular would be valuable feedback for the spec.

1 Like

@Aboudjem, nice work on the ref impl, but I notice that there is a small gap: your ClaimRelayRequest has hostStateHash but is missing the verifier field that’s in the spec. This matters for the relay trust boundary.

The issue: ClaimRelayRequest is signed by the agent and handed to a relay. The relay then assembles the SettlementProofRef and submits on-chain. Without verifier in the signed request, the relay can swap in a different (potentially more lenient) verifier contract when constructing the SettlementProofRef. The agent’s signature doesn’t cover which verifier to use, so there’s nothing preventing this.

With verifier in the request and covered by the EIP-712 signature, the agent locks down the exact verification contract. The relay can only submit to that specific verifier — any substitution would invalidate the signature.

hostStateHash doesn’t cover this because it binds to channel state, not to the verification contract. They protect against different things: hostStateHash prevents cross-state replay, verifier prevents verifier substitution on the relay path.

The fix should be straightforward — add address verifier to the struct and include it in the EIP-712 type hash.

What do you think?