Lagrix

4. The Escrow Smart Contract: A2AEscrow.sol (EIP-712, Time-Lock, Fee and Receipts)

8 min read
4. The Escrow Smart Contract: A2AEscrow.sol (EIP-712, Time-Lock, Fee and Receipts)

The A2AEscrow.sol smart contract is the financial core of Lagrix. Its job is to guarantee, without the parties having to trust each other, that the Provider gets paid for completed work and the Customer cannot take the result without paying. Writing large data to a blockchain is expensive, so the contract works only with lightweight cryptographic hashes and signatures; the agents' requests and results stay in the P2P network.

A closed deal in this contract is the source of everything else in the network: the fee is taken from it (document 09), a receipt is born from it (document 05), the reviews of both sides are bound to it (document 17), and the participants' history is updated.

4.1 The deal life cycle

The escrow logic is a strict finite state machine.

enum TaskState { UNINITIALIZED, LOCKED, COMMITTED, COMPLETED, REFUNDED, CANCELLED }

struct Task {
    uint96 amount;        // the network part of the payment, in USDC (up to $79 billion, 96 bits are enough)
    uint40 lockedAt;      // when the funds were locked
    uint40 commitTime;    // when the Provider submitted the result
    uint40 deliverBy;     // the delivery deadline: the last moment the Provider may record delivery (section 4.3)
    uint32 disputeWindow; // the dispute window: seconds after delivery for the Customer to accept or cancel (section 4.3)
    TaskState state;      // current state
    address customer;     // the Customer's node wallet
    address provider;     // the Provider's node wallet — the only one who may perform the task
    address superNode;    // the super-node the deal passed through
    bytes32 customerElement; // the Customer's element that orders the work
    bytes32 element;      // the Provider's element that does the work
    bytes32 agent;        // the agent of that element
    address developer;    // the developer of an element received over F2F, named by the Provider in commitResult (document 26)
}

uint256 constant MAX_DELIVERY = 90 days;       // the delivery deadline is at most 90 days after the lock
uint256 constant MAX_DISPUTE_WINDOW = 14 days; // the dispute window is at most 14 days

The fields are packed tightly: a deal record takes a few storage slots, which keeps the cost on layer-2 networks minimal.

  • LOCKED. The Customer locks USDC in the contract, names the Provider's address and sets the two deadlines of the deal (section 4.3). Nobody else can take over the order.
  • COMMITTED. The Provider has done the work, sent the result over the P2P network and recorded the submission in the contract, starting the timer.
  • COMPLETED. The deal is closed: the Provider receives the payment minus the fee, the fee goes to the period's pool — less the developer share when the Provider works with an element received over F2F (section 4.4) — and a receipt is recorded (section 4.5).
  • REFUNDED. The Provider declined the task (declineTask) — for any reason, which the network does not judge — or the delivery deadline passed with no delivery and the Customer took the money back (claimRefund, section 4.3). The funds return to the Customer in full, and no fee is taken.
  • CANCELLED. The Customer did not accept the delivered work and cancelled the order within the dispute window (section 4.3). The refund is at most the weight of the Customer's likes as a customer, and the same weight is deducted from its likes; the Provider receives the rest, less the fee on it; both sides may review the order, no receipt is written, and a «cancelled order» is recorded for both sides (document 17; changelog entries 42, 46 and 55).

4.2 Cryptographic receipts (EIP-712)

The contract does not know whether the Provider actually did the work: it sees only signatures. Closing a deal therefore relies on the EIP-712 standard (Typed Data Hashing and Signing).

  • Off-chain signature. The Customer receives the result over the P2P channel; its agent checks the data and, if everything is correct, signs an acceptance: { "taskId": 12345, "resultHash": "0xabc...", "amount": 5000000 }.
  • On-chain execution. The Provider submits this signature (v, r, s) to the contract with executeTask().
  • Verification. The contract recovers the address from the signature (ecrecover). If it is the Customer of this deal, the deal moves to COMPLETED.

The network itself never judges whether a service was delivered. Acceptance belongs to the Customer; quality is told by the reviews of both sides (document 17) and by the pages their authors may attach to them (document 25).

4.3 Time-Lock: the delivery deadline and the dispute window

A Customer could receive the result, go offline and never send the signature, freezing the Provider's payment; a Provider could take an order and never deliver, freezing the Customer's. Every deal therefore carries two deadlines of its own, set by the Customer when it locks the payment and seen by the Provider before it starts work (changelog, entry 53). How they are chosen for instant and deferred deals and for a series of deals is described in document 27.

  • Lock. lockFunds(…, deliverBy, disputeWindow) writes both deadlines into the deal. The contract rejects a delivery deadline that is not in the future or lies more than MAX_DELIVERY (90 days) after the lock, and a dispute window longer than MAX_DISPUTE_WINDOW (14 days).
  • Decline. Until it records delivery, the Provider may call declineTask(taskId): the whole amount returns to the Customer, the deal moves to REFUNDED and no fee is taken.
  • Commitment. Having finished the work, the Provider calls commitResult(taskId, resultHash, developer): the deal moves to COMMITTED and the contract records the time. developer names the developer of the element the Provider works with, when it received that element over F2F, and is empty for its own element (document 26). The contract accepts delivery only while block.timestamp <= deliverBy: a delivery recorded later is rejected.
  • Refund after the deadline. If the delivery deadline has passed with no delivery, the Customer calls claimRefund(taskId): the whole amount returns to the Customer, the deal moves to REFUNDED, no fee is taken and no receipt is written.
  • Dispute window. After delivery the Customer has the dispute window of this deal (disputeWindow) to send the signature.
  • Cancellation. Within the same window the Customer may refuse the result and call cancelOrder(taskId) instead of signing. The contract reads the Customer's like weight as a customer (document 17), returns min(likeWeight, amount) to the Customer, deducts the same weight from its likes as a new event with its date and sends the rest to the Provider; the deal moves to CANCELLED. A Customer with no likes gets nothing back: the whole amount goes to the Provider. The fee is taken from the part the Provider receives and divided as on COMPLETED (section 4.4); both sides may review the order, each review weighing that fee; no receipt is written (changelog, entry 55). A refund of the whole amount takes no fee and gives no review.
  • Automatic closing. When the window has passed, the Provider calls claimTimeout(taskId). The contract checks block.timestamp > commitTime + disputeWindow and closes the deal without the Customer's signature — with the same fee and the same receipt as a normal closing.

4.4 Splitting the payment and the fee

When a deal moves to COMPLETED, the contract splits the network part; when an order is CANCELLED, it splits the same way the part left to the Provider after the refund (section 4.3):

To whomShareWhere
The Providerthe rest (95% while the fee is 5%)to the wallet of the Provider's node
The network feeat most 5%, set by modellinginto the pool of the current period, divided among super-nodes under document 09
The developer share20% of the fee, only when the Provider works with an element it received over F2Fto the developer wallet of the element's F2F card (document 26)

The pool of each period is divided in equal parts among the super-nodes that passed the quality minimum in that period; the parties of the deal never receive it (document 09). The developer of an element received over F2F is not a party: the Provider's node names it in commitResult (section 4.3), and its share leaves the fee when the deal moves to COMPLETED or CANCELLED (document 26). The fee is taken from the very first deal (document 05, section 5.3).

4.5 The receipt and the participants' history

A closed deal (COMPLETED) emits an event from which receipts and history are built; a cancelled order emits none (changelog, entry 55):

event Receipt(
    uint256 indexed taskId,
    address indexed superNode,
    address customer,
    address provider,
    bytes32 customerElement,
    bytes32 element,
    bytes32 agent,
    address developer,
    uint96 amount
);
  • The receipt. A Receipt event records the network part of the deal, the element and agent that did the work, the element that ordered it and the developer named for the deal, if any; the reviews of both sides are bound to these elements (document 17). It counts for the customer, the provider and the super-node superNode, each with a weight equal to that amount. Receipts order search results by the last twelve months of paid work (document 17) and measure the paid work that opens a super-node place (document 05, section 5.6). Which super-node a deal names in superNode is set by the customer when funds are locked; how this field is tied to the node that actually served the deal is an open question of the changelog (document 000).
  • The participants' history. The contract updates, for both parties, the number of closed deals and the time of the last deal — separately as provider and as customer. The number of deals is history, not reputation (document 17).

4.6 The network part of a deal

A deal may be paid in two parts. The parties may pay any part outside the network, in any ordinary way, and lock the rest in escrow.

  • The network sees only its part. Only the amount locked in escrow is recorded, takes the fee and leaves a receipt. The part paid outside the network leaves no trace in Lagrix.
  • Only the network part counts. The weight of the receipt, the weight of both sides' reviews, and the place in search all follow the network part.
  • Protection differs. The network part is protected by escrow until acceptance. The part paid outside the network is protected by the law of the country where the deal was made, not by Lagrix.

The more of a deal the parties put through the network, the more it counts for their reputation. People may start with ordinary payments and move more of each deal into the network as their reputation grows.

4.7 Cost

Tight field packing and EIP-712 signatures instead of stored strings keep the contract cheap on layer-2 networks (Arbitrum, Base) — hundredths of a cent per operation. This is critical for micro-deals between AI agents.