Subscription Payments with Permissions
Learn how to implement recurring subscription payments using JAW's permission system. This enables services like Netflix, Spotify, or any SaaS to charge users automatically without requiring approval for each payment.
The Problem with Traditional Crypto Payments
In traditional crypto payments, users must approve every single transaction. This creates a poor user experience for recurring payments:
- User subscribes to Service
- Every month, Service sends a payment request
- User must open their wallet and approve
- If user misses the approval, subscription lapses
This friction makes crypto unsuitable for subscription businesses.
How Permissions Solve This
JAW's permission system (based on ERC-7715) allows users to grant time-limited, amount-capped permissions to service providers. The key insight is that permissions are scoped - they can only be used for specific actions you define.
Flow:- User Subscribes - User grants permission: $10 USDC/month to Service
- Service Stores Permission ID - Service stores permissionId in their database
- Monthly Charge - Service transfers USDC using permissionId (no user interaction needed!)
- User Can Cancel Anytime - User revokes permission, subscription ends immediately
Implementation
Installation
Wagmi:npm install @jaw.id/wagmi wagmi viem @tanstack/react-querynpm install @jaw.id/core viemSetup
import { createConfig, http } from 'wagmi';
import { base } from 'wagmi/chains';
import { jaw } from '@jaw.id/wagmi';
export const config = createConfig({
chains: [base],
connectors: [
jaw({
apiKey: 'YOUR_API_KEY',
appName: 'My Streaming Service',
appLogoUrl: 'https://myservice.com/logo.png',
}),
],
transports: {
[base.id]: http(),
},
});1. Grant Subscription Permission
When a user clicks "Subscribe", request a permission that allows your service to charge them periodically.
import { useGrantPermissions } from '@jaw.id/wagmi';
import { parseUnits, type Address } from 'viem';
const SERVICE_SPENDER: Address = '0x...'; // Your backend wallet
const USDC_ADDRESS: Address = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base
function SubscribeButton({ planPrice }: { planPrice: string }) {
const { mutate: grantPermission, isPending } = useGrantPermissions();
const handleSubscribe = () => {
grantPermission({
expiry: Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60, // 1 year
spender: SERVICE_SPENDER,
permissions: {
spends: [{
token: USDC_ADDRESS,
allowance: parseUnits(planPrice, 6).toString(),
unit: 'month',
multiplier: 1,
}],
calls: [{
target: USDC_ADDRESS,
functionSignature: 'transfer(address,uint256)',
}],
},
}, {
onSuccess: (data) => {
// Store data.permissionId in your backend!
console.log('Permission ID:', data.permissionId);
},
});
};
return (
<button onClick={handleSubscribe} disabled={isPending}>
{isPending ? 'Confirming...' : `Subscribe ${planPrice}/month`}
</button>
);
}2. Execute Charge (Server Side)
Your backend service charges users using their stored permission IDs. This typically runs as a cron job or scheduled task - no user approval needed since the permission authorizes the charge.
import { Account } from '@jaw.id/core';
import { privateKeyToAccount } from 'viem/accounts';
import { encodeFunctionData, parseUnits, type Address } from 'viem';
const USDC_ADDRESS: Address = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base
const SERVICE_TREASURY: Address = '0x...'; // Your treasury address
// Load the spender's private key from environment
const spenderAccount = privateKeyToAccount(process.env.SPENDER_PRIVATE_KEY as `0x${string}`);
async function chargeSubscription(permissionId: string, amount: string) {
// Create account instance from the spender's private key
const account = await Account.fromLocalAccount(
{ chainId: 8453, apiKey: process.env.JAW_API_KEY! },
spenderAccount
);
// Transfer USDC from user to your treasury
const { id } = await account.sendCalls(
[{
to: USDC_ADDRESS,
data: encodeFunctionData({
abi: [{
name: 'transfer',
type: 'function',
inputs: [
{ name: 'to', type: 'address' },
{ name: 'amount', type: 'uint256' }
],
outputs: [{ type: 'bool' }]
}],
functionName: 'transfer',
args: [SERVICE_TREASURY, parseUnits(amount, 6)],
}),
}],
{ permissionId: permissionId as `0x${string}` }
);
return id; // user operation ID
}3. Cancel Subscription
Users can cancel anytime by revoking the permission. Once revoked, the spender can no longer execute charges.
import { useRevokePermissions } from '@jaw.id/wagmi';
function CancelButton({ permissionId }: { permissionId: string }) {
const { mutate: revokePermission, isPending } = useRevokePermissions();
return (
<button
onClick={() => revokePermission({ id: permissionId as `0x${string}` })}
disabled={isPending}
>
{isPending ? 'Cancelling...' : 'Cancel Subscription'}
</button>
);
}Permission Parameters Reference
| Parameter | Purpose | Example |
|---|---|---|
spender | Address authorized to use this permission | Your backend wallet |
expiry | Unix timestamp when permission expires | 1 year from now |
permissions.spends.token | Token address to spend | USDC address |
permissions.spends.allowance | Max amount per period | $10 (10000000 for USDC) |
permissions.spends.unit | Time period | month |
permissions.calls.target | Contract that can be called | USDC contract address |
permissions.calls.functionSignature | Function that can be called | transfer(address,uint256) |
Time Units
| Unit | Description |
|---|---|
minute | Resets every minute |
hour | Resets every hour |
day | Resets every 24 hours |
week | Resets every 7 days |
month | Resets every ~30 days |
year | Resets every 365 days |
Related
- useGrantPermissions - Wagmi hook reference
- useRevokePermissions - Cancel subscriptions
- wallet_grantPermissions - RPC method reference