Transaction Fee Sponsorship
With ABC Paymaster, you can sponsor transaction fees for your users without changing their existing EOA wallet address.
Supported Networks
Currently, the supported networks for gas sponsorship are Ethereum, Kaia, and Base.
For testnets, Ethereum Sepolia, Kaia Kairos, and Base Sepolia are supported.
Avalanche C-Chain is not supported with this approach. Please refer to the SmartAccount guide.
API Guide
The overall flow consists of 3 steps.
| Step | Method | Endpoint |
|---|---|---|
| 1. Sponsor | POST | /core/evm/v2/eoa/paymaster/sponsor |
| 2. Send | POST | /core/evm/v2/eoa/paymaster/send |
| 3. Receipt | GET | /core/evm/v2/eoa/paymaster/receipt |
1. Sponsor { #1-sponsor }
Send the gas sponsorship request and receive the data to sign.
Request
POST /core/evm/v2/eoa/paymaster/sponsor
{
"network": "ethereum_sepolia",
"from": "0xAbC...",
"to": "0xDeF...",
"value": "0x0",
"data": "0x..."
}
| Field | Description |
|---|---|
network | Target network identifier (e.g., ethereum_sepolia, kaia_kairos, base_sepolia) |
from | Sender EOA address |
to | Recipient address (contract or EOA) |
value | Amount of ETH to transfer (wei, hex) |
data | Contract call data. Use "0x" for simple transfers |
Response
sponsored_transaction is an ERC-4337 v0.7 UserOperation payload (split format). The client must pass it through to the send step without modifying any field.
{
"sponsored_transaction": {
"sender": "0xAbC...",
"nonce": "0x1",
"callData": "0x...",
"callGasLimit": "0x...",
"verificationGasLimit": "0x...",
"preVerificationGas": "0x...",
"maxFeePerGas": "0x...",
"maxPriorityFeePerGas": "0x...",
"paymaster": "0xabc7777777641b746429c25961fe99ea48797cbe",
"paymasterVerificationGasLimit": "0x...",
"paymasterPostOpGasLimit": "0x0",
"paymasterData": "0x...",
"signature": "0x"
},
"sign_hash": "0xabcdef...",
"authorization": {
"chainId": 11155111,
"address": "0x...",
"nonce": 0
},
"authorization_hash": "0x..."
}
| Field | Description |
|---|---|
sponsored_transaction | v0.7 UserOperation payload to forward as-is in Step 2 Send |
sign_hash | UserOperation hash the EOA must sign |
authorization | Included only on the first transaction. See below |
authorization_hash | Included only on the first transaction. Hash to sign for authorization. See below |
The paymaster reads the EOA's on-chain code and detects whether it has already been delegated to the SimpleAccount v0.7 implementation (EIP-7702 0xef0100… prefix).
- Not yet delegated: response includes
authorizationandauthorization_hash. The client must sign the EIP-7702 authorization and pass the signed result to/send. - Already delegated:
authorizationandauthorization_hashare both omitted. The client signs onlysign_hashand calls/sendwithout theauthorizationbody field.
2. Send { #2-send }
Submit the signed data.
Request
POST /core/evm/v2/eoa/paymaster/send
{
"network": "ethereum_sepolia",
"sponsored_transaction": { "..." },
"signature": "0x...",
"authorization": {
"chainId": 11155111,
"address": "0x...",
"nonce": 0,
"v": 0,
"r": "0x...",
"s": "0x..."
}
}
| Field | Description |
|---|---|
sponsored_transaction | Pass the sponsored_transaction from Step 1 response as-is |
signature | Signature over sign_hash. See below |
authorization | Required only when the sponsor response included authorization. For an already-delegated EOA, omit this field — sending only sponsored_transaction and signature is sufficient. See below |
Modifying any field in sponsored_transaction will cause signature verification to fail. Pass it exactly as received.
Response
{
"tx_id": "0x..."
}
3. Receipt { #3-receipt }
Query the transaction result using tx_id.
Request
GET /core/evm/v2/eoa/paymaster/receipt?tx_id={tx_id}&network={network}
Response
When complete:
{
"success": true,
"sender": "0xAbC...",
"receipt": {
"transactionHash": "0x...",
"blockNumber": "0x...",
"status": "0x1"
}
}
While processing (not yet indexed):
null
If the Transaction is still in the mempool or receipt indexing is in progress, the response is null. Poll every 1–5 seconds until receipt.transactionHash is populated.
Signing { #signing }
Both sign_hash and authorization_hash are signed using the WaaS MPC Sign API.
POST /v3/wallet/sign
{
"curve": "secp256k1",
"encrypted_share": "Encrypted MPC share from wallet generation/recovery",
"key_id": "Unique identifier of the signing key",
"message": "Hash to sign (hex string)",
"secret_store": "Key used to decrypt the share"
}
The two hashes differ in how they are processed before signing.
| Hash | EIP-191 prefix | Value passed to MPC message |
|---|---|---|
sign_hash | Applied (personal_sign) | The 32-byte hash with the EIP-191 prefix applied |
authorization_hash | Not applied | authorization_hash as-is |
Signing sign_hash
sign_hash must pass SimpleAccount v0.7's toEthSignedMessageHash().recover() verification, so compute the Ethereum personal_sign prefixed hash before passing it to /v3/wallet/sign.
import { keccak256, toBytes, concat } from 'viem';
// Apply personal_sign prefix
const prefix = toBytes(`\x19Ethereum Signed Message:\n32`);
const messageHash = keccak256(concat([prefix, toBytes(sponsorResponse.sign_hash)]));
const signRes = await fetch('/v3/wallet/sign', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` },
body: JSON.stringify({
curve: 'secp256k1',
encrypted_share: encryptedShare,
key_id: keyId,
message: messageHash,
secret_store: secretStore,
}),
});
const { signature } = await signRes.json();
const sendSignature = `0x${signature}`;
Signing authorization_hash (first transaction only) { #authorization }
authorization and authorization_hash are included in the Sponsor response only when the EOA is sending a sponsored transaction for the first time. On subsequent transactions, both fields are omitted, and you only need to sign sign_hash and call /send.
Pass authorization_hash as message (no EIP-191 prefix), then split the 65-byte signature (130 hex chars) into r, s, v to populate the authorization object.
const authSignRes = await fetch('/v3/wallet/sign', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` },
body: JSON.stringify({
curve: 'secp256k1',
encrypted_share: encryptedShare,
key_id: keyId,
message: sponsorResponse.authorization_hash,
secret_store: secretStore,
}),
});
const { signature: authSig } = await authSignRes.json();
// Split 65-byte signature (130 hex chars) into r, s, v
const r = `0x${authSig.slice(0, 64)}`;
const s = `0x${authSig.slice(64, 128)}`;
// Normalize v to EIP-7702 yParity (0 or 1).
// The MPC response is in legacy ECDSA form (27/28), so subtract 27.
let v = parseInt(authSig.slice(128, 130), 16);
if (v >= 27) v -= 27;
// chainId / nonce are reused from the sponsor response (already decimal integers).
const authorization = {
...sponsorResponse.authorization,
v,
r,
s,
};