DEV Community

jimquote
jimquote

Posted on

Building a Gasless Crypto Payment Flow: How ERC-3009 Eliminates User Gas Fees

Gas fees have always been a UX nightmare for crypto payments. Users want to pay with USDC, but they need ETH first. This article explores a practical implementation using PayinGo — a payment service that leverages ERC-3009 signatures to make users' gas fees disappear.


The Technical Challenge

When a user pays with ERC-20 tokens, they must:

  1. Hold the native token (ETH/SOL) for gas
  2. Approve the token transfer
  3. Execute the transfer

This creates a chicken-and-egg problem: users can't spend their stablecoins without first acquiring gas tokens. Most solutions involve wrapping, meta-transactions, or relayers with complex smart contracts.

ERC-3009 offers a cleaner path.


How ERC-3009 Works

ERC-3009 (TransferWithAuthorization) allows token transfers via off-chain signatures. Instead of the user submitting a transaction, they sign an authorization message. Anyone holding that signature can execute the transfer on-chain.

function transferWithAuthorization(
    address from,
    address to,
    uint256 value,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    uint8 v, bytes32 r, bytes32 s
) external;
Enter fullscreen mode Exit fullscreen mode

USDC on major chains (Base, Polygon, Ethereum) supports this natively. No wrapper contracts needed.


Architecture Overview

PayinGo implements this pattern with a facilitator model:

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌──────────────┐
│   Frontend  │────▶│  PayinGo    │────▶│ Facilitator │────▶│ USDC Contract│
│  (User signs│     │    API      │     │ (pays gas)  │     │  (on-chain)  │
│   EIP-712)  │     │ (validates) │     │             │     │              │
└─────────────┘     └─────────────┘     └─────────────┘     └──────────────┘
Enter fullscreen mode Exit fullscreen mode

The user signs an EIP-712 typed message. The facilitator submits it on-chain and pays gas. Funds transfer directly from payer to recipient — the service never touches the money.


Implementation

Creating a Payment Session

curl -X POST https://go.payin.com/api/v1/checkout/create \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "50.00",
    "currency": "USDC",
    "recipientAddress": "0xMerchantWallet",
    "network": "base",
    "metadata": { "orderId": "ORD-123" },
    "callbackUrl": "https://yoursite.com/webhook",
    "callbackSecret": "your-secret"
  }'
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "success": true,
  "payment": {
    "id": "pay_abc123",
    "status": "PENDING",
    "paymentUrl": "https://go.payin.com/checkout/pay_abc123",
    "expiresAt": "2024-01-15T10:30:00Z"
  }
}
Enter fullscreen mode Exit fullscreen mode

Frontend Signing (EVM)

The user's wallet signs an EIP-712 message:

const domain = {
  name: 'USD Coin',
  version: '2',
  chainId: 8453,  // Base
  verifyingContract: USDC_ADDRESS
};

const types = {
  TransferWithAuthorization: [
    { name: 'from', type: 'address' },
    { name: 'to', type: 'address' },
    { name: 'value', type: 'uint256' },
    { name: 'validAfter', type: 'uint256' },
    { name: 'validBefore', type: 'uint256' },
    { name: 'nonce', type: 'bytes32' }
  ]
};

const message = {
  from: payerAddress,
  to: merchantAddress,
  value: parseUnits('50', 6),  // USDC has 6 decimals
  validAfter: 0,
  validBefore: Math.floor(Date.now() / 1000) + 900,  // 15 min
  nonce: randomBytes(32)
};

const signature = await wallet.signTypedData(domain, types, message);
Enter fullscreen mode Exit fullscreen mode

Webhook Handling

app.post('/webhook', (req, res) => {
  const signature = req.headers['x-payingo-signature'];
  const timestamp = req.headers['x-payingo-timestamp'];
  const payload = JSON.stringify(req.body);

  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(`${timestamp}.${payload}`)
    .digest('hex');

  if (signature !== expected) {
    return res.status(401).send('Invalid signature');
  }

  if (req.body.event === 'payment.completed') {
    // Update order status
    db.orders.update(req.body.metadata.orderId, {
      status: 'paid',
      txHash: req.body.txHash
    });
  }

  res.status(200).send('OK');
});
Enter fullscreen mode Exit fullscreen mode

Solana Implementation

Solana doesn't have ERC-3009, so the approach differs. The frontend builds a partial transaction with the user as signer but the facilitator as fee payer:

const transaction = new Transaction()
  .add(ComputeBudgetProgram.setComputeUnitLimit({ units: 100000 }))
  .add(createTransferCheckedInstruction(
    payerTokenAccount,
    USDC_MINT,
    merchantTokenAccount,
    payerPublicKey,
    amount * 1e6,
    6
  ));

transaction.feePayer = FACILITATOR_PUBKEY;
transaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;

// User signs
const signed = await wallet.signTransaction(transaction);
// Send to API, facilitator adds fee payer signature and broadcasts
Enter fullscreen mode Exit fullscreen mode

Network Support

Network Method Settlement
Base ERC-3009 ~2s
Polygon ERC-3009 ~2s
Solana Partial TX signing ~2s

Alternative: Payment Links

For simpler use cases (donations, invoices), there's a no-code option:

curl -X POST https://go.payin.com/api/v1/link/create \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Project Donation",
    "evmAddress": "0xYourWallet",
    "notifyEmail": "you@example.com"
  }'
Enter fullscreen mode Exit fullscreen mode

This returns a shareable URL. Users visit, connect wallet, enter amount, sign, done. No backend integration required.


Trade-offs

What works well:

  • Zero gas for end users
  • Direct P2P settlement (non-custodial)
  • Simple API surface
  • Fast finality (~2 seconds)

Limitations:

  • USDC only (no ETH, BTC, or other tokens)
  • Three networks currently (Base, Polygon, Solana)
  • Dependent on facilitator availability
  • Facilitator economics unclear long-term

When to Use This

This pattern fits well for:

  • E-commerce checkout flows
  • SaaS subscription payments
  • Donations and tips
  • Invoice payments
  • Any scenario where UX matters more than token variety

If you need multi-token support or prefer fully decentralized infrastructure, other approaches (like Account Abstraction with ERC-4337) might be more appropriate.


References


Questions or alternative implementations? Drop a comment below.

Top comments (0)