> 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/sdk/errors.md).

# SDK Error Catalog

Everything `@pepu-chain/sdk` throws is a `PepuError` subclass with a stable `code`, a one-sentence `hint` and a `docsUrl`. This page is the catalog as a table, with the helpers that read it: `explainError`, `toPepuError`, `toErrorBody` and `toErrorResponse`. The long-form entries, with anchors, are on [Error Reference](/reference/errors.md).

## Prerequisites

* `@pepu-chain/sdk` installed. The error helpers are exported from the package root.

## Using the catalog

```ts
import { PepuError, explainError, toErrorResponse } from "@pepu-chain/sdk";

// Explain anything: an Error, an error message, or raw revert data (0x...). Pure; never throws.
export function describe(e: unknown): string {
  const x = explainError(e);
  return `${x.code}: ${x.fix} (${x.docsUrl})`;
}

// Route handlers: return the typed error as JSON with the catalog's HTTP status.
export async function POST(): Promise<Response> {
  try {
    // Positional constructor: new PepuError(code, message?, { hint?, docsUrl?, retryable?, status?, details?, cause? })
    throw new PepuError("CONFIG_MISSING_ENV", "PEPU_BUNDLER_URL is not set.", { hint: "Set it in .env.local." });
  } catch (e) {
    return toErrorResponse(e); // { error: { code, message, hint } }
  }
}
```

| Helper                       | Does                                                                                                                               |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `explainError(e)`            | Returns `{ code, title, cause, fix, docsUrl, matched }` for an error, a message or revert data. First matching catalog entry wins. |
| `toPepuError(e, ctx?)`       | Wraps anything thrown into the right `PepuError` subclass. `PepuError`s pass through.                                              |
| `toErrorBody(e)`             | The `{ error: { code, message, hint } }` JSON body.                                                                                |
| `toErrorResponse(e)`         | A `Response` with that body and the status from the table below.                                                                   |
| `errorDocsUrl(code)`         | `https://docs.pepeunchained.com/reference/errors#<CODE>`                                                                           |
| `mapPaymasterReason(reason)` | Paymaster denial reason to catalog code. See [Error Reference](/reference/errors.md#paymaster-denial-reasons).                     |

## Catalog

| Code                                                                                          | HTTP | Retryable | Title                                                   | Cause                                                                                                                                                                                                              | Fix                                                                                                                                                                             |
| --------------------------------------------------------------------------------------------- | ---- | --------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`CONFIG_BAD_URL`](/reference/errors.md#CONFIG_BAD_URL)                                       | 500  | no        | Invalid endpoint URL                                    | A configured bundler, paymaster or RPC URL is not a valid http(s) URL.                                                                                                                                             | Use a full URL such as <https://bundler.example.com>.                                                                                                                           |
| [`CONFIG_MISSING_ENV`](/reference/errors.md#CONFIG_MISSING_ENV)                               | 500  | no        | Missing environment variable                            | A required environment variable is not set.                                                                                                                                                                        | Set the listed variables (see .env.example) and restart the process.                                                                                                            |
| [`CONFIG_ORIGIN_MISMATCH`](/reference/errors.md#CONFIG_ORIGIN_MISMATCH)                       | 500  | no        | Origin is not allowed by Privy                          | The page origin (scheme, host and port) is not in the Privy app's allowed origins. A different dev port is the usual cause.                                                                                        | Add the exact origin, including the port, to Privy dashboard Allowed origins.                                                                                                   |
| [`CONFIG_PRIVY_CREATE_ON_LOGIN`](/reference/errors.md#CONFIG_PRIVY_CREATE_ON_LOGIN)           | 500  | no        | Embedded wallet never appears after login               | The dashboard's automatic embedded wallet creation is off, so createOnLogin never resolves.                                                                                                                        | Create the wallet server-side with the ensure-wallet route (createEnsureWalletRoute), or enable creation in the Privy dashboard.                                                |
| [`CONFIG_PRIVY_DASHBOARD`](/reference/errors.md#CONFIG_PRIVY_DASHBOARD)                       | 500  | no        | Privy dashboard misconfiguration                        | A required Privy dashboard setting is missing.                                                                                                                                                                     | Run checkPrivyDashboardConfig() and follow its fix list.                                                                                                                        |
| [`CONFIG_SERVER_ONLY`](/reference/errors.md#CONFIG_SERVER_ONLY)                               | 500  | no        | Server-only module imported in the browser              | A server entry point was bundled into client code.                                                                                                                                                                 | Import server entries only from route handlers, server actions and scripts.                                                                                                     |
| [`AUTH_INVALID_TOKEN`](/reference/errors.md#AUTH_INVALID_TOKEN)                               | 401  | no        | Invalid or missing Privy access token                   | The request carried no bearer token, or Privy could not verify it.                                                                                                                                                 | Send Authorization: Bearer \<getAccessToken()> and verify the token server-side.                                                                                                |
| [`WALLET_COINBASE_UNSUPPORTED_CHAIN`](/reference/errors.md#WALLET_COINBASE_UNSUPPORTED_CHAIN) | 400  | no        | Coinbase Smart Wallet does not support PEPU             | Coinbase Smart Wallet is a smart-contract wallet on chains it knows; PEPU (97741) is not one of them, and it floods the console with unsupported-chain errors.                                                     | Pin Coinbase to eoaOnly via createPepuPrivyConfig() from @pepu-chain/sdk/privy.                                                                                                 |
| [`WALLET_INJECTED_ACTIVE`](/reference/errors.md#WALLET_INJECTED_ACTIVE)                       | 400  | no        | An injected browser wallet is the active account        | wagmi's active account is a browser extension rather than the Privy embedded wallet.                                                                                                                               | Pin the embedded wallet with useActiveWalletPin() from @pepu-chain/sdk/privy/wagmi.                                                                                             |
| [`WALLET_NOT_PROVISIONED`](/reference/errors.md#WALLET_NOT_PROVISIONED)                       | 409  | yes       | User has no embedded wallet yet                         | The user is signed in but no embedded Ethereum wallet exists.                                                                                                                                                      | Call the ensure-wallet route (useEnsureWallet or WalletGate) before acting.                                                                                                     |
| [`PRIVY_CHAIN_REJECTED_97741`](/reference/errors.md#PRIVY_CHAIN_REJECTED_97741)               | 502  | no        | Privy rejects server-side eth\_sendTransaction on PEPU  | Privy's server-side send path does not know chain 97741 and rejects it. Signing works; sending does not.                                                                                                           | Sign with eth\_signTransaction on the server and broadcast the raw transaction yourself (signAndBroadcast from @pepu-chain/sdk/session).                                        |
| [`PRIVY_KEY_QUORUM_MISMATCH`](/reference/errors.md#PRIVY_KEY_QUORUM_MISMATCH)                 | 500  | no        | Privy authorization key does not match the wallet owner | Privy returned 401 'No valid authorization keys or user signing keys available': the server's PRIVY\_AUTHORIZATION\_KEY is not a signer on the wallet, or the key quorum id differs from the one the user granted. | Make sure PRIVY\_AUTHORIZATION\_KEY is the private half of the key whose quorum id is NEXT\_PUBLIC\_PRIVY\_KEY\_QUORUM\_ID, and that the user granted that quorum as a signer.  |
| [`POLICY_NAME_TOO_LONG`](/reference/errors.md#POLICY_NAME_TOO_LONG)                           | 400  | no        | Privy policy or rule name is too long                   | Privy rejects policy and rule names of 50 or more characters (the real limit is 49).                                                                                                                               | Shorten the policy or rule name to 49 characters or fewer.                                                                                                                      |
| [`POLICY_UNSCOPED`](/reference/errors.md#POLICY_UNSCOPED)                                     | 400  | no        | Session policy is not scoped                            | A session policy needs a target contract, at least one function and an expiry.                                                                                                                                     | Pass contractAddress, functions and expiresAtUnixSeconds to buildSessionPolicy().                                                                                               |
| [`SESSION_APPROVAL_MISMATCH`](/reference/errors.md#SESSION_APPROVAL_MISMATCH)                 | 409  | no        | Session approval does not match                         | The approval was made for another account, permission id or mode.                                                                                                                                                  | Re-run prepare, have the owner sign again, then activate.                                                                                                                       |
| [`SESSION_EXPIRED`](/reference/errors.md#SESSION_EXPIRED)                                     | 410  | no        | Session expired                                         | The session's expiry has passed.                                                                                                                                                                                   | Grant a new session.                                                                                                                                                            |
| [`SESSION_NONE`](/reference/errors.md#SESSION_NONE)                                           | 409  | no        | No active session                                       | The user has not granted a session, or it was revoked.                                                                                                                                                             | Ask the user to grant a session first.                                                                                                                                          |
| [`BUNDLER_AA_FAILED`](/reference/errors.md#BUNDLER_AA_FAILED)                                 | 502  | no        | EntryPoint rejected the UserOperation                   | The EntryPoint reported FailedOp with an AAxx code during validation or execution.                                                                                                                                 | Read the AAxx code: AA21 prefund missing, AA23 signature or validation reverted, AA25 invalid nonce, AA31/AA33/AA34 paymaster problems.                                         |
| [`BUNDLER_UNREACHABLE`](/reference/errors.md#BUNDLER_UNREACHABLE)                             | 502  | yes       | Bundler or paymaster is unreachable                     | The request to the bundler or paymaster URL failed before any response.                                                                                                                                            | Check PEPU\_BUNDLER\_URL and PEPU\_PAYMASTER\_URL. Use checkSponsorship() to see which one failed.                                                                              |
| [`PAYMASTER_API_ERROR`](/reference/errors.md#PAYMASTER_API_ERROR)                             | 502  | no        | Paymaster API request failed                            | The paymaster API returned an error that has no more specific PEPU code.                                                                                                                                           | Read error.details (HTTP status and the service's error code) and the OpenAPI spec for that route.                                                                              |
| [`PAYMASTER_CAP`](/reference/errors.md#PAYMASTER_CAP)                                         | 429  | yes       | Sponsorship cap reached                                 | A daily, per-sender or per-operation sponsorship cap was hit.                                                                                                                                                      | Wait for the cap window to reset, or raise the project's daily and sender limits.                                                                                               |
| [`PAYMASTER_DENIED`](/reference/errors.md#PAYMASTER_DENIED)                                   | 403  | no        | Paymaster denied the sponsorship                        | The paymaster rejected the credential or the sender (for example unknown\_or\_invalid\_app\_credential or sender\_not\_owned\_by\_token\_user).                                                                    | Check the app credential or token, and that the smart account belongs to the user whose access token you sent.                                                                  |
| [`PAYMASTER_DESTINATION_NOT_ALLOWED`](/reference/errors.md#PAYMASTER_DESTINATION_NOT_ALLOWED) | 403  | no        | Paymaster refused the call destination                  | The UserOperation targets a contract that is not on the project's target allowlist.                                                                                                                                | Add the contract to the project's targets, or sponsor only calls to allowed targets.                                                                                            |
| [`PAYMASTER_FREE_TIER_UNAVAILABLE`](/reference/errors.md#PAYMASTER_FREE_TIER_UNAVAILABLE)     | 429  | yes       | Free tier cannot sponsor this operation                 | The free tier is disabled, its pool is empty, or the operation is larger than the per-op cap.                                                                                                                      | Deposit into your own project and use its credentials, or retry later.                                                                                                          |
| [`PAYMASTER_IDENTITY_REQUIRED`](/reference/errors.md#PAYMASTER_IDENTITY_REQUIRED)             | 401  | no        | A verified Privy account is required                    | The free tier (and any project with requireIdentity on) only sponsors users with a verified Privy access token.                                                                                                    | Send the user's Privy access token as Authorization: Bearer (or context.privyAccessToken for ERC-7677).                                                                         |
| [`PAYMASTER_NO_BALANCE`](/reference/errors.md#PAYMASTER_NO_BALANCE)                           | 429  | yes       | Sponsorship project has no balance                      | The project's paymaster deposit cannot cover this operation.                                                                                                                                                       | Deposit more PEPU to the project, then retry.                                                                                                                                   |
| [`PAYMASTER_ORIGIN_NOT_ALLOWED`](/reference/errors.md#PAYMASTER_ORIGIN_NOT_ALLOWED)           | 403  | no        | Paymaster refused the request origin                    | The request Origin is not on the project's allowlist.                                                                                                                                                              | Add the exact origin (scheme, host and port) to the project's allowed origins.                                                                                                  |
| [`PAYMASTER_PROJECT_NOT_CLAIMED`](/reference/errors.md#PAYMASTER_PROJECT_NOT_CLAIMED)         | 409  | no        | Sponsorship project is not claimed on-chain             | Deposits can sit in an unclaimed project, but nothing is sponsored until its owner has claimed it on-chain (the contract reports ProjectNotClaimed, the service project\_not\_claimed\_onchain).                   | Prove the Privy app (requestClaimAttestation from @pepu-chain/sdk/paymaster/server), call claimProject, then configureProject (setTargets, setDailyLimit, setSenderDailyLimit). |
| [`PAYMASTER_PROJECT_OWNER_CHANGED`](/reference/errors.md#PAYMASTER_PROJECT_OWNER_CHANGED)     | 403  | no        | Project owner changed since the credentials were issued | The on-chain owner of the project is no longer the address the off-chain credentials were issued to (an ownership transfer or re-claim completed).                                                                 | As the new owner, sign in with SIWE, register the project again and rotate the client token and server key.                                                                     |
| [`PAYMASTER_PROJECT_PAUSED`](/reference/errors.md#PAYMASTER_PROJECT_PAUSED)                   | 403  | no        | Sponsorship project is paused                           | The project owner paused sponsorship on-chain.                                                                                                                                                                     | The owner can resume it with setPaused(projectId, false).                                                                                                                       |
| [`PAYMASTER_RATE_LIMITED`](/reference/errors.md#PAYMASTER_RATE_LIMITED)                       | 429  | yes       | Paymaster API rate limit                                | Too many requests to the paymaster API from this IP or account.                                                                                                                                                    | Back off for the Retry-After interval (error.details.retryAfterSeconds when present).                                                                                           |
| [`PAYMASTER_SESSION_REQUIRED`](/reference/errors.md#PAYMASTER_SESSION_REQUIRED)               | 401  | no        | Paymaster owner session missing or expired              | Owner endpoints need a SIWE session token (1 h lifetime) for the project's current on-chain owner.                                                                                                                 | Call loginWithWallet() on the paymaster API client, then retry.                                                                                                                 |
| [`PAYMASTER_UNREACHABLE`](/reference/errors.md#PAYMASTER_UNREACHABLE)                         | 502  | yes       | Paymaster service is unreachable                        | The request to the paymaster service failed before any response (network error, DNS failure or timeout).                                                                                                           | Check PEPU\_PAYMASTER\_URL and your network. The hosted service is <https://paymaster.pepeunchained.com>; run checkSponsorship() or probePaymaster() for detail.                |
| [`MULTICALL3_CANONICAL_ADDRESS`](/reference/errors.md#MULTICALL3_CANONICAL_ADDRESS)           | 500  | no        | Canonical Multicall3 address used on PEPU               | PEPU's Multicall3 is not at the canonical 0xcA11...CA11 address.                                                                                                                                                   | Use getMulticall3() or pepuMulticall() from @pepu-chain/sdk, or the pepuMainnet chain object, which carries the right address.                                                  |
| [`TX_GAS_UNDERSHOOT`](/reference/errors.md#TX_GAS_UNDERSHOOT)                                 | 502  | yes       | Gas limit too low                                       | Nitro gas estimation undershoots on PEPU: the L1 data component is not covered by the plain estimate.                                                                                                              | Add headroom to the estimate, 20% via withGasLimitBuffer() from @pepu-chain/sdk/session.                                                                                        |
| [`TX_NONCE_RACE`](/reference/errors.md#TX_NONCE_RACE)                                         | 409  | yes       | Nonce conflict                                          | Two transactions raced for one nonce, or a replacement was underpriced.                                                                                                                                            | Serialize sends per account (a lock), re-read the pending nonce, and retry.                                                                                                     |
| [`TX_REVERTED`](/reference/errors.md#TX_REVERTED)                                             | 502  | no        | Transaction reverted                                    | The contract call reverted.                                                                                                                                                                                        | Decode the revert reason with explainError() and check arguments, allowances and balances.                                                                                      |
| [`UNKNOWN`](/reference/errors.md#UNKNOWN)                                                     | 500  | no        | Unknown error                                           | The error did not match any known PEPU failure.                                                                                                                                                                    | Read the original message. Run pepu\_known\_gotchas in the MCP server, or search the docs.                                                                                      |

## What to expect

* Messages name environment variables, never their values. Nothing in this catalog is a secret.
* Order in the catalog matters for `explainError`: specific entries match before general ones.

## Common failures

* `explainError` returns `UNKNOWN` for an error you expected to match: the SDK is older than the error text. Upgrade, or open the [Error Reference](/reference/errors.md) and search the symptom.

## Related

* [Error Reference](/reference/errors.md)
* [Troubleshooting](/troubleshooting/troubleshooting.md)
* [SDK Overview and Reference](/sdk/reference.md)
* [Core: @pepu-chain/sdk](/sdk/core.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/sdk/errors.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.
