Permit2 / EIP-2612 — One-Click Subscribe
Subscribe to a plan in a single transaction using EIP-2612 gasless approval. No separate approve() transaction needed.
The problem
Standard ERC-20 subscriptions require two transactions: first approve() to grant the registry an allowance, then subscribe(). This is two wallet popups and two gas payments — a poor UX.
The solution: EIP-2612 permit
Tokens that support EIP-2612 (USDC on Base, DAI, WETH on some chains) allow off-chain signature-based approvals. CronRegistry's subscribeWithPermit() function batches the approval and subscription into a single on-chain transaction. The user signs one message (off-chain) and submits one transaction.
Flow
- User signs an EIP-712 permit message off-chain (no gas)
- Frontend submits
subscribeWithPermit()with the permit signature - Contract calls
permit()to set allowance, then immediately charges and subscribes
SDK usage
import { CronClient } from "@cron/sdk";
const client = new CronClient({
chain: "base",
walletClient,
publicClient,
});
// Read current nonce from the token contract
const tokenNonce = await publicClient.readContract({
address: USDC_BASE,
abi: [{ name: "nonces", type: "function", inputs: [{ name: "owner", type: "address" }], outputs: [{ type: "uint256" }], stateMutability: "view" }],
functionName: "nonces",
args: [userAddress],
});
// One call — signs permit off-chain, submits single tx
const { transactionHash, subscription, permit } = await client.subscribeWithPermit(
1n, // planId
"USD Coin", // token name for EIP-712 domain
tokenNonce, // current nonce from token contract
{
mintNFT: true,
tokenVersion: "2", // USDC on Base uses version "2"
// deadline defaults to 1 hour from now
// permitAmount defaults to 12× the plan charge amount
}
);
console.log("Subscribed in one tx!", transactionHash);Low-level signPermit
You can also sign a permit manually for more control:
import { signPermit, recommendedPermitAmount } from "@cron/sdk";
const permit = await signPermit({
walletClient,
tokenAddress: USDC_BASE,
tokenName: "USD Coin",
tokenVersion: "2",
chainId: 8453,
spender: CRON_REGISTRY_ADDRESS,
value: recommendedPermitAmount(planAmount), // 12× plan amount
nonce: tokenNonce,
deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
});
// permit.deadline, permit.v, permit.r, permit.sToken compatibility
- USDC on Base — EIP-2612 supported, use
tokenVersion: "2" - DAI — EIP-2612 supported, use
tokenVersion: "1" - USDT — Does not support EIP-2612, use standard
subscribe()with prior approval
[!] Permit replay protection
Graceful fallback
The subscribeWithPermit() contract function wraps the permit() call in a try/catch. If the permit fails (e.g. already consumed or unsupported token), the function falls through and attempts the charge using any existing allowance. This means you can safely call it even if the user already has an approval in place.