详解基于 Base 链的 USDC Escrow 机制解决 AI Agent 收费难题:稳定币无波动、2 秒出块、Gas 费极低,适合 AI Agent 的微支付场景。
目标读者:正在构建需要自主接收付款的 AI Agent(而无需依赖中心化中介)的开发者。
AI Agent 通常作为"黑箱"工作者运作:它们接收请求、执行计算(如 LLM 推理、数据标注、微任务执行),然后返回结果。在纯点对点模型中,请求者必须信任 Agent 会在付款前完成工作,而 Agent 则必须信任请求者在看到输出后会付款。这个双向信任问题通过托管机制来解决——托管将资金锁定直至满足可验证的条件。
在像 Base 这样的低费用 L2 上使用 USDC 给我们带来:
稳定价值——1 USDC ≈ 1 美元,避免了与波动性相关的定价头痛。
快速最终性——Base 上约 2 秒出块时间,保持交互式 Agent 的低延迟。
低 Gas——典型交易成本低于 0.001 美元,使微支付变得可行。
托管并不能消除对链外工作验证的需求;它只是将信任从交易对手转移到确定性合约加上验证机制(预言机、仲裁人或证明)。
+----------------+ +----------------+ +----------------+
| Requester | <---> | Escrow (SC) | <---> | AI Agent |
| (pays USDC) | deposit| holds USDC | earns | (does work) |
+----------------+ +----------------+ +----------------+
^ | |
| dispute / refund | proof of completion |
+-------------------------+-------------------------+
资金注入——请求者将 USDC 存入托管合约,指定 Agent 地址和最高价格。
工作触发——Agent 调用 startWork 函数(或简单地监听存款事件)并开始链外任务。
完成证明——工作完成后,Agent 通过 submitProof 提交加密证明(例如,存储在链上的输出哈希,或来自可信预言机的签名)。
释放——如果证明在挑战期内通过验证,托管将资金释放给 Agent。
退款——如果 Agent 未能在超时前提交有效证明,请求者可以调用 refund 取回存款。
合约有意做得极简;任何复杂的仲裁机制(例如多签陪审团)都可以叠加在其上,而无需更改核心托管逻辑。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
}
contract USDCewiseEscrow {
IERC20 public immutable usdc; // USDC on Base (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)
address public requester;
address public agent;
uint256 public amount; // locked USDC
uint256 public deadline; // block.timestamp after which requester can refund
bytes32 public proofHash; // keccak256 of the expected output (set by agent)
bool public released;
bool public refunded;
// Roles
address public arbiter; // optional, can be 0x0 for trust‑less version
event Deposited(address indexed requester, address indexed agent, uint256 amount);
event ProofSubmitted(address indexed agent, bytes32 proofHash);
event Released(address indexed agent, uint256 amount);
event Refunded(address indexed requester, uint256 amount);
constructor(
address _usdc,
address _requester,
address _agent,
uint256 _amount,
uint256 _timeoutSeconds, // e.g., 1 hour
address _arbiter
) {
require(_usdc != address(0), "USDC addr");
require(_requester != address(0), "Requester addr");
require(_agent != address(0), "Agent addr");
require(_amount > 0, "Zero amount");
usdc = IERC20(_usdc);
requester = _requester;
agent = _agent;
amount = _amount;
deadline = block.timestamp + _timeoutSeconds;
arbiter = _arbiter == address(0) ? address(this) : _arbiter;
// Pull funds from requester (they must have approved the contract)
require(usdc.transferFrom(requester, address(this), amount), "Transfer failed");
emit Deposited(requester, agent, amount);
}
/// @notice Agent submits a hash of the work output. The requester (or arbiter)
/// later reveals the pre‑image to prove correctness.
function submitProof(bytes32 _proofHash) external {
require(msg.sender == agent, "Only agent");
require(!released && !refunded, "Already settled");
proofHash = _proofHash;
emit ProofSubmitted(agent, _proofHash);
}
/// @notice Requester (or arbiter) provides the pre‑image; if it matches,
/// funds are released to the agent.
function release(bytes calldata _preimage) external {
require(msg.sender == requester || msg.sender == arbiter, "Unauthorized");
require(!released && !refunded, "Already settled");
require(keccak256(_preimage) == proofHash, "Invalid proof");
released = true;
usdc.transfer(agent, amount);
emit Released(agent, amount);
}
/// @notice Requester can reclaim funds after deadline if no valid proof.
function refund() external {
require(msg.sender == requester, "Only requester");
require(block.timestamp >= deadline, "Not timed out");
require(!released && !refunded, "Already settled");
refunded = true;
usdc.transfer(requester, amount);
emit Refunded(requester, amount);
}
/// @notice Fallback to reject plain ether.
receive() external payable {
revert("Only USDC accepted");
}
}
合约通过 ERC-20 transferFrom 持有 USDC;请求者必须事先批准托管合约(标准 ERC-20 流程)。
Agent 仅提交哈希(proofHash)。这将实际结果保留在链外,在保护隐私的同时仍允许请求者通过揭示原像来验证。
简单的超时机制实现退款;仲裁人地址可以设置为多签或 DAO 以进行争议解决。
在 Base 上,典型 deposit → submitProof → release 流程的 Gas 成本大约为 150k gas(在 5 gwei 时约为 0.001 美元)。
import { ethers } from "ethers";
import usdcAbi from "./usdc-abi.json"; // minimal IERC20 ABI
import escrowAbi from "./escrow-abi.json";
// Configuration (Base mainnet)
const RPC_URL = "https://base-mainnet.infura.io/v3/<PROJECT_ID>";
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const PRIVATE_KEY = process.env.AGENT_PRIVATE_KEY!; // funded with a little ETH for gas
const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const usdc = new ethers.Contract(USDC_ADDRESS, usdcAbi, signer);
// Helper: approve escrow to pull USDC (called once per session)
async function approveEscrow(escrow: string, amount: ethers.BigNumberish) {
const tx = await usdc.approve(escrow, amount);
await tx.wait();
}
// Agent workflow
async function runJob(escrowAddress: string, jobInput: any) {
const escrow = new ethers.Contract(escrowAddress, escrowAbi, signer);
// 1. Wait for funding (listening to Deposit event)
const filter = escrow.filters.Deposited(null, signer.address);
escrow.on(filter, async (requester, agent, amount) => {
console.log(`Funded: ${ethers.formatUnits(amount, 6)} USDC from ${requester}`);
// 2. Do the work off‑chain (example: LLM inference)
const result = await performInference(jobInput); // returns string/bytes
const proofHash = ethers.keccak256(ethers.toUtf8Bytes(result));
// 3. Submit hash to escrow
const submitTx = await escrow.submitProof(proofHash);
await submitTx.wait();
console.log("Proof submitted:", proofHash);
// 4. Wait for release (or timeout)
escrow.once("Released", (agent, amount) => {
console.log(`Paid ${ethers.formatUnits(amount, 6)} USDC`);
});
});
}