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.

4 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.

1 Like

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

2 Likes

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?

@xrqin good catch on the verifier gap, you’re right that hostStateHash and verifier protect against different things, cross-state replay vs verifier substitution.

Fixed in the ref impl. address verifier now sits in ClaimRelayRequest between proofDigest and hostStateHash, and the EIP-712 sig binds it so the relay can submit but can’t swap.

struct ClaimRelayRequest {
    bytes32 channelId;
    bytes32 lockId;
    bytes32 escrowCommitment;
    bytes32 outputCommitmentHash;
    uint256 maxRelayFee;
    uint256 deadline;
    bytes32 proofType;
    bytes32 proofDigest;
    address verifier;       // new, locked into typehash
    bytes32 hostStateHash;
    uint256 nonce;
}

Updated EIP-712 type string:

ClaimRelayRequest(
  bytes32 channelId,
  bytes32 lockId,
  bytes32 escrowCommitment,
  bytes32 outputCommitmentHash,
  uint256 maxRelayFee,
  uint256 deadline,
  bytes32 proofType,
  bytes32 proofDigest,
  address verifier,
  bytes32 hostStateHash,
  uint256 nonce
)

Commit: Aboudjem/erc-8203-ref@48afc3a. 24/24 tests still green, will add a dedicated substitution-attack test next so the regression is explicit.

Worth a one-liner in the spec security considerations imo, something like “the relay path MUST bind verifier in the signed request to prevent substitution on assembly”. Can open a PR for that if useful.

Also, the metered billing worked example is in decent shape, planning to drop it as a non-normative profile next week and will tag this thread.


@JackyWang thanks for opening 28097, will drop a longer reply there. Short version on the batch question: each batch item as a separate Job feels right. The merkle settlement on 8203 is a gas-amortization trick, doesn’t change the unit-of-claim, so JobAssurance stays 1:1 and partial claims fall out naturally.

On dispute interop, settleConditional’s outcome (proofType + proofDigest) is the on-chain attestation your resolvers can consume directly. IRiskHook can hash against the digest. Can mock a flow if it helps.

1 Like

@xrqin small PR on your branch (fix: satisfy eipw markdown-rel-links on ref impl link by Aboudjem · Pull Request #1 · xrqin/ERCs · GitHub) that clears the eipw markdown-rel-links fail. moved the interface into assets/erc-8203/ and pointed at it with a relative link. once you pull it in, CI on Add ERC: Agent Offchain Conditional Settlement by xrqin · Pull Request #1614 · ethereum/ERCs · GitHub should go green.

1 Like

merged. Thank you @Aboudjem !

1 Like

Metered billing worked example from #16:

Repo: GitHub - Aboudjem/erc-8203-metered-billing: Non-normative profile for ERC-8203: pay-per-call billing for autonomous agents, without paying gas per call. · GitHub
Spec: erc-8203-metered-billing/docs/profile.md at main · Aboudjem/erc-8203-metered-billing · GitHub

Pattern. Client locks a max budget via ConditionalLock, provider streams signed meter receipts off-chain, either party calls settleConditional with the latest receipt to close. The metering oracle slots into the verifier field from #15:

ConditionalLock {
  amount:        50 USDC          // hard ceiling
  verifier:      meteringOracle   // signs receipts
  expiry:        sessionStart + 24h
  conditionType: COMPOSITE        // oracle attestation + timelock
}

Sequence numbers in receipts block replay, and refundConditional covers the timeout case. Reference impl at GitHub - Aboudjem/erc-8203-ref: Reference implementation for ERC-8203: Agent Off-Chain Conditional Settlement · GitHub doesn’t change.

Three open questions worth a read. The profile defaults to COMPOSITE because it makes the dispute path explicit, but single-condition sessions could use ORACLE_ATTESTATION directly. Which form is the canonical example? Second, batching: the profile uses proofType: RECEIPT_ROOT with merkle leaves of (lockId, cumulativeAmount, oracleSignature), roughly 20x cheaper at 50 sessions. Does that belong in the spec or stay in the profile? Third: should the oracle attest to units only and let the contract compute amount, or attest to the final amount? Units-only is cleaner but bakes the price model into the contract.

@JackyWang on partial claims from #14: each merkle leaf is its own settlement, so it doesn’t come up at the batch layer. Inside a single session it would be a separate extension.

2 Likes

Thanks for the worked example and for closing the loop on the partial claim question. “Each merkle leaf is its own settlement” makes the AAP integration simpler: if each leaf maps to an independent Job, then each gets its own JobAssurance and the partial claim path stays clean. No special handling needed on the AAP side.

Jacky

nice! the profile reads cleanly and the worked example is exactly what the spec needed to show the pattern is actually usable.

on your three questions:

COMPOSITE vs ORACLE_ATTESTATION — I’d go with COMPOSITE as the canonical example. Single-condition sessions could use ORACLE_ATTESTATION directly, but in practice you almost always want the timelock fallback so the client can refund if the provider disappears. Makes the dispute path explicit from the start.

batching — better keep it in the profile. The core spec is already at the right level of abstraction. Batching is an optimization for high-volume operators, not a primitive. If multiple implementations independently converge on the same merkle leaf format, that’s the signal to pull it up.

oracle attests units vs amount — amount directly. Units-only is cleaner in theory but bakes the price model into the contract, which makes upgrades painful. The oracle knowing the final amount is fine; it’s within the trust boundary you’ve already accepted by using a metering oracle at all.

the composition with ERC-8183 section is a good addition — worth keeping that in the profile. might reference it from the spec’s rationale section to show the layering in practice.

1 Like