Skip to main content

Gas sponsorship

Every transaction on HPP costs gas in ETH. With sponsorship, your users do not need ETH: HPP's paymaster covers the gas for the operations your policy allows, and — if you choose — charges the gas in USDC.e once the free ones are used up.

This is the mechanism HPP Smart Wallet uses for its "first operations free, then USDC.e" model.

Early access

Sponsorship on HPP Mainnet is set up per app, by application, with limits agreed with you. There is no self-serve dashboard yet and no uptime guarantee. Develop on HPP Sepolia first.

Policies and anchors​

Two IDs decide whether an operation is sponsored:

What it isWhere it comes from
policyIdYour app's rules: fee mode, monthly budget, limits per user, per day, and per operation, start and end datesIssued by HPP for your app
anchorIdOne user of your app. Limits count per anchor, so a user cannot get more free operations by creating more addressesRegistered with HPP for each user — how this works for your app is agreed during onboarding
const user = await createHppAccount({
owner, chain, bundlerUrl,
paymaster: { policyId: "<your policy>", anchorId: "<this user's anchor>" },
});

The paymaster shares the bundler endpoint (/rpc) and speaks the standard ERC-7677 paymaster API, with { policyId, anchorId } as the context. Sponsorship is available for EntryPoint v0.7 (the SDK default).

Keep anchors private

An anchor ID is all the paymaster needs to count an operation against that user's quota. Treat anchor IDs like session tokens: do not derive them from public data (usernames, addresses, GitHub IDs), do not expose other users' anchors, and prefer sending sponsored requests from your backend — or through a backend proxy — rather than embedding anchors in a public frontend.

Fee modes​

A policy has one of three modes:

ModeWho pays the gasWhat the user needs
sponsoredHPPNothing
tokenThe user, in USDC.e, charged after the operation runsA USDC.e balance
sponsored_then_tokenHPP for the first N operations per user, then the user in USDC.eUSDC.e once the free ones are used

The SDK switches automatically: the same sendCalls goes through the free paymaster while free operations remain, and through the USDC.e paymaster afterwards. Each result tells you which:

const r = await user.sendCalls([...]);
r.fee; // { mode: "sponsored", freeRemaining: 2 }
// { mode: "token", symbol: "USDC.e", decimals: 6, maxToken, charged, … } ← charged = actual USDC.e taken

What a user pays in USDC.e​

The user pays the operation's actual gas cost, converted to USDC.e at HPP's current ETH/USD rate plus a 5% markup. The rate is updated periodically from public market prices. Before signing, the user approves a maximum (never less than 0.001 USDC.e); the actual charge is usually lower.

Show the maximum before the user signs:

import { formatUnits } from "viem";

const q = await user.quoteFee(calls);
if (q.mode === "token" && q.maxToken !== null) {
ui.show(`up to ${formatUnits(q.maxToken, q.decimals)} ${q.symbol} (actual cost is charged)`);
}

Users need no separate "approve" step. When an operation will be charged in USDC.e and the allowance is not enough, sendCalls adds a small approve for the paymaster to that same operation — one signature in total.

Apply​

Open a Gas sponsorship request issue on GitHub. The issue is public, so include only what you are happy to share:

  • your app's name, website, and what users do with it;
  • the network — Sepolia (sandbox, for development) or Mainnet;
  • the fee mode you want (sponsored, token, or sponsored_then_token with how many free operations);
  • the contracts and functions your users will call;
  • expected users and operations per month;
  • how users sign in (embedded wallet, browser wallet, or server keys).

We reply on the issue and send the policyId and anchor details to you privately — never post them in the issue.

For development you get a sandbox policy (sponsored, free) with small monthly and daily caps. Use it for your own testing; ask for a policy of your own before you onboard real users.

When a request is refused​

If an operation does not fit the policy, the paymaster refuses it before anything is sent, so nobody pays gas. Read the reason with the SDK's helper:

import { paymasterReason } from "@hpp-io/aa-sdk";

try {
await user.sendCalls(calls);
} catch (e) {
const reason = paymasterReason(e); // e.g. "fee_balance_insufficient", or null if it was not the paymaster
}

The SDK retries the transient ones (free_exhausted, fee_quote_stale, fee_allowance_missing) by itself. The most common of the rest:

reasonMeaning
fee_balance_insufficientNot enough USDC.e for the fee
anchor_budget_exceeded / anchor_daily_count_exceededThis user's monthly budget or daily count is used up
budget_exceeded / count_exceededThe policy's overall budget or count is used up
would_revertThe calls would fail on-chain
anchor_unknownThe anchor is not registered for this policy

When a limit is reached the operation fails by default; your app can retry without the paymaster so the user pays their own gas. Every reason, with fixes: Errors & troubleshooting.

Paymaster approvals are short-lived (5 minutes by default) and cover the operation exactly as it was when approved: sign and send right away, and do not change any field afterwards.