Skip to main content

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.

Using Avalanche C-Chain?

Avalanche C-Chain is not supported with this approach. Please refer to the SmartAccount guide.

API Guide

The overall flow consists of 3 steps.

StepMethodEndpoint
1. SponsorPOST/core/evm/v2/eoa/paymaster/sponsor
2. SendPOST/core/evm/v2/eoa/paymaster/send
3. ReceiptGET/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..."
}
FieldDescription
networkTarget network identifier (e.g., ethereum_sepolia, kaia_kairos, base_sepolia)
fromSender EOA address
toRecipient address (contract or EOA)
valueAmount of ETH to transfer (wei, hex)
dataContract 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..."
}
FieldDescription
sponsored_transactionv0.7 UserOperation payload to forward as-is in Step 2 Send
sign_hashUserOperation hash the EOA must sign
authorizationIncluded only on the first transaction. See below
authorization_hashIncluded only on the first transaction. Hash to sign for authorization. See below
Delegation auto-detection

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 authorization and authorization_hash. The client must sign the EIP-7702 authorization and pass the signed result to /send.
  • Already delegated: authorization and authorization_hash are both omitted. The client signs only sign_hash and calls /send without the authorization body 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..."
}
}
FieldDescription
sponsored_transactionPass the sponsored_transaction from Step 1 response as-is
signatureSignature over sign_hash. See below
authorizationRequired 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
warning

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.

HashEIP-191 prefixValue passed to MPC message
sign_hashApplied (personal_sign)The 32-byte hash with the EIP-191 prefix applied
authorization_hashNot appliedauthorization_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,
};