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

# Security

> Protect wallet credentials and authenticate Rhinestone SDK requests safely.

Treat wallet signing credentials, Rhinestone API keys, RPC credentials, paymaster keys, and JWT private keys as separate secrets. A wallet provider authenticates the user; it does not authenticate your application to Rhinestone.

## Choose an authentication boundary

<Tabs>
  <Tab title="Server-side SDK">
    Use API-key authentication only in a trusted server runtime:

    ```ts theme={null}
    import { RhinestoneSDK } from '@rhinestone/sdk'

    const rhinestone = new RhinestoneSDK({
      auth: {
        mode: 'apiKey',
        apiKey: process.env.RHINESTONE_API_KEY!,
      },
    })
    ```

    Store the key in a secret manager or a server-only environment variable. Never use a `NEXT_PUBLIC_*`, `VITE_*`, or equivalent client-exposed variable for it.
  </Tab>

  <Tab title="Browser or mobile SDK">
    Use short-lived JWT access tokens issued by your authenticated backend:

    ```ts theme={null}
    import { RhinestoneSDK } from '@rhinestone/sdk'

    const rhinestone = new RhinestoneSDK({
      auth: {
        mode: 'experimental_jwt',
        accessToken: async () => {
          const response = await fetch('/api/auth/access-token', {
            credentials: 'include',
          })
          if (!response.ok) throw new Error('Unable to authenticate with Rhinestone')
          return ((await response.json()) as { token: string }).token
        },
        getIntentExtensionToken: async (intentInput) => {
          const response = await fetch('/api/auth/extension-token', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            credentials: 'include',
            body: JSON.stringify({ intentInput }),
          })
          if (!response.ok) throw new Error('Sponsorship was not authorized')
          return ((await response.json()) as { token: string }).token
        },
      },
    })
    ```

    The access token is expected to reach the client, so keep its lifetime short. The JWT signing private key must remain on the server.
  </Tab>
</Tabs>

Register the JWT public key in the [Dashboard](https://dashboard.rhinestone.dev), keep the matching private key on your backend, and implement authenticated token endpoints. JWT mode is experimental in SDK 2.16.1.

## Secure sponsored intents

A valid client session should not automatically authorize unlimited sponsorship. Before your backend calls `getIntentExtensionToken`, parse the complete serialized intent, bind it to the authenticated user and account, and enforce a server-side policy for:

* Source and destination chains
* Destination contracts, function selectors, and recipients
* Token addresses and maximum amounts
* Sponsorship categories and limits
* Rate limits and replay behavior

The SDK's `shouldSponsor` option provides built-in filters only for destination chain IDs, account addresses, and destination calls. Use those filters as an additional guard, not as complete intent validation.

The extension-token endpoint should validate the complete request against a runtime schema before applying your application policy. This example assumes `parseSerializedIntentInput` mirrors the full exported `SerializedIntentInput` type, validates every nested field, and rejects unknown fields:

```ts theme={null}
import type { SerializedIntentInput } from '@rhinestone/sdk'
import { createJwtSigner, shouldSponsor } from '@rhinestone/sdk/jwt-server'
import { parseSerializedIntentInput } from './intent-schema'
import { requireSession } from './session'
import { sponsorshipPolicy } from './sponsorship-policy'

const signer = createJwtSigner({
  jwt: {
    privateKey: JSON.parse(process.env.RHINESTONE_JWT_PRIVATE_JWK!),
    integratorId: process.env.RHINESTONE_INTEGRATOR_ID!,
    projectId: process.env.RHINESTONE_PROJECT_ID!,
    appId: process.env.RHINESTONE_APP_ID!,
    keyId: process.env.RHINESTONE_KEY_ID!,
  },
})

export async function POST(request: Request) {
  const session = await requireSession(request)
  const body = (await request.json()) as { intentInput?: unknown }

  let intentInput: SerializedIntentInput
  try {
    intentInput = parseSerializedIntentInput(body.intentInput)
  } catch {
    return Response.json({ error: 'Invalid intent' }, { status: 400 })
  }

  if (intentInput.account.address !== session.walletAddress) {
    return Response.json({ error: 'Account not authorized' }, { status: 403 })
  }

  const builtInPolicyPassed = await shouldSponsor(intentInput, {
    chain: ({ id }) => sponsorshipPolicy.destinationChains.has(id),
    account: (address) => address === session.walletAddress,
    calls: (calls) => sponsorshipPolicy.allowsCalls(calls),
  })
  const applicationPolicyPassed =
    sponsorshipPolicy.allowsSources(intentInput) &&
    sponsorshipPolicy.allowsTokenRequests(intentInput.tokenRequests) &&
    sponsorshipPolicy.allowsSponsorSettings(intentInput.options.sponsorSettings)

  if (!builtInPolicyPassed || !applicationPolicyPassed) {
    return Response.json({ error: 'Sponsorship denied' }, { status: 403 })
  }

  await sponsorshipPolicy.consumeRateLimit(session.userId, intentInput)
  const token = await signer.getIntentExtensionToken(intentInput)
  return Response.json({ token })
}
```

`allowsSources` must inspect every source-bearing field your application enables, including account access lists, pre-claim executions, and auxiliary funds. Keep the runtime schema and policy in server-owned code, bind the account to the authenticated session, and consume the rate limit before signing. Deny requests that cannot be parsed or do not match every applicable allowlist. See [sponsorship setup](/transactions/sponsorship/set-up) and [sponsorship policies and security](/transactions/sponsorship/policies-and-security).

## Protect each credential

* **Rhinestone API keys:** create separate production and test keys in the [Dashboard](https://dashboard.rhinestone.dev), grant only required scopes, rotate them, and revoke suspected leaks.
* **JWT signing keys:** register only the public key in the [Dashboard](https://dashboard.rhinestone.dev). Keep the private JWK server-side, use distinct key IDs for rotation, and never return it from an API route.
* **Wallet provider secrets:** only publish values the provider explicitly labels publishable. Turnkey API private keys, Magic secret keys, and OIDC private keys belong on the server.
* **Wallet keys:** use provider policy controls or a secret manager. Do not log private keys, recovery material, signatures awaiting submission, or raw authentication tokens.
* **RPC, bundler, and paymaster keys:** server-side is safest. If a provider permits browser credentials, restrict them by origin, chain, method, and budget.

## Validate signing requests

Display or independently validate the chain, recipient, token, amount, and call data before asking a user to sign. Treat provider callbacks and connected-wallet changes as untrusted state: confirm the active address before creating the account and clear cached account objects when the signer changes.

For server wallets, authorize every signing request against the application session. Do not expose an endpoint that signs arbitrary hashes or typed data for any authenticated caller.

For EIP-7702, verify the delegation target and chain scope before signing an authorization. The EOA remains a root authority after delegation; module-level controls do not make a compromised EOA safe.

## Operational controls

* Separate production, staging, and development projects and credentials.
* Apply request-size limits, rate limits, abuse detection, and structured audit logs without sensitive payloads.
* Alert on sponsorship spikes, repeated authorization failures, and unexpected destination contracts.
* Rotate a credential immediately after suspected exposure and invalidate active sessions where supported.
* Test recovery and key-rotation procedures before an incident.

Do not override internal SDK service endpoints in production. Use the public SDK configuration and authentication modes documented above.
