Skip to content

Integration guide ​

This guide sends an ERC-20 transfer from an EntryPoint v0.7 smart account and reimburses the public Ink paymaster in USDC, USD₮0, or USDG. The paymaster supplies native ETH through EntryPoint; the selected ERC-20 is reimbursement and does not become Ink's protocol gas token.

1. Install the SDK ​

bash
pnpm add @hellomoon/ink-paymaster@^0.2.2 viem

You need an EntryPoint v0.7-compatible Viem SmartAccount. How you create and sign that account depends on your wallet provider. Keep its signer in the same trusted environment you normally use for wallet operations.

2. Choose a reimbursement token ​

The public deployment supports these token keys:

ts
import {INK_PAYMENT_TOKENS} from "@hellomoon/ink-paymaster";

const token = INK_PAYMENT_TOKENS.USDC;
// const token = INK_PAYMENT_TOKENS.USDT0;
// const token = INK_PAYMENT_TOKENS.USDG;

Use SDK metadata instead of copying token or paymaster addresses into application code.

3. Fund the smart account ​

Before the first sponsored operation, the smart account needs:

  • enough of the selected token for the application call and its gas-reimbursement ceiling; and
  • enough ETH for one owner-paid approval UserOperation.

The approval must come from the smart account that owns the tokens. Approving from its owner EOA does not create the required allowance. The public paymaster cannot sponsor its own approval.

4. Create the Ink clients ​

ts
import {createPublicClient, defineChain, http} from "viem";
import {createBundlerClient} from "viem/account-abstraction";
import {
  createInkPublicPaymasterClient,
  createInkViemPaymaster,
  INK_PUBLIC_PAYMASTER,
} from "@hellomoon/ink-paymaster";

// Supply a production-grade Ink RPC from your application's configuration.
const inkRpcUrl = process.env.INK_RPC_URL!;
const ink = defineChain({
  id: INK_PUBLIC_PAYMASTER.chainId,
  name: INK_PUBLIC_PAYMASTER.chainName,
  nativeCurrency: {name: "Ether", symbol: "ETH", decimals: 18},
  rpcUrls: {default: {http: [inkRpcUrl]}},
});

const publicClient = createPublicClient({chain: ink, transport: http(inkRpcUrl)});
const service = createInkPublicPaymasterClient();

service.bundlerUrl points to Alto, the ERC-4337 bundler. Alto submits UserOperations to EntryPoint; the separate paymaster contract supplies their native gas.

5. Check balance and allowance ​

ts
import {
  getPublicPaymasterAccountState,
  INK_PUBLIC_PAYMASTER,
} from "@hellomoon/ink-paymaster";

const maxTokenCost = 100_000n; // 0.1 token for the current six-decimal tokens
const state = await getPublicPaymasterAccountState(
  publicClient,
  account.address,
  INK_PUBLIC_PAYMASTER,
  token.symbol,
);

console.log({
  balance: state.tokenBalance,
  allowance: state.paymasterAllowance,
  refundCredit: state.refundCredit,
});

The account must have enough token balance and allowance to cover maxTokenCost. If the operation also transfers that token, its balance must cover both the transfer and the reimbursement ceiling.

6. Approve once, owner-paid ​

Skip this step when state.paymasterAllowance >= maxTokenCost. Otherwise send a finite approval without attaching the paymaster:

ts
import {encodePublicPaymasterApproval} from "@hellomoon/ink-paymaster";

const ownerPaidClient = createBundlerClient({
  account,
  chain: ink,
  client: publicClient,
  transport: http(service.bundlerUrl),
  // No `paymaster`: the smart account pays ETH for this operation.
});

const approvalHash = await ownerPaidClient.sendUserOperation({
  calls: [{
    to: token.address,
    value: 0n,
    data: encodePublicPaymasterApproval(10_000_000n), // finite 10-token allowance
  }],
});

const approvalReceipt = await ownerPaidClient.waitForUserOperationReceipt({
  hash: approvalHash,
});
if (!approvalReceipt.success) throw new Error("paymaster approval reverted");

Allowances are independent for USDC, USD₮0, and USDG. Repeat this owner-paid step when selecting a token that has not previously approved the paymaster or when its allowance becomes too low.

7. Send a sponsored transfer ​

This example transfers 1.25 units of the selected token. Viem converts calls into the smart account's execution calldata, asks the paymaster for a quote, signs the UserOperation, and sends it through Alto.

ts
import {encodeFunctionData, parseUnits} from "viem";

const erc20Abi = [{
  type: "function",
  name: "transfer",
  stateMutability: "nonpayable",
  inputs: [
    {name: "recipient", type: "address"},
    {name: "amount", type: "uint256"},
  ],
  outputs: [{type: "bool"}],
}] as const;

const bundlerClient = createBundlerClient({
  account,
  chain: ink,
  client: publicClient,
  transport: http(service.bundlerUrl),
  paymaster: createInkViemPaymaster(service),
  paymasterContext: {
    token: token.symbol,
    maxTokenCost,
    idempotencyKey: "transfer-order-123",
  },
});

const userOperationHash = await bundlerClient.sendUserOperation({
  calls: [{
    to: token.address,
    value: 0n,
    data: encodeFunctionData({
      abi: erc20Abi,
      functionName: "transfer",
      args: [recipient, parseUnits("1.25", token.decimals)],
    }),
  }],
});

const receipt = await bundlerClient.waitForUserOperationReceipt({
  hash: userOperationHash,
});
if (!receipt.success) throw new Error("sponsored transfer reverted");

console.log({
  userOperationHash,
  transactionHash: receipt.receipt.transactionHash,
});

Use a stable idempotency key for retries of this exact logical operation. Generate a new key if the sender, nonce, calls, token, or reimbursement ceiling changes.

Complete reusable helper ​

The repository's type-checked application example combines the checks, conditional owner-paid approval, sponsored transfer, and receipt validation. sendSponsoredTokenTransfer is an example helper to copy into your application, not an additional SDK export. Once copied, call it with:

ts
const result = await sendSponsoredTokenTransfer({
  account,
  rpcUrl: process.env.INK_RPC_URL!,
  token: "USDG", // "USDC" | "USDT0" | "USDG"
  recipient,
  amount: "1.25",
  maxTokenCost: 100_000n,
  idempotencyKey: "transfer-order-123",
});

console.log(result.userOperationHash, result.transactionHash);

The complete helper is type-checked with the SDK during documentation CI.

Production checklist ​

  • Never embed raw private keys or authenticated API keys in browser bundles. Use your wallet or custody provider's normal signing flow.
  • Use finite allowances and monitor them before requesting a quote.
  • Persist the business operation ID, idempotency key, UserOperation hash, and transaction hash.
  • Retry transient failures with the same idempotency key and exponential backoff.
  • Treat 429 as quota exhaustion or rate limiting; do not rotate idempotency keys to bypass it.
  • Treat 503 as unavailable pricing, RPC state, or paymaster readiness and stop submissions until service health recovers.
  • Check receipt.success; transaction inclusion does not guarantee the smart-account call succeeded.

See Errors and retries for response-specific handling and Approval and refunds for allowance and refund-credit operations.

ERC-4337 paymaster clients for Ink Mainnet