Skip to content

@paygate/client-ethereum

x402 client for Ethereum and EVM-compatible chains. Uses viem for signing and implements EIP-3009 (transferWithAuthorization) for gasless USDC payments - the payer never pays gas.

Installation

bash
npm install @paygate/client-ethereum viem
# or
pnpm add @paygate/client-ethereum viem

How EIP-3009 Works

Instead of broadcasting a transaction, the payer signs a structured permit (EIP-712) authorising the facilitator to transfer tokens on their behalf. The facilitator then broadcasts the transferWithAuthorization call and pays gas.

Payer signs EIP-712 permit  (no gas, off-chain)

Sends signed permit to resource server via X-PAYMENT header

Resource server forwards to facilitator

Facilitator calls transferWithAuthorization on-chain  (pays gas)

USDC moves from payer → merchant

EthereumX402Client

typescript
import { EthereumX402Client } from '@paygate/client-ethereum'

const client = new EthereumX402Client(config: EthereumX402ClientConfig)

EthereumX402ClientConfig

typescript
interface EthereumX402ClientConfig {
  walletConnector: EVMWalletConnector  // wallet implementation (see below)
  chainId: number                       // EVM chain ID (e.g. 84532 for Base Sepolia)
  rpcUrl?: string                       // optional custom RPC
  facilitatorUrl?: string               // for discoverResources()
}

Methods

MethodReturnsDescription
fetchWithPayment(url, options?)Promise<Response>Fetch a URL, auto-paying any 402.
getAccount()Promise<{ address: string }>Returns the connected wallet address.
discoverResources(facilitatorUrl?, filters?)Promise<any>Lists resources from the facilitator registry.

Wallet Connectors

InjectedWalletConnector - MetaMask / Browser Wallets

For browser applications. Works with MetaMask, Coinbase Wallet, Rabby, and any window.ethereum provider.

typescript
import { EthereumX402Client, InjectedWalletConnector } from '@paygate/client-ethereum'

if (!InjectedWalletConnector.isAvailable()) {
  throw new Error('Please install MetaMask')
}

const client = new EthereumX402Client({
  walletConnector: new InjectedWalletConnector(),
  chainId: 84532, // Base Sepolia
})

const response = await client.fetchWithPayment('https://api.example.com/premium')

LocalEVMWalletConnector - Private Key / Mnemonic

For server-side Node.js applications, scripts, and AI agents.

typescript
import { EthereumX402Client, LocalEVMWalletConnector } from '@paygate/client-ethereum'

// From private key
const connector = LocalEVMWalletConnector.fromPrivateKey(
  '0xYourPrivateKey',
  84532 // Base Sepolia chain ID
)

// From mnemonic
const connector = LocalEVMWalletConnector.fromMnemonic(
  'word1 word2 ... word12',
  84532
)

const client = new EthereumX402Client({
  walletConnector: connector,
  chainId: 84532,
})

const response = await client.fetchWithPayment('https://api.example.com/premium')
const data = await response.json()

EVMWalletConnector Interface

Implement this to support any other EVM wallet:

typescript
interface EVMWalletConnector {
  getAddress(): Promise<Address>
  getChainId(): Promise<number>
  signTypedData(params: {
    domain: any
    types: any
    primaryType: string
    message: any
  }): Promise<Hex>
  signArbitrary(message: string): Promise<{ signature: any }>
}

EIP-3009 Payment Payload

The @paygate/client-ethereum package constructs the full EIP-3009 authorization internally. Here's what gets sent in the X-PAYMENT header (base64-encoded JSON):

typescript
{
  x402Version: 1,
  paymentRequirements: { ... },  // echo of the 402 requirements
  paymentPayload: {
    x402Version: 1,
    scheme: "exact",
    network: "base-sepolia",
    payload: {
      signature: "0x...",        // EIP-712 signature
      authorization: {
        from: "0xPayerAddress",
        to: "0xMerchantAddress",
        value: "1000000",        // amount in atomic units
        validAfter: "1712345678",
        validBefore: "1712345738",
        nonce: "0xRandom32Bytes"
      }
    }
  }
}

The extra field in PaymentRequirements must contain EVM-specific fields for EIP-712 domain construction:

typescript
extra: {
  chainId: number      // EVM chain ID
  name: string         // token name (e.g. "USD Coin")
  version: string      // token version (e.g. "2")
}

Full Example - React App

tsx
import { useState } from 'react'
import { EthereumX402Client, InjectedWalletConnector } from '@paygate/client-ethereum'

export function PremiumDataButton() {
  const [data, setData] = useState(null)
  const [status, setStatus] = useState('')

  async function fetchPremiumData() {
    const client = new EthereumX402Client({
      walletConnector: new InjectedWalletConnector(),
      chainId: 84532,
    })

    const response = await client.fetchWithPayment(
      'https://api.example.com/premium',
      {
        onPaymentRequired: async (req) => {
          const usdcAmount = (Number(req.maxAmountRequired) / 1e6).toFixed(2)
          setStatus(`Paying $${usdcAmount} USDC...`)
          return true
        },
        onSigningRequired: () => {
          setStatus('Check MetaMask for signature request...')
        },
        onPaymentComplete: (settlement) => {
          setStatus(`Paid! Tx: ${settlement?.transaction?.slice(0, 10)}...`)
        },
      }
    )

    setData(await response.json())
  }

  return (
    <div>
      <button onClick={fetchPremiumData}>Fetch Premium Data</button>
      {status && <p>{status}</p>}
      {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
    </div>
  )
}

Full Example - Node.js Script / AI Agent

typescript
import { EthereumX402Client, LocalEVMWalletConnector } from '@paygate/client-ethereum'

async function main() {
  const client = new EthereumX402Client({
    walletConnector: LocalEVMWalletConnector.fromPrivateKey(
      process.env.PRIVATE_KEY as `0x${string}`,
      84532
    ),
    chainId: 84532,
  })

  const response = await client.fetchWithPayment('https://api.example.com/ai-data', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ prompt: 'Summarise the market today' }),
    onPaymentComplete: (s) => console.log('Settled:', s?.transaction),
  })

  const result = await response.json()
  console.log(result)
}

main()

Supported Networks

Any EVM chain where USDC supports EIP-3009. Tested networks:

NetworkChain IDUSDC Contract
Base Sepolia845320x036CbD53842c5426634e7929541eC2318f3dCF7e
Base Mainnet84530x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Polygon1370x3c499c542cEF5E3811e1192ce70d8cC03d5c3359

Released under the MIT License.