Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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:
  1. User Subscribes - User grants permission: $10 USDC/month to Service
  2. Service Stores Permission ID - Service stores permissionId in their database
  3. Monthly Charge - Service transfers USDC using permissionId (no user interaction needed!)
  4. User Can Cancel Anytime - User revokes permission, subscription ends immediately

Implementation

Installation

Wagmi:
npm
npm install @jaw.id/wagmi wagmi viem @tanstack/react-query
Core SDK:
npm
npm install @jaw.id/core viem

Setup

Wagmi
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.

Wagmi
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.

Wagmi
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

ParameterPurposeExample
spenderAddress authorized to use this permissionYour backend wallet
expiryUnix timestamp when permission expires1 year from now
permissions.spends.tokenToken address to spendUSDC address
permissions.spends.allowanceMax amount per period$10 (10000000 for USDC)
permissions.spends.unitTime periodmonth
permissions.calls.targetContract that can be calledUSDC contract address
permissions.calls.functionSignatureFunction that can be calledtransfer(address,uint256)

Time Units

UnitDescription
minuteResets every minute
hourResets every hour
dayResets every 24 hours
weekResets every 7 days
monthResets every ~30 days
yearResets every 365 days

Related