Kevo Docs

Gas Sponsorship

Pay gas on behalf of your users. Kevo bundles the user's signed UserOperation through ERC-4337 v0.8 EntryPoint and bills your team for the consumed gas.

How it works

Sponsorship runs on a deposit-based model — no paymaster contract required. For each user, the sponsor wallet deposits ETH into the EntryPoint on the user's behalf, then submits the UserOperation. The EntryPoint debits the deposit during validation; refunds go back to the same deposit.

  1. SDK calls /sponsor-tx/quote — server builds the UserOp.
  2. User signs the UserOp hash through the embedded wallet flow (and a 7702 authorization on first use).
  3. SDK calls /sponsor-tx/submit — server deposits if needed, then broadcasts handleOps.

Enable in your project

Sponsorship requires smartAccounts.enabled = true and gasSponsorship.enabled = true on the project's features. Optional policy fields cap exposure per user/project.

json
{
  "smartAccounts": { "enabled": true, "delegateImpl": "simple7702" },
  "gasSponsorship": {
    "enabled": true,
    "allowedChainIds": [11155111],
    "allowedContracts": ["0xabc..."],
    "maxWeiPerProjectMonth": "1000000000000000000",
    "maxWeiPerUserMonth":    "100000000000000000",
    "maxTxPerUserPerHour":   20
  }
}
When the project's monthly budget is exhausted, /sponsor-tx/submit returns 429 and the SDK falls back to user-paid execution.

SDK usage

Sponsorship is fully transparent. Just call sendTransaction — if sponsorship is enabled and the chain is allowed, the SDK takes the sponsored path automatically; otherwise it sends a normal user-paid transaction.

typescript
import { useKevo } from '@kevo-ws/sdk/react'

function Pay() {
  const { client } = useKevo()

  const handle = async () => {
    const txHash = await client.sendTransaction({
      chainId: 11155111,
      to: '0xRecipient...',
      value: '0x0',
      data: '0xa9059cbb...', // encoded transfer()
    })
    console.log('tx:', txHash)
  }

  return <button onClick={handle}>Pay (gasless)</button>
}
The tryFallback for non-sponsored chains is automatic. You write the same code regardless of whether the user has funds.

HTTP API

POST/v1/wallets/me/sponsor-tx/quoteBearer (user)

Build the UserOperation and return its hash plus all fields needed for client-side signing.

Request

ts
{
  chainId: number
  to: string         // 0x address
  data?: string      // 0x bytes
  value?: string     // 0x or decimal
}

Response 200

ts
{
  supported: true
  chainId: number
  userOpHash: string
  userOpFields: {
    sender: string; nonce: string; initCode: string; callData: string;
    accountGasLimits: string; preVerificationGas: string;
    gasFees: string; paymasterAndData: string;
  }
  domainSeparator: string
  initCodeHashOverride?: string
  needsAuth: boolean
  authFields: { chainId: number; address: string; nonce: number } | null
  delegateAddress: string
  sponsorAddress: string
  gasEstimateWei: string
  userAddress: string
}
POST/v1/wallets/me/sponsor-tx/submitBearer (user)

Submit the signed UserOp and (on first use) the 7702 authorization. Server deposits ETH to EntryPoint if needed and broadcasts handleOps.

Request

ts
{
  chainId: number
  to: string
  data?: string
  value?: string
  userOpSignature: string      // 0x.. (65-byte ECDSA from the embedded wallet)
  userOpFields: { /* exact object from /quote */ }
  signedAuth?: {               // only when needsAuth=true
    chainId: number
    address: string
    nonce: number
    yParity: 0 | 1
    r: string
    s: string
  }
}

Response 200

ts
{
  txHash: string
  sponsorAddress: string
  estimatedGasWei: string
}

Error codes

ts
400 { code: 'chain_not_7702_compatible' }   // SDK falls back to user-paid
403 { error: 'Sponsorship not enabled' | '... not in allowlist' }
429 { error: 'Project monthly gas sponsorship budget exhausted' }
502 { code: 'broadcast_failed' }

Webhooks

Successful sponsored transactions emit a signing.completed event with type: 'sponsored_evm_tx' in the payload.

json
{
  "event": "signing.completed",
  "data": {
    "userId": "uuid",
    "type": "sponsored_evm_tx",
    "chainId": 11155111,
    "txHash": "0x...",
    "sponsorAddress": "0x..."
  },
  "timestamp": 1750000000000
}

Operator setup

The API process needs sponsor keys and RPC endpoints per chain:

bash
# packages/api/.env
SPONSOR_KEYS={"11155111":"0x<sponsor_priv_key>"}
SPONSOR_RPCS={"11155111":"https://sepolia.example/v2/<key>"}

# Optional overrides (defaults shown)
SIMPLE7702_DELEGATE_ADDRESS=0xe6Cae83BdE06E4c305530e199D7217f42808555B
ENTRYPOINT_V08_ADDRESS=0x4337084d9e255ff0702461cf8895ce9e3b5ff108
The sponsor wallet must hold native token on each enabled chain. Top it up before launching — empty sponsor wallet means every submit returns 502. For production, swap the env-based key for a KMS/HSM signer.