8.0
热点
AI SCORE
技术实践2026-09-08 16:33
用 Solidity 智能合约为 AI Agent 构建 USDC 托管支付
dev.to · AI#智能合约#USDC#AI Agent
Editor brief · 编辑速览
通过链上托管解决 Agent 间服务调用的付款信任问题:客户质押 USDC,Agent 交付可验证凭证后自动释放资金,支持 Base 等 L2 低手续费链。
目标读者:正在构建需要链上支付或接收微服务费用的自主 AI Agent 的开发者。
自主 Agent 经常需要相互调用能力(例如,视觉模型调用语言模型进行后处理)。直接的链上支付适用于静态定价,但许多服务是有状态的:Agent 必须验证工作完成后才能释放资金。托管合约解决了这个先有鸡还是先有蛋的问题:
由于合约持有资金,任何一方都无法在未经对方同意的情况下卷走对方的钱——或者引发争议机制(我们暂时保持简单)。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract USDCتEscrow {
// ERC20 token address (USDC on Base: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)
IERC20 public immutable usdc;
address public immutable client;
address public immutable agent;
uint256 public amount; // escrowed amount in USDC (6 decimals)
enum State { Created, Funded, Completed, Refunded, Released }
State public state;
event Deposited(address indexed from, uint256 amount);
event Released(address indexed to, uint256 amount);
event Refunded(address indexed to, uint256 amount);
constructor(
address _usdc,
address _client,
address _agent,
uint256 _amount
) {
require(_usdc != address(0), "zero token");
require(_client != address(0), "zero client");
require(_agent != address(0), "zero agent");
require(_amount > 0, "zero amount");
usdc = IERC20(_usdc);
client = _client;
agent = _agent;
amount = _amount;
state = State.Created;
}
/// @notice Client funds the escrow. Can be called only once.
function deposit() external {
require(msg.sender == client, "only client");
require(state == State.Created, "wrong state");
require(usdc.transferFrom(client, address(this), amount), "transfer failed");
state = State.Funded;
emit Deposited(msg.sender, amount);
}
/// @notice Agent calls after completing work and providing proof off‑chain.
/// The verifier (client or a trusted oracle) must call `release` after validating the proof.
function release() external {
require(msg.sender == client, "only client can release");
require(state == State.Funded, "not funded");
require(usdc.transfer(agent, amount), "transfer failed");
state = State.Released;
emit Released(agent, amount);
}
/// @notice Client can reclaim funds if the agent never completes work.
/// A timeout or dispute period can be added; here we allow immediate refund for simplicity.
function refund() external {
require(msg.sender == client, "only client");
require(state == State.Funded, "not funded");
require(usdc.transfer(client, amount), "transfer failed");
state = State.Refunded;
emit Refunded(client, amount);
}
/// @notice Helper to check if escrow is currently funded.
function isFunded() public view returns (bool) {
return state == State.Funded;
}
}
合约是最简版本:无升级性,无复杂争议解决机制。
Base 上的 USDC 有 6 位小数;amount 参数必须反映这一点。
Agent 从不触碰托管资金;只有委托方可以调用 release 或 refund。
在生产系统中,你需要添加超时机制(例如 block.number > deadline)以允许自动退款。
以下是自主 Agent 如何完成以下操作的简洁示例:
import { ethers } from "ethers";
import escrowAbi from "./USDCتEscrow.json"; // ABI generated by solc
import { keccak256, toUtf8Bytes } from "ethers/lib/utils";
// ---------- Configuration ----------
const RPC_URL = "https://base.mainnet.rpc.dev"; // public Base RPC
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const ESCROW_ADDRESS = "0xYourEscrowDeployedHere";
const PRIVATE_KEY = process.env.AGENT_PRIVATE_KEY!; // never hard‑code
const CLIENT_ADDRESS = "0xClientThatWillPay";
const AGENT_ADDRESS = await new ethers.Wallet(PRIVATE_KEY).getAddress();
const AMOUNT_USDC = ethers.utils.parseUnits("0.05", 6); // $0.05
// ---------- Setup ----------
const provider = new ethers.JsonRpcProvider(RPC_URL);
const signer = new ethers.Wallet(PRIVATE_KEY, provider);
const usdc = new ethers.Contract(USDC_ADDRESS, ["function balanceOf(address) view returns (uint256)"], signer);
const escrow = new ethers.Contract(ESCROW_ADDRESS, escrowAbi, signer);
// ---------- Helper: call a paid HTTP endpoint ----------
async function callPaidService(url: string): Promise<string> {
// The service returns 402 Payment Required with a macaroon‑style invoice.
// For simplicity we assume the client already paid via escrow and the service
// accepts a Bearer token derived from the escrow tx hash.
const resp = await fetch(url, {
headers: {
Authorization: `Bearer ${await escrowDepositTxHash()}`, // placeholder
},
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return await resp.text();
}
// ---------- Agent main flow ----------
(async () => {
// 1. Ensure we have enough USDC
const bal = await usdc.balanceOf(AGENT_ADDRESS);
if (bal < AMOUNT_USDC) throw new Error("Insufficient USDC");
// 2. Fund escrow (client does this; agent just verifies)
if (!(await escrow.isFunded())) {
throw new Error("Escrow not funded by client");
}
// 3. Perform work: call the provider's API
const result = await callPaidService("https://api.example.com/summarize");
console.log("Service output:", result);
// 4. Create a receipt: hash of the output + escrow nonce
const receipt = keccak256(toUtf8Bytes(result + await escrow.nonce()));
// In reality you would send this receipt to the client via off‑chain channel
// or store it on IPFS and reference the CID.
// 5. Client validates receipt (off‑chain) then calls escrow.release()
// Here we simulate the client's action by signing a release tx ourselves
// (only possible if the client delegated signing – not recommended).
// For demonstration we just show the call:
const tx = await escrow.release();
await tx.wait();
console.log("Escrow released, agent paid:", AMOUNT_USDC.toString());
})();
Agent 从不直接转移 USDC;它只读取托管状态。
实际支付验证(收据检查)发生在链下;托管合约对工作性质保持无感知。
x402 风格的头部是说明性的;实际实现会使用 x402 规范来嵌入支付指针和 macaroon。