> For the complete documentation index, see [llms.txt](https://docs.pepeunchained.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.pepeunchained.com/infrastructure/erc7677.md).

# ERC-7677 Methods

The policy service speaks [ERC-7677](https://eips.ethereum.org/EIPS/eip-7677), the paymaster web-service standard. Two JSON-RPC methods, `pm_getPaymasterStubData` and `pm_getPaymasterData`, are what Privy's smart-wallet client and viem's paymaster client call. Read this page to understand how the two differ, what to put in `context`, what the responses look like and how to build the requests with the SDK.

Endpoint `POST /v2/paymaster/<token>`, hosted at `https://paymaster.pepeunchained.com`. The URL always embeds a token, so there is no tokenless default; `PEPU_PAYMASTER_URL` holds the complete URL. The v1 route `POST /paymaster/<appToken>` is retired. Local default: `http://localhost:8788`.

## Endpoints

| Version                                                                                                                                                                                           | Path                                                                               | Credential in the URL                                                                            |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| v1                                                                                                                                                                                                | `POST {PEPU_PAYMASTER_URL}/paymaster/<appToken>` (and `OPTIONS` for CORS), retired | The app's raw API key. Treat the whole URL as a secret                                           |
| v2                                                                                                                                                                                                | `POST https://paymaster.pepeunchained.com/v2/paymaster/<token>` (and `OPTIONS`)    | A project's client token `ppt_...` (browser-safe, origin-bound) or server key `ppk_...` (secret) |
| Privy's dashboard "Paymaster URL" field is a bare URL with no separate header or secret field, which is why the credential travels in the path. See [Credential Matrix](/infrastructure/auth.md). |                                                                                    |                                                                                                  |

A `ppt_` client token works only from the project's allowed origins; with none set, or from another origin, the service answers `project_origin_not_allowed`. If Privy calls from its own servers, use the `ppk_` server key and keep it secret. The free tier needs `context.privyAccessToken` from PEPU's own Privy app; a third-party app's token is ignored for billing, and the dashboard URL cannot attach one, so the free tier is not reachable from the Smart Wallets dashboard. Privy's dashboard "Paymaster URL" field is a bare URL with no separate header or secret field, which is why the credential travels in the path. See [Credential Matrix](/infrastructure/auth.md).

Both routes accept a single request or a JSON-RPC batch array (v2 handles at most 20 per batch). Only these two methods exist; anything else returns `-32601`. `pm_sponsorUserOperation` is not implemented.

## Request shape

Both methods take positional params `[userOperation, entryPoint, chainId, context?]`:

| Param           | Value                                                                   |
| --------------- | ----------------------------------------------------------------------- |
| `userOperation` | Unpacked v0.7 UserOperation                                             |
| `entryPoint`    | `0x0000000071727De22E5E9d8BAf0edAc6f37da032` (v0.7 only)                |
| `chainId`       | `"0x17dcd"` (97741). A decimal number or string is also accepted        |
| `context`       | Optional object. v1 accepts and ignores it. v2 reads `privyAccessToken` |

## Stub versus data semantics

|                        | `pm_getPaymasterStubData`                                                                                    | `pm_getPaymasterData`                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
| When it is called      | Before gas estimation                                                                                        | Right before submission to the bundler                     |
| Purpose                | Fields that let `eth_estimateUserOperationGas` simulate without reverting                                    | The real, signed approval                                  |
| Runs the policy engine | **No.** No cap consumed, no audit row, nothing recorded                                                      | **Yes.** Every cap, allowlist, budget and the signing step |
| Authenticated          | **No, by spec.** The URL credential is not resolved for the stub (v1). Unknown credentials get the same stub | Yes                                                        |
| Signature              | A real ECDSA signature from a throwaway key over a zero hash: well formed, never authorised                  | From the policy signer                                     |
| `isFinal`              | `false`                                                                                                      | not returned                                               |

**Stub data is unauthenticated by design, and that is not a hole.** ERC-7677 defines the stub call purely as a gas-estimation aid and draws the authorisation line at `pm_getPaymasterData`. The stub carries a dummy signature that the contract treats as an unauthorised signer, so using it on chain fails cleanly (`sigFailed`), never pays. The only cost of a bad credential is that you learn about it at the second call instead of the first. In v2 the stub carries the project's sponsor id if the URL token resolves to an active project, otherwise the free pool id, in the real 97-byte layout so `preVerificationGas` estimates match.

### The dummy signature must be well formed

If you estimate without paymaster fields, EntryPoint demands the sender's own prefund and a zero-balance account fails with `AA21 didn't pay prefund`. If you attach a signature-shaped blob that is not a valid ECDSA signature, the contract's recovery reverts instead of failing closed. That is why the service returns a real signature from a disposable key. Use the stub fields exactly as returned.

## pm\_getPaymasterStubData

Request:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "pm_getPaymasterStubData",
  "params": [
    {
      "sender": "0x…",
      "nonce": "0x0",
      "callData": "0x…",
      "callGasLimit": "0x0",
      "verificationGasLimit": "0x0",
      "preVerificationGas": "0x0",
      "maxFeePerGas": "0x0",
      "maxPriorityFeePerGas": "0x0"
    },
    "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
    "0x17dcd"
  ]
}
```

Only `sender` is validated for the stub; gas fields may be placeholders.

Response:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "paymaster": "0xdb28528C9399C33c9A0297E8A62F39145acbC30D",
    "paymasterData": "0x…",
    "paymasterVerificationGasLimit": "0x…",
    "paymasterPostOpGasLimit": "0x…",
    "isFinal": false
  }
}
```

For v1 (retired) the `paymaster` was the v1 contract and `paymasterData` is `mode(1)=0x00 ‖ validUntil(6) ‖ validAfter(6) ‖ signature(65)`. For v2 the `paymaster` is the v2 contract and `paymasterData` is `mode(1)=0x02 ‖ validUntil(6) ‖ validAfter(6) ‖ sponsorId(32) ‖ signature(65)`. You do not need to build these yourself.

## pm\_getPaymasterData

Request: the same shape with the real, estimated gas fields filled in. For v2 add `context` when the free tier may be used:

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "pm_getPaymasterData",
  "params": [
    { "sender": "0x…", "nonce": "0x…", "factory": "0x…", "factoryData": "0x…", "callData": "0x…",
      "callGasLimit": "0x…", "verificationGasLimit": "0x…", "preVerificationGas": "0x…",
      "maxFeePerGas": "0x…", "maxPriorityFeePerGas": "0x…" },
    "0x0000000071727De22E5E9d8BAf0edAc6f37da032",
    "0x17dcd",
    { "privyAccessToken": "<the user's Privy access token>" }
  ]
}
```

Success:

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "paymaster": "0x…",
    "paymasterData": "0x…",
    "paymasterVerificationGasLimit": "0x…",
    "paymasterPostOpGasLimit": "0x…"
  }
}
```

The approval is valid for a short window: 90 seconds on v1 (`VALIDITY_WINDOW_SECONDS`), 60 seconds on v2. A signature that sits unsubmitted past `validUntil` is rejected by EntryPoint. If you hit `-32503` or `AA32`, ask for fresh paymaster data.

### Idempotency: duplicate calls are expected

Privy's smart-wallet client can run its prepare flow twice in parallel, producing two `pm_getPaymasterData` calls for the **same** UserOperation 10 to 300 ms apart. The policy service de-duplicates by **userOpHash**: if a live (unexpired, not voided) approval already exists for that hash, the second call skips the cap checks and the cost booking and returns a fresh signature for the same hash, so the spend is booked once. v2 also treats a re-request for the same (sponsor, sender, nonce) as a replacement of the earlier unexecuted approval, so retries never stack reservations (a smaller re-request keeps the earlier, larger reservation live until its signature expires). Design your own client to be safe to call twice, and do not treat a repeat as a new spend.

## Context fields

| Field              | Version | Meaning                                                                                                                                                                                                                                 |
| ------------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `privyAccessToken` | v2      | The user's verified Privy access token. **Required for the free tier**; without it the free tier is denied with `free_tier_identity_required`. A project's own budget does not need it (unless the project turned on `requireIdentity`) |
| anything else      | both    | Accepted and ignored                                                                                                                                                                                                                    |

`buildPaymasterContext` from the SDK drops `undefined` entries and rejects values that do not survive JSON (bigint, function, symbol).

## Errors

JSON-RPC errors use the standard envelope. Sponsorship denials carry the machine-readable reason in `error.data.reason` (v2 also adds `detail`, the reason per tier tried).

| `error.code` | Meaning                                                                                                                                                                                       |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `-32700`     | Parse error                                                                                                                                                                                   |
| `-32600`     | Invalid request                                                                                                                                                                               |
| `-32601`     | Method not found (only the two methods above exist)                                                                                                                                           |
| `-32602`     | Invalid params: bad entry point, wrong chain, malformed UserOperation                                                                                                                         |
| `-32603`     | Internal error                                                                                                                                                                                |
| `-32000`     | Sponsorship denied. `error.data.reason` is one of the [denial reasons](/infrastructure/rest-api-v2.md#denial-reason-catalog)                                                                  |
| `-32001`     | New-account deployment (`factory`/`factoryData`) is disabled on this endpoint (`PAYMASTER_RPC_ALLOW_ACCOUNT_DEPLOYMENT` is `false`). Default is allowed, because a first transaction needs it |

Example denial:

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "error": {
    "code": -32000,
    "message": "Sponsorship denied: …",
    "data": { "reason": "user_hourly_spend_cap_exceeded" }
  }
}
```

The SDK maps reasons to typed errors: `paymasterReasonToError` and `mapPaymasterReason` from `@pepu-chain/sdk/paymaster`. See [Error Reference](/reference/errors.md).

## CORS

The route is called straight from browsers, so it answers `OPTIONS` itself. By default it allows any origin (`Access-Control-Allow-Origin: *`); v1 narrows that with `PAYMASTER_RPC_ALLOWED_ORIGINS`. That is a browser speed bump only, not authentication. v2 binds the **project** to allowed origins separately, per client token, inside the engine.

## Using the SDK builders

```ts
import {
  buildPaymasterContext,
  buildPaymasterDataRequest,
  buildPaymasterStubDataRequest,
} from "@pepu-chain/sdk";
import { buildErc7677Url } from "@pepu-chain/sdk/paymaster";

const url = buildErc7677Url({
  baseUrl: "https://paymaster.pepeunchained.com", // hosted base; http://localhost:8788 locally
  token: process.env.PEPU_CLIENT_TOKEN!,    // a ppt_ client token, never a ppk_ server key in a browser
  version: 2,                               // v2 serves /v2/paymaster/<token>; use 1 for /paymaster/<token>
});

const context = buildPaymasterContext({ privyAccessToken });
const stubRequest = buildPaymasterStubDataRequest({ userOperation, context });
const dataRequest = buildPaymasterDataRequest({ userOperation, context });
```

Both builders default the EntryPoint to the registry's v0.7 address and the chain to 97741. `buildErc7677Url` refuses a `ppk_` server key in a browser because the URL would embed the secret. In practice, Privy or viem makes these calls once you give them the URL; reach for the builders when debugging or writing a custom client. See [/paymaster](/sdk/paymaster.md).

## Common failures

| Symptom                                           | Cause                                                                                | Fix                                                                                                 |
| ------------------------------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| `invalid params: entryPoint ... not supported`    | Sent a v0.6 EntryPoint                                                               | Use `0x0000000071727De22E5E9d8BAf0edAc6f37da032`                                                    |
| `invalid params: chainId`                         | Wrong chain, or a typo such as 97721                                                 | `0x17dcd` (97741)                                                                                   |
| `-32000` with `invalid_or_expired_token`          | Missing, expired or unverifiable Privy token                                         | Send a fresh token                                                                                  |
| `-32000` with `unknown_or_invalid_app_credential` | The credential in the URL is unknown or revoked                                      | Check the URL; rotate if a key leaked                                                               |
| Works in `curl`, fails in the browser             | Origin not allowed for this client token, or CORS                                    | Add the origin to the project's `allowedOrigins`; see [REST API v2](/infrastructure/rest-api-v2.md) |
| `AA33 reverted` at the bundler                    | Approval expired, or (v2) the bundler executor is not allowlisted                    | Request fresh data; use the PEPU bundler                                                            |
| First sponsored transaction denied on cap         | Per-user cap smaller than the one-time account deployment (about 12 PEPU worst case) | Size caps above it. See [Policy, Caps and the Circuit Breaker](/infrastructure/policy-and-caps.md)  |

## Related

* [Paymaster Overview](/infrastructure/overview-2.md)
* [Credential Matrix](/infrastructure/auth.md)
* [REST API v1](/infrastructure/rest-api-v1.md), [REST API v2](/infrastructure/rest-api-v2.md)
* [Bundler JSON-RPC API](/infrastructure/api.md)
* [Send a Sponsored Transaction](/recipes/sponsored-send.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.pepeunchained.com/infrastructure/erc7677.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
