> ## Documentation Index
> Fetch the complete documentation index at: https://docs.startale.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Secure your paymaster

> A paymaster URL in client code is public. Proxy your credentials, scope what you sponsor, and cap what a single user can spend.

The paymaster you configure through `paymasterOptions` lives in your application's client code. Anyone using your application can read it, and so can anyone who never intends to use your application at all.

Every ERC-7677 integration shares this. The consequence is that an unscoped paymaster will pay for whatever it is asked to pay for, by whoever asks, until it is empty.

Startale forwards paymaster requests and nothing more. It applies no policy on your behalf, has no visibility into your sponsorship decisions, and cannot recover funds a drained paymaster has spent.

<Warning>
  Do not fund a paymaster before it has a policy. The window between "it works" and "it has rules" is the window in which it gets drained.
</Warning>

## Keep the provider credential server-side

Hosted gas managers authenticate with an API key, usually embedded in the URL. Putting that URL straight into `paymasterOptions` publishes the key, and a key that can sign sponsorships can spend your balance from outside your application entirely.

Run a thin endpoint of your own and point `paymasterOptions` at that instead. Your endpoint holds the credential, applies your rules, and forwards to the provider.

```ts theme={null}
// POST /api/paymaster
// Your endpoint is what the Startale App calls. Your credential never leaves it.
export async function POST(request: Request) {
  const rpc = await request.json()

  if (rpc.method !== 'pm_getPaymasterStubData' && rpc.method !== 'pm_getPaymasterData') {
    return jsonRpcError(rpc.id, -32601, 'Method not supported')
  }

  const [userOp, entryPoint, chainId, context] = rpc.params

  // Stub requests carry a placeholder operation, not the user's, so let them through.
  // Apply your policy to the operation you are asked to sign.
  if (rpc.method === 'pm_getPaymasterData' && !isSponsorable(userOp)) {
    return jsonRpcError(rpc.id, -32602, 'Operation not eligible for sponsorship')
  }

  // Add the context your provider needs (for example a policy id) to the
  // context the Startale App sent, without dropping its fields.
  const params = [userOp, entryPoint, chainId, { ...context, policyId: process.env.PAYMASTER_POLICY_ID }]

  const upstream = await fetch(process.env.PAYMASTER_URL!, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ...rpc, params }),
  })

  return new Response(await upstream.text(), {
    headers: { 'content-type': 'application/json' },
  })
}
```

Hiding the key is the obvious benefit. The one that matters more during an incident is that you can express rules your provider cannot, and you can stop sponsoring by changing a single deployment instead of waiting on a dashboard or a support ticket.

Note that the Startale App forwards no headers to your endpoint, so it cannot present a shared secret. Treat the endpoint as publicly reachable and let the policy, not the authentication, be what protects it.

## Scope what you sponsor

`isSponsorable` in the sketch above is where the real protection lives. Decide it from the UserOperation itself, inside `pm_getPaymasterData`, before you sign anything. Do not apply it to `pm_getPaymasterStubData`: the Startale App requests the stub with a placeholder operation (zero-address `sender`, empty `callData`), so any sender or contract rule would reject it and the transaction would fail. See [the stub request](/app-sdk/custom-paymaster#pm_getpaymasterstubdata).

| Scope on | Why it matters |
| - | - |
| Target contract | Sponsor calls to your own contracts only. An address allowlist is the highest-value rule and the cheapest to write. |
| Method selector | Sponsor the specific functions your product calls. The first four bytes of `callData` are enough to check. |
| Per-operation gas ceiling | Reject any single operation whose declared fees far exceed what your flows legitimately need. This bounds the damage of one abusive operation. |
| Per-sender rate and spend | Cap operations and cumulative gas per account over a rolling window, above real usage and below the point where one account becomes expensive. |
| Your own user records | If sponsorship is a benefit of holding an account, resolve `sender` against your database and decline addresses you do not recognise. |

An allowlist on contract and method also rejects, by construction, operations that do nothing useful. An account can be made to submit an operation that performs no meaningful work while still burning real gas at a high fee, and a paymaster that sponsors anything will pay for every one of them. Scoping to your own methods removes that whole class of abuse without having to detect it.

<Note>
  Check what your provider's policy layer can actually express before relying on it. Hosted controls usually cover spending limits and per-account allowlists, but scoping by contract address or method selector is frequently missing, and some providers offer it only through a webhook back into your own service. Where the provider cannot express a rule, implement it in your proxy.
</Note>

## Cap the blast radius

Set your numbers on the assumption that everything above will eventually be bypassed by something you did not anticipate.

Fund incrementally. A paymaster holding a week of expected spend has a bounded worst case; one holding a year of spend does not. Alert on burn rate rather than balance, since a drain shows up as an unusual rate long before the balance runs out. Keep a kill switch you can reach in minutes, which with your own proxy is a flag that makes `isSponsorable` return false, and make sure someone other than you knows how to flip it.

Reconcile as well. Log every signature you issue against the resulting UserOperation hash and compare that against the gas your paymaster actually paid. A gap between the two is how you find out something is being sponsored that you did not intend.

## Fail honestly

When you decline to sponsor, return a JSON-RPC error rather than a malformed result, so the transaction fails cleanly.

Your error message does not reach your application. The Startale App reports the failure to your `wallet_sendCalls` request as `-32603` with a short category such as `paymaster_error`, not your paymaster's own text. Your interface can show a generic "not sponsored" state, but not your specific reason. See [Failure modes](/app-sdk/gasless#failure-modes).

Design that declined path as deliberately as the happy path. A user told "this action is not sponsored, and your account needs ETH to continue" can act on it. A user who sees an unexplained failure files a support ticket, or leaves.

## Related

<CardGroup cols={2}>
  <Card title="Paymaster wire format" icon="plug" href="/app-sdk/custom-paymaster">
    The methods your proxy has to implement, and the limits applied when forwarding to it.
  </Card>

  <Card title="Configure paymasterOptions" icon="bolt" href="/app-sdk/gasless">
    Where the URL goes, and why only batched calls carry it.
  </Card>

  <Card title="Who pays for gas" icon="gas-pump" href="/concepts/gasless">
    The division of responsibility between Startale and your application.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/app-sdk/errors">
    Surfacing a declined sponsorship to the user.
  </Card>
</CardGroup>
