Skip to main content

Trustless Sessions

This document describes a future enhancement to the escrow system. The current implementation uses a trusted facilitator model. Trustless verification is planned for a future release.

Current Trust Model

The current escrow system has a trusted facilitator model:
ComponentTrust LevelNotes
Deposit authorizationTrustlessERC-3009 signature verified on-chain
Off-chain debitsTrustedFacilitator tracks in database
Capture amountsTrustedFacilitator decides what to capture
Fund recoveryTrustlessSmart contract enforces reclaim after expiry
Key assumption: Users trust the facilitator to honestly report API consumption.

What’s Trustless Today

  • Deposits: User signs ERC-3009 authorization - cryptographically verified on-chain
  • Reclaims: Smart contract enforces reclaim after authorizationExpiry - no facilitator involvement needed
  • Bounded exposure: Only deposited amount at risk, never entire wallet

What Requires Trust

  • Usage tracking: Facilitator maintains authoritative ledger of session debits
  • Capture amounts: Facilitator decides how much to capture - no cryptographic proof submitted
  • Billing accuracy: No on-chain verification that captures match actual consumption

Trustless Architecture (Planned)

Overview

The trustless model replaces facilitator trust with cryptographic verification:
1. SESSION CREATION (unchanged)
   User signs ERC-3009 (one-time)
   Facilitator creates session, returns sessionToken

2. SESSION USAGE (unchanged UX)
   Client sends session token per request
   Facilitator returns CUMULATIVE receipt:
     { sessionId, nonce, cumulativeAmount, timestamp, facilitatorSig }
   Client stores only latest receipt (can re-fetch if needed)

3. FACILITATOR COMMITS CAPTURE
   Builds merkle tree of ALL issued receipts
   Publishes: (merkleRoot, captureAmount) for sessionId
   Stakes collateral (10% of capture value)
   CRITICAL: Cannot forge receipts after commitment

4. CHALLENGE WINDOW (3-7 days)
   User disputes if captureAmount > their latest receipt's cumulative
   Provides their highest-nonce receipt as proof
   Facilitator can counter with higher-nonce receipt if user is lying
   BUT: Facilitator must prove receipt exists in committed merkle tree
   Contract compares nonces: highest nonce wins (if merkle proof valid)

5. SETTLEMENT
   After window: funds released to receiver
   Collateral returned to facilitator

Cumulative Receipts

For each API request, the facilitator signs a cumulative receipt showing the running total spent. This prevents users from selectively discarding receipts.
interface CumulativeReceipt {
  sessionId: bytes32;          // Links to on-chain escrow (and original ERC-3009)
  nonce: uint256;              // Monotonically increasing per session
  cumulativeAmount: uint256;   // TOTAL spent so far (not just this request)
  timestamp: uint256;          // Unix timestamp
  facilitatorSignature: bytes; // EIP-712 signature over all above fields
}
The facilitatorSignature is the key binding:
  • Proves facilitator acknowledged this exact (sessionId, nonce, amount) tuple
  • Cannot be forged by user (only facilitator has signing key)
  • sessionId links to on-chain escrow → links to original ERC-3009 authorization
Example sequence:
Request 1:  { nonce: 1,   cumulativeAmount: $0.05 }
Request 2:  { nonce: 2,   cumulativeAmount: $0.10 }
Request 3:  { nonce: 3,   cumulativeAmount: $0.15 }
...
Request 100: { nonce: 100, cumulativeAmount: $5.00 }
Why cumulative? Users can only dispute using their highest nonce receipt. If the latest receipt shows $5.00 cumulative, they cannot claim less was consumed. No selective discarding possible.
Receipts are returned in the PAYMENT-RESPONSE header. Clients only need to store their latest receipt as proof.

Receipt Re-Fetching

If a receipt response is lost (network failure), clients can re-fetch any past receipt:
GET /api/receipts/:sessionId/:nonce
Authorization: Bearer <sessionToken>

Response: { receipt: CumulativeReceipt }
Clients should sync their receipts before the challenge window opens. After capture is committed, the facilitator may not be required to serve old receipts.

Why This Prevents Attacks

Attack: “I only received 80 receipts, not 100”Defense: Cumulative model means only the highest nonce receipt matters. If user’s latest receipt shows { nonce: 100, cumulative: $5.00 }, the cumulative amount proves total consumption.
Attack: User discards receipt showing 5.00,disputeswithearlier5.00, disputes with earlier 3.00 receiptDefense: Facilitator counter-disputes with the higher-nonce receipt:
  • User provides: { nonce: 60, cumulative: $3.00 }
  • Facilitator counters: { nonce: 100, cumulative: $5.00 } with valid signature
  • Contract verifies: facilitator’s nonce > user’s nonce → dispute rejected
Attack: Facilitator captures 10.00butonlyissuedreceiptsfor10.00 but only issued receipts for 5.00Defense: User disputes with their highest receipt:
  • User provides: { nonce: 100, cumulative: $5.00 } with valid facilitator signature
  • Facilitator cannot counter (never issued higher receipt)
  • Contract verifies: 5.00<5.00 < 10.00 captured → facilitator slashed
Attack: Facilitator processes requests but doesn’t return receiptsDefense: Without a signed receipt, the charge effectively didn’t happen.
  • User’s latest receipt shows actual acknowledged consumption
  • Facilitator can only capture up to what they signed for
  • Incentivizes facilitator to always return receipts
Attack: Network failure causes user to miss receiptsDefense: Receipt re-fetch endpoint allows recovery:
  • User calls GET /api/receipts/:sessionId/:nonce for any past receipt
  • User syncs all receipts before challenge window
  • User’s responsibility to ensure they have receipts before disputing
Attack: Facilitator adds fake receipts to merkle tree before committingStatus: NOT FULLY PREVENTED - this is a known limitation.Why it’s hard: Facilitator controls the merkle tree. They can add fake receipts before committing. Without per-request user acknowledgments, we cannot prove delivery.Mitigations (not cryptographic):
  • User’s exposure bounded by deposit (maxAmount)
  • Reputation damage deters fraud
  • User can stop using facilitator and reclaim remaining balance
  • Facilitator cannot capture MORE than tree total (still enforced)

Merkle Commitment

Facilitator batches receipts into a merkle tree. This is critical for preventing receipt forgery - once committed, the facilitator cannot create fake receipts.
PropertyValue
Leaf formatkeccak256(abi.encode(receipt))
Tree typeBinary merkle tree
CommitmentcommitCapture(sessionId, merkleRoot, captureAmount)
Collateral10% of batch value
PurposePrevents facilitator from forging receipts after commitment

Challenge Window

  1. Facilitator commits captureAmount = $50 with collateral
  2. User sees capture, checks their latest receipt: { nonce: 80, cumulative: $30 }
  3. User submits disputeOverCapture(sessionId, userReceipt)
  4. Contract verifies: facilitator signature valid, 30<30 < 50
  5. Dispute opens - facilitator has 24-48h to counter
  6. If no valid counter → resolveDispute() slashes facilitator, refunds user $20
Counter-dispute flow:
  • Facilitator submits { nonce: 100, cumulative: $50 } receipt
  • Contract verifies: signature valid, nonce 100 > 80
  • User’s dispute rejected (they had a higher receipt)
  • Over-capture: User’s highest receipt shows less than captured amount
  • Invalid signature: Facilitator signature on receipt doesn’t verify
ParameterValueRationale
Duration3-7 daysBalance security vs UX
Collateral10% of batchEconomic deterrent
Slash100% of disputed amountFull user refund

Smart Contract Interface

interface ITrustlessEscrow {
  // Facilitator commits capture with merkle root of all receipts
  function commitCapture(
    bytes32 sessionId,
    bytes32 merkleRoot,      // Root of all issued receipts
    uint256 captureAmount
  ) external payable; // msg.value = collateral stake (10% of captureAmount)

  // User disputes: "My receipt shows less than you captured"
  function disputeOverCapture(
    bytes32 sessionId,
    CumulativeReceipt calldata userReceipt  // User's highest nonce receipt
  ) external;
  // Contract verifies:
  // 1. facilitatorSignature is valid
  // 2. userReceipt.cumulativeAmount < captureAmount
  // If both true → dispute opened, facilitator must counter

  // Facilitator counters with higher nonce receipt + merkle proof
  function counterDispute(
    bytes32 sessionId,
    CumulativeReceipt calldata facilitatorReceipt,
    bytes32[] calldata merkleProof  // Proves receipt was in committed tree
  ) external;
  // Contract verifies:
  // 1. facilitatorSignature is valid
  // 2. Receipt exists in committed merkle tree (prevents forgery!)
  // 3. facilitatorReceipt.nonce > userReceipt.nonce
  // If all true → dispute rejected (user was lying)

  // After challenge window with no successful counter
  function resolveDispute(bytes32 sessionId) external;
  // Slashes facilitator, refunds user the difference

  // After challenge window with no disputes
  function finalize(bytes32 sessionId) external;
  // Releases funds to receiver, returns collateral
}

struct CumulativeReceipt {
  bytes32 sessionId;
  uint256 nonce;
  uint256 cumulativeAmount;
  uint256 timestamp;
  bytes facilitatorSignature;  // EIP-712 over (sessionId, nonce, cumulativeAmount, timestamp)
}
Verification logic:
function verifyReceipt(CumulativeReceipt calldata receipt) internal view returns (bool) {
  bytes32 structHash = keccak256(abi.encode(
    RECEIPT_TYPEHASH,
    receipt.sessionId,
    receipt.nonce,
    receipt.cumulativeAmount,
    receipt.timestamp
  ));
  bytes32 digest = _hashTypedDataV4(structHash);
  address signer = ECDSA.recover(digest, receipt.facilitatorSignature);
  return signer == facilitatorAddress;
}

Security Properties

PropertyCurrentTrustlessNotes
Over-capture (capture > receipts)UnverifiableTrustlessUser can prove with receipts
Receipt inflation (fake receipts)TrustedTrustedCan’t prove delivery without acks
Fraud detectionNoneChallenge windowFor provable over-capture
Economic securityNoneCollateral slashingFor provable fraud
User recourseOnly reclaimDispute + refundFor over-capture only
Client signingOnceOncePreserves UX
Bounded exposuremaxAmountmaxAmountLimits damage from inflation
Limitation: Facilitator can inflate receipts (add fake ones to merkle tree) because we cannot prove delivery without per-request user signatures. The protection is:
  • Facilitator cannot capture MORE than what’s in their committed tree
  • User’s exposure is bounded by their deposit (maxAmount)
  • Reputation damage from fraud is a deterrent

Trade-offs

Advantages

  • Users can cryptographically prove over-capture fraud
  • Economic incentive against fraud (collateral at risk)
  • Preserves “sign once” UX (no per-request client signatures)
  • Bounded exposure limits damage from any fraud

Disadvantages

  • 3-7 day finality delay (receiver waits for funds)
  • Client must store receipts locally (storage overhead)
  • Requires collateral capital from facilitator
  • More complex implementation
  • Higher gas costs (commitment transactions)
  • Facilitator must sign each response (compute overhead)
  • Receipt inflation still trusted - facilitator can add fake receipts to tree

Migration Path

1

Phase 1: Trusted (Current)

Current model with trusted facilitator. Faster UX, simpler implementation. Trust assumptions clearly documented.
2

Phase 2: Hybrid (Opt-in)

Signed receipts as optional feature. Users who want trustless can enable. Default remains trusted for faster UX.
3

Phase 3: Full Trustless

Signed receipts mandatory. Dispute contract deployed. Watchtower service for monitoring. Deprecate trusted-only mode.

Open Questions

3 days vs 7 days?
  • Shorter = better UX for receivers (faster access to funds)
  • Longer = more time to detect and dispute fraud
Facilitator-funded vs protocol treasury?
  • Facilitator stake = direct incentive alignment
  • Treasury = requires governance, less direct incentive
Who monitors for fraud?
  • Self-monitoring (users watch own sessions)
  • Decentralized watchtower network
  • Agentokratia-operated service
Who pays for dispute transactions?
  • User pays (may disincentivize small disputes)
  • Protocol subsidizes (encourages fraud detection)

References