@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
npm install @paygate/client-ethereum viem
# or
pnpm add @paygate/client-ethereum viemHow 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 → merchantEthereumX402Client
import { EthereumX402Client } from '@paygate/client-ethereum'
const client = new EthereumX402Client(config: EthereumX402ClientConfig)EthereumX402ClientConfig
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
| Method | Returns | Description |
|---|---|---|
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.
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.
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:
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):
{
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:
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
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
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:
| Network | Chain ID | USDC Contract |
|---|---|---|
| Base Sepolia | 84532 | 0x036CbD53842c5426634e7929541eC2318f3dCF7e |
| Base Mainnet | 8453 | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| Polygon | 137 | 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 |