How must agents communicate partial settlements when the blockchain is congested?
**Date of Record:** 2026 October 27 **Scope:** Applies when autonomous agents execute high-frequency trades or payments via a blockchain, and during congestion, on-chain settlement becomes too expensive or too slow, requiring off-chain settlement channels. Does not apply to low-frequency trades or systems with no off-chain settlement capability.
Core Distinction
The separation between **on-chain settlement** (each trade is a blockchain transaction, final and costly during congestion) and **off-chain channel state** (trades are tracked off-chain and only the final net settlement is submitted on-chain). Relying on on-chain settlement creates the **Congestion Bankruptification**: during congestion, transaction fees exceed trade value, and the agent cannot settle trades at all, freezing operations.
What Was Observed
In autonomous agent trading architectures, failing to use off-chain settlement during congestion produces three primary failure modes:
- **Fee Explosion:** Gas prices spike, on-chain settlement becomes more expensive than the trade value, and the agent either aborts (breaking obligations) or loses money on fees.
- **Stuck Payments:** Transactions sit in the mempool for hours, unsettled. The agent's counterparty does not receive funds, and the agent's balance is exhausted but payments are not final.
- **State Divergence:** The agent tracks on-chain state, but the mempool's pending transactions cause the effective balance to differ from the on-chain balance, leading to inconsistent decisions.
Recorded Elements
- **`off_chain_channel_state_hash`:** The current state of the off-chain channel (balances, pending trades, settlement deadline), cryptographically signed by both parties.
- **`dispute_window_epoch`:** The exact epoch until which the off-chain state can be disputed on-chain before it becomes final.
- **`net_settlement_receipt`:** The cryptographic proof that both parties agree on the net settlement amount to be submitted on-chain after the dispute window.
- **`congestion_detection_receipt`:** The cryptographic proof that congestion was detected (e.g., median gas price above threshold, mempool depth), triggering the switch to off-chain.
Condition of Validity
* **Applies when:** The agent executes high-frequency trades, on-chain settlement is congested, and both parties have agreed to an off-chain settlement protocol. * **Does not apply when:** The trades are low-frequency (each settlement is independently justified), or the agents have no off-chain capability or trust relationship.
Decision Boundary
- **Is the blockchain congested (gas price above threshold, mempool backed up)?** If no, on-chain settlement is fine. If yes, proceed.
- **Does the agent have an open off-chain channel with the counterparty?** If no, the agent cannot use off-chain settlement and must wait or abort. If yes, proceed.
- **Is the `off_chain_channel_state_hash` signed by both parties?** If no, the state is not mutually agreed. The agent must refuse to trade until consensus is reached.
- **Conversely, when can an agent abandon off-chain settlement and go back to on-chain?** Only when congestion clears, or the off-chain channel is closed via the `net_settlement_receipt` submission on-chain.
Operational Effect
- **Congestion Resilience:** Enables trading to continue during blockchain congestion by moving state off-chain.
- **Fee Minimization:** Reduces per-trade cost to near zero during congestion, only paying on-chain for net settlement.
- **Finality Assurance:** Uses the dispute window to ensure both parties agree on the state before the net settlement is submitted, preventing disputes.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class SettlementVerdict(Enum):
ON_CHAIN_ACCEPTABLE = auto()
OFF_CHAIN_ACTIVE = auto()
OFF_CHAIN_UNAUTHORIZED = auto()
DISPUTE_WINDOW_EXPIRED = auto()
@dataclass(frozen=True)
class ChannelState:
channel_id: str
off_chain_balance_a: int
off_chain_balance_b: int
signed_by_a: bool
signed_by_b: bool
dispute_window_epoch: float
current_epoch: float
@dataclass(frozen=True)
class CongestionContext:
gas_price_gwei: float
congestion_threshold_gwei: float
trade_value_wei: int
@dataclass(frozen=True)
class SettlementManifest:
verdict: SettlementVerdict
directive: str
manifest_digest: str
class OffChainSettlementGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_settlement(
cls, channel: ChannelState, ctx: CongestionContext
) -> SettlementManifest:
# Gate 1: No congestion
if ctx.gas_price_gwei <= ctx.congestion_threshold_gwei:
digest = cls._hash(json.dumps({
"channel": channel.channel_id,
"gas": f"{ctx.gas_price_gwei:.2f}",
"mode": "ON_CHAIN"
}, sort_keys=True))
return SettlementManifest(
verdict=SettlementVerdict.ON_CHAIN_ACCEPTABLE,
directive=f"PROCEED: Gas {ctx.gas_price_gwei:.2f} below threshold {ctx.congestion_threshold_gwei:.2f}. On-chain ok.",
manifest_digest=digest
)
# Gate 2: Mutual signing
if not (channel.signed_by_a and channel.signed_by_b):
return SettlementManifest(
verdict=SettlementVerdict.OFF_CHAIN_UNAUTHORIZED,
directive=f"HALT: Channel {channel.channel_id} not mutually signed. Cannot use off-chain.",
manifest_digest=cls._hash(f"UNSIGNED:{channel.channel_id}")
)
# Gate 3: Dispute window
if channel.current_epoch > channel.dispute_window_epoch:
return SettlementManifest(
verdict=SettlementVerdict.DISPUTE_WINDOW_EXPIRED,
directive=f"HALT: Dispute window expired at {channel.dispute_window_epoch:.0f}. Submit net settlement on-chain.",
manifest_digest=cls._hash(f"EXPIRED:{channel.channel_id}")
)
# Gate 4: Off-chain active
digest = cls._hash(json.dumps({
"channel": channel.channel_id,
"balance_a": str(channel.off_chain_balance_a),
"balance_b": str(channel.off_chain_balance_b),
"dispute_until": f"{channel.dispute_window_epoch:.0f}",
"gas": f"{ctx.gas_price_gwei:.2f}"
}, sort_keys=True))
return SettlementManifest(
verdict=SettlementVerdict.OFF_CHAIN_ACTIVE,
directive=f"PROCEED: Use off-chain settlement. Dispute until {channel.dispute_window_epoch:.0f}.",
manifest_digest=digest
)