Recurring payments for AI agents
AuthOnce has native ERC-1271 support for smart-contract agent wallets, alongside standard EIP-2612 permit support for EOA agents, on Base Network.
Developer guide. This page assumes familiarity with EVM smart contracts, ethers.js or viem, and AI agent wallet architectures. For a business-level overview, read our AI agent payments blog post first.
Status: ERC-1271 subscriptions are not yet live in production. createSubscription() works today and records a smart-contract-wallet subscription on-chain. But the production keeper bot does not yet execute recurring pulls for ERC-1271 subscriptions — that support is still being built. A subscription created via this path will not be billed automatically until this ships. If you're integrating an AI agent for production use now, contact us first so we can tell you the real status.
Why AI agents need AuthOnce
AI agents increasingly need to pay for services autonomously — API access, data feeds, compute, tooling. Current payment infrastructure is not designed for autonomous actors:
- Requires human to enter card details
- OAuth flows need browser + human
- API keys are not payment primitives
- No programmable spending limits
- Chargebacks and disputes possible
- EOA agents: sign a permit once. Smart-contract agent wallets: sign a fresh authorisation each cycle
- No manual re-entry of payment details after setup
- Payment IS the access credential
- Guardian sets hard spending limits
- On-chain — cryptographically final
How it works
Agent wallet creates the subscription
The AI agent's smart contract wallet calls createSubscription(), registering the token, amount, interval, and grace period on-chain. This one call does not authorise any future payment by itself.
Vault stores the subscription on-chain
The subscription is registered in the SubscriptionVault contract. The agent's vault address, token, amount, interval, and grace period are immutably stored.
Agent wallet signs a fresh authorisation each cycle
Unlike an EOA's one-time permit, a smart-contract wallet must produce a new EIP-712 signature — bound to that specific subscription, amount, pull number, and deadline — for every single billing cycle. Something has to generate that signature autonomously each cycle for this to run without a human; AuthOnce does not hold a key that can do this on the agent's behalf.
Keeper submits the pull
The keeper bot calls executePull() with that cycle's signature. The contract validates it via isValidSignature() and transfers USDC from the agent's vault to the merchant.
Guardian retains override control
The human operator (guardian) can pause or cancel any subscription at any time. The agent cannot exceed its pre-authorised spending without guardian approval.
ERC-1271 & EIP-2612
ERC-1271 — Smart contract signature validation
ERC-1271 is the standard for smart contract wallets to validate signatures. Instead of a private key signing a message (as with EOAs), the contract exposes an isValidSignature(bytes32 hash, bytes signature) function. AuthOnce's vault contract calls this during executePull() to verify the agent authorised the subscription.
This means any AI agent wallet that implements ERC-1271 — including Safe multisigs, custom agent wallets, and EIP-4337 smart accounts — is natively compatible with AuthOnce.
EIP-2612 — Permit-based token approval
EIP-2612 permits allow token approvals via off-chain signatures rather than on-chain transactions. For AI agents:
- No gas required for the approval step
- The permit is included in the
createSubscription()call — one transaction to set up the whole subscription - USDC on Base Network natively supports EIP-2612
EOA agents work too. If your agent uses a regular private key (EOA), the ERC-1271 check is skipped entirely — no per-cycle signature needed. The EIP-2612 permit, bundled into the same createSubscription() call described above, is what authorises the keeper to pull USDC on schedule going forward.
Agent wallet setup
Your agent needs:
- An EVM wallet on Base Network — EOA (private key) or smart contract wallet (ERC-1271)
- USDC balance — at least 1× the subscription amount per billing cycle. Keep 2–3× as buffer.
- ETH for gas — only needed for the initial
createSubscription()call. The keeper pays gas for all subsequentexecutePull()calls.
Vault funding model. The agent's vault is funded with exactly 1× the subscription amount per billing cycle. The vault is not a pooled balance — each subscription cycle requires the agent to maintain sufficient USDC in the vault address. AuthOnce's grace period gives the agent time to top up if a payment fails.
Creating a subscription
Call createSubscription() on the SubscriptionVault contract. The key parameters for agent integrations:
| Parameter | Type | Notes |
|---|---|---|
| merchant | address | The merchant's registered wallet address |
| safeVault | address | Must equal msg.sender. The agent's own address. |
| token | address | USDC on Base: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| amount | uint256 | Amount in token decimals (USDC has 6 decimals) |
| interval | uint8 | 0 = Weekly, 1 = Monthly, 2 = Yearly |
| guardian | address | Human operator address. Can pause or cancel. Set to address(0) for no guardian. |
| gracePeriodDays_ | uint256 | 1–30. Days before expiry if payment fails. |
| dataVaultId_ | bytes32 | Set to bytes32(0) — reserved for DataOnce phase. |
Spending limits
AuthOnce's vault architecture enforces spending limits at the protocol level:
- The vault holds exactly 1× the subscription amount per cycle — no more can be pulled
- The subscription amount is immutable after creation — the merchant cannot raise it unilaterally
- Price changes require a 30-day on-chain notice via
setProductExpiry()— the agent can cancel before the new price takes effect
This means the maximum the protocol can ever charge the agent in a single cycle is the amount encoded at subscription creation. This is a hard on-chain guarantee, not a policy.
Guardian control
The guardian field gives a human operator override capability over the agent's subscriptions:
| Action | Who can trigger |
|---|---|
| Cancel subscription | Agent wallet OR guardian |
| Pause subscription | Guardian only |
| Resume subscription | Guardian only |
| Change vault address | Agent wallet only (must equal msg.sender) |
Set the guardian to your human operator's address when creating the subscription. This gives you a kill switch if the agent behaves unexpectedly.
ethers.js example
Full integration for an EOA agent wallet:
import { ethers } from "ethers";
const VAULT_ADDRESS = "0x..."; // SubscriptionVault on Base Mainnet
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const MERCHANT_ADDR = "0x..."; // Merchant registered address
const GUARDIAN_ADDR = "0x..."; // Your human operator wallet
const provider = new ethers.JsonRpcProvider("https://mainnet.base.org");
const agentWallet = new ethers.Wallet(process.env.AGENT_PRIVATE_KEY, provider);
const VAULT_ABI = [
"function createSubscription(address merchant, address safeVault, address token, uint256 amount, uint256 introAmount, uint256 introPulls, uint8 interval, address guardian, uint256 trialDays, uint256 gracePeriodDays_, bytes32 dataVaultId_) returns (uint256)",
];
const ERC20_ABI = ["function approve(address spender, uint256 amount) returns (bool)"];
const vault = new ethers.Contract(VAULT_ADDRESS, VAULT_ABI, agentWallet);
const usdc = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, agentWallet);
// 1. Approve vault to pull USDC
const MONTHLY_AMOUNT = ethers.parseUnits("49.00", 6); // $49.00 USDC
await (await usdc.approve(VAULT_ADDRESS, MONTHLY_AMOUNT)).wait();
// 2. Create subscription
const tx = await vault.createSubscription(
MERCHANT_ADDR,
agentWallet.address, // safeVault == msg.sender
USDC_ADDRESS,
MONTHLY_AMOUNT,
0n, // introAmount
0n, // introPulls
1, // Monthly
GUARDIAN_ADDR,
0n, // trialDays
7n, // gracePeriodDays
ethers.ZeroHash // dataVaultId
);
const receipt = await tx.wait();
console.log("Subscription created. Tx:", receipt.hash);
// From here, the keeper handles all future pulls automatically.
viem example
import { createWalletClient, http, parseUnits, zeroHash } from "viem";
import { base } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY);
const client = createWalletClient({ account, chain: base, transport: http() });
const MONTHLY_AMOUNT = parseUnits("49.00", 6);
// 1. Approve
await client.writeContract({
address: USDC_ADDRESS,
abi: [{ name: "approve", type: "function", inputs: [{ name: "spender", type: "address"}, { name: "amount", type: "uint256"}], outputs: [{ type: "bool"}] }],
functionName: "approve",
args: [VAULT_ADDRESS, MONTHLY_AMOUNT],
});
// 2. Create subscription
await client.writeContract({
address: VAULT_ADDRESS,
abi: VAULT_ABI,
functionName: "createSubscription",
args: [MERCHANT_ADDR, account.address, USDC_ADDRESS, MONTHLY_AMOUNT, 0n, 0n, 1, GUARDIAN_ADDR, 0n, 7n, zeroHash()],
});
ERC-1271 verification (smart contract agents)
If your agent is a smart contract wallet (Safe, EIP-4337 account, custom), the contract must implement:
// Solidity — implement in your agent contract
bytes4 constant MAGIC_VALUE = 0x1626ba7e;
function isValidSignature(
bytes32 hash,
bytes memory signature
) external view returns (bytes4) {
// Validate the signature against your agent's signing key
// Return MAGIC_VALUE if valid, 0xffffffff if not
address signer = recoverSigner(hash, signature);
if (authorisedSigners[signer]) {
return MAGIC_VALUE;
}
return 0xffffffff;
}
AuthOnce's executePull() calls isValidSignature() on the subscriber vault if it detects a contract at that address. Return 0x1626ba7e to approve, anything else to reject.
Contract addresses
| Contract | Network | Address |
|---|---|---|
| SubscriptionVault v7 | Base Sepolia (testnet) | 0xeb068B47731261F7B4A5ae8535686D67D7f72321 |
| MerchantRegistry v4 | Base Sepolia (testnet) | 0xAE681E431c353f5930dDFfBC74037d3f2afE3264 |
| SubscriptionVault | Base Mainnet | Not yet deployed |
| USDC | Base Mainnet | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| USDC | Base Sepolia | 0x036CbD53842c5426634e7929541eC2318f3dCF7e |
Key ABI methods
// Core methods you need
[
"function createSubscription(address merchant, address safeVault, address token, uint256 amount, uint256 introAmount, uint256 introPulls, uint8 interval, address guardian, uint256 trialDays, uint256 gracePeriodDays_, bytes32 dataVaultId_) returns (uint256 id)",
"function cancelSubscription(uint256 id)",
"function pauseSubscription(uint256 id, string reason)",
"function resumeSubscription(uint256 id)",
"function nextPullDue(uint256 id) view returns (uint256)",
"function getSubscription(uint256 id) view returns (tuple(address owner, address merchant, address safeVault, address token, uint256 amount, uint8 interval, uint8 status, uint256 lastPulledAt, uint256 gracePeriodDays, address guardian))",
// Events
"event SubscriptionCreated(uint256 indexed id, address indexed owner, address indexed merchant, address safeVault, address token, uint256 amount, uint256 introAmount, uint256 introPulls, uint8 interval, address guardian, uint256 trialEndsAt, uint256 gracePeriodDays, bool isContractVault)",
"event PaymentExecuted(uint256 indexed id, address indexed merchant, address indexed token, uint256 amount, uint256 merchantReceived, uint256 protocolFee)",
"event SubscriptionCancelled(uint256 indexed id, address indexed cancelledBy)"
]
Full ABI available on Basescan. The contracts are verified. Search for the SubscriptionVault address on sepolia.basescan.org to read the full ABI and source code.
Building an AI agent on Base?
Get in touch — we're actively supporting early AI agent integrations and can assist with custom configurations.
Contact us → Web3 guide