> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://www.openfort.io/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

# Spend with Gnosis Pay

A [Gnosis Pay](https://gnosispay.com) card is a Visa debit card that spends from a self-custodial [Safe](https://safe.global) on Gnosis Chain. In this recipe an Openfort embedded wallet signs in to Gnosis Pay, takes the user through onboarding, and becomes the **owner** of the Safe that Gnosis Pay deploys. The app then issues a virtual card and funds the Safe with EURe. It calls the [Gnosis Pay User API](https://docs.gnosispay.com/guides/configuring-environment) directly from React Native, with no backend.

With Openfort and Gnosis Pay together, your app can:

* Create a self-custodial **EOA** wallet on login, with password recovery and no seed phrase
* Sign in to Gnosis Pay with that wallet (Sign-In with Ethereum)
* Run the Gnosis Pay onboarding: email, terms, Sumsub KYC, and source of funds
* Get a Gnosis Pay account, issue a virtual Visa card, and fund it with EURe

:::warning
**Gnosis Pay requires approval from the Gnosis Pay team.** Access isn't self-serve:

* **Sandbox** is by invite: the Gnosis Pay team sends you access to the [Partner Dashboard](https://partners.gnosispay.com).
* **Production** needs a partnership agreement with Gnosis Pay.
* **Your app's SIWE domain** must be approved and whitelisted by Gnosis Pay in both environments.

If you need help coordinating access with Gnosis Pay, [contact the Openfort team](https://t.me/joalavedra).
:::

:::note
This recipe is not yet in the recipes-hub default branch. Its source is in review in [recipes-hub#44](https://github.com/openfort-xyz/recipes-hub/pull/44), and it has not been run end to end against the Gnosis Pay sandbox yet.
:::

## How it works

Gnosis Pay deploys the user's Safe, with its Roles and Delay modules, when the app calls `POST /user/account`. The embedded wallet is created as an **EOA**. Its address is the only one that can sign in to the account and the only owner of the Safe, and Gnosis Pay doesn't let you change it later. The wallet signs the SIWE message with `personal_sign` and sends EURe to the Safe with `eth_sendTransaction`.

| Step | What happens | Gnosis Pay User API |
| --- | --- | --- |
| Sign in | The wallet signs a SIWE message and gets an access and refresh token | `GET /auth/siwe/{address}`, `POST /auth/siwe` |
| Register | Email one-time code, then create the Gnosis Pay user | `POST /email-verification`, `POST /user` |
| Onboard | Follow the status: terms, KYC, source of funds | `GET /user/onboarding`, `POST /user/terms`, `POST /source-of-funds` |
| Create account | Gnosis Pay deploys the Safe the wallet owns | `POST /user/account`, `GET /user/account` |
| Issue card | A virtual Visa card on the account | `POST /cards/virtual`, `GET /cards` |
| Fund | Transfer EURe from the wallet into the Safe | ERC-20 `transfer` on Gnosis Chain |
| Balances | Spendable balance and deposits still in AML screening | `GET /user/balances/v2` |

## Sandbox and production

| | Sandbox | Production |
| --- | --- | --- |
| User API base URL | `https://core.sandbox.gnosispay.in/user-api` | `https://core.prod.gnosispay.com/user-api` |
| Access | Invite from Gnosis Pay | Partnership agreement |
| KYC | Sumsub sandbox, test documents | Sumsub production |
| Card activity | Simulated | Real Visa network |
| SIWE domain whitelisting | Required | Required |

The recipe picks the base URL from `GNOSIS_PAY_ENV`. The Safe and funding are on Gnosis Chain (chain ID 100) in both environments.

## Getting started

:::note
* macOS with Xcode (iOS Simulator) or Android Studio (Android Emulator)
* An [Openfort account](https://dashboard.openfort.io)
* Gnosis Pay sandbox access and a whitelisted SIWE domain
* **EURe** on Gnosis Chain to fund the card, and a little **xDAI** for gas unless you sponsor it
:::

:::steps
### Set up your project

Check out the recipe from [recipes-hub#44](https://github.com/openfort-xyz/recipes-hub/pull/44) (branch `recipe/gnosis-pay`) and install it:

```bash
cd gnosis-pay
pnpm install
```

The recipe uses **password** wallet recovery, so it needs no backend for Shield encryption sessions.

### Configure your Openfort credentials

In the [Openfort Dashboard](https://dashboard.openfort.io):

1. Create an account or sign in
2. Get your **Publishable Key** (`pk_test_...`) from **Developers → API Keys**
3. Copy your **Shield Publishable Key** from the **Shield** section of the same page
4. (Optional) Create a **Gas sponsorship** on Gnosis Chain and note its **gas sponsorship ID** (`pol_...`)

### Configure your environment

```bash
cp .env.example .env
```

```bash
OPENFORT_PROJECT_PUBLISHABLE_KEY=pk_test_...
OPENFORT_SHIELD_PUBLISHABLE_KEY=your-shield-publishable-key
OPENFORT_FEE_SPONSORSHIP_ID=          # Optional: leave blank to pay gas in xDAI
GNOSIS_PAY_ENV=sandbox                # or production
GNOSIS_PAY_SIWE_DOMAIN=myapp.xyz      # the domain Gnosis Pay whitelisted for you
GNOSIS_PAY_SIWE_URI=                  # Optional: defaults to https://<domain>
GNOSIS_PAY_APP_NAME=                  # Optional: app name shown in the SIWE message
GNOSIS_RPC_URL=https://rpc.gnosischain.com
```

### Configure the provider

The provider targets Gnosis Chain and creates the wallet as an EOA with password recovery:

```tsx
// app/_layout.tsx
import { AccountTypeEnum, OpenfortProvider } from "@openfort/react-native";
import { GNOSIS_CHAIN } from "@/lib/constants"; // Gnosis Chain (id 100)

<OpenfortProvider
  publishableKey={getPublishableKey()}
  supportedChains={[GNOSIS_CHAIN]}
  walletConfig={{
    shieldPublishableKey: getShieldPublishableKey(),
    recoveryMethod: "password", // key is encrypted client-side
    accountType: AccountTypeEnum.EOA, // plain ECDSA signer for SIWE and the Safe owner
    feeSponsorshipId: getFeeSponsorshipId(),
  }}
>
  {/* app */}
</OpenfortProvider>
```

On login, `components/CardApp.tsx` creates the EOA with `ethereum.create(...)`, or restores it with `ethereum.setActive(...)` if the user already has one.

### Sign in to Gnosis Pay with SIWE

Gnosis Pay returns the SIWE message to sign. The wallet signs it with `personal_sign`, and Gnosis Pay exchanges the signature for a 15-minute access token and a 7-day refresh token:

```ts
// lib/session.ts
const query = new URLSearchParams({ domain: siwe.domain, uri: siwe.uri, appName: siwe.appName });
const { message } = await raw(`/auth/siwe/${address}?${query}`);
const signature = await signMessage(message); // personal_sign from the embedded wallet
await save(await raw("/auth/siwe", { method: "POST", body: JSON.stringify({ address, signature, message }) }));
```

Every refresh rotates the refresh token, and Gnosis Pay logs the user out if a refresh token is used twice. The recipe keeps the refresh token in the device keychain (`expo-secure-store`), keeps the access token in memory, and shares one refresh between concurrent requests that get a `401`.

### Register and onboard the user

`POST /user` returns a new access token that carries the user ID. Use that token for every later call. Then poll `GET /user/onboarding`: it returns the next step, and the app renders one screen per status:

| Status | What the app does |
| --- | --- |
| `action_accept_tos` | Shows the terms and calls `POST /user/terms` |
| `action_complete_kyc`, `action_kyc_resubmission_requested` | Opens the Sumsub `webSdkUrl` in the browser |
| `action_complete_sof` | Shows the source-of-funds questions and calls `POST /source-of-funds` |
| `action_create_account` | Calls `POST /user/account` |
| `waiting_kyc_setup`, `waiting_account_setup`, `kyc_manual_review` | Waits, then refreshes |
| `completed` | Shows the card, the Safe, and funding |

```ts
// lib/gnosisPay.ts
export const gnosisPay = {
  getOnboarding: () => session.request<Onboarding>("/user/onboarding"),
  async register(email: string, otp: string) {
    const { accessToken } = await session.request<{ accessToken: string }>("/user", post({ email, otp }));
    session.useAccessToken(accessToken);
  },
  // Gnosis Pay deploys the user's Safe (owner = the signed-in wallet) and provisions the account.
  createAccount: () => session.request<{ accountId: string }>("/user/account", post()),
  // `phone` is required for the first card (it creates the cardholder).
  createVirtualCard: (cardName: string, phone?: string) =>
    session.request<{ cardId: string }>("/cards/virtual", post({ cardName, phone })),
  // ...
};
```

### Fund the card

`GET /user/account` returns the account's `safeAddress` and `nativeCurrency`. Funding is an ERC-20 transfer of the matching token from the embedded wallet into the Safe:

```ts
// lib/gnosisPay.ts
const data = transferInterface.encodeFunctionData("transfer", [account.safeAddress, amount]);
return eip1193SendTransaction(provider, owner, { to: tokenFor(account).address, data });
```

Gnosis Pay screens every deposit for AML first. `GET /user/balances/v2` reports it under `processingDeposits` until it moves to `spendable`.

### Run the application

```bash
pnpm run ios       # iOS Simulator
pnpm run android   # Android Emulator
```

Log in, sign in to Gnosis Pay, and follow the onboarding steps. In sandbox, Sumsub accepts its test documents. Once the account is active, create a virtual card, send EURe and a little xDAI on Gnosis Chain to your wallet, and fund the card.
:::

## Account limits and the Delay module

Gnosis Pay configures the Safe's modules for you:

* The Roles module enforces a fixed onchain limit of 20,000 USD per token per day, twice the card limit, so card settlement never needs a user signature.
* The Delay module uses a 25-hour delay and a 31-hour expiration, to discourage direct use. Any interaction with the Delay module blocks the card until Gnosis Pay support resolves it.
* Withdrawals are instant through `POST /user/withdrawal`.

Set a lower daily spending limit with `PUT /user/account/limit` for the account or `PUT /cards/{cardId}/limit` for a card.

## Test card spending in sandbox

The sandbox [card transaction simulator](https://docs.gnosispay.com/guides/cards/card-simulator) runs authorizations, reversals, and clearings against a card without the Visa network. It uses HTTP Basic Auth with a password Gnosis Pay gives you during onboarding. That password is a partner secret, so call the simulator from a terminal or your backend, never from the app.

## Spendable tokens

[Tokens Gnosis Pay accounts support](https://docs.gnosispay.com/concepts/safe/tokens-and-chains-supported) on Gnosis Chain. The recipe picks the token from the account's `nativeCurrency`:

| Currency | Token | Address | Decimals |
| --- | --- | --- | --- |
| EUR | EURe | `0x420CA0f9B9b604cE0fd9C18EF134C705e5Fa3430` | 18 |
| GBP | GBPe | `0x8E34bfEC4f6Eb781f9743D9b4af99CD23F9b7053` | 18 |
| USD | USDCe | `0x2a22f9c3b484c3629090FeED35F17Ff8F88f76F0` | 6 |

Gnosis Pay also supports USDC and USDT accounts on Celo; this recipe covers Gnosis Chain only.

## Next steps

* [Gnosis Pay documentation](https://docs.gnosispay.com)
* [Authenticating with SIWE](https://docs.gnosispay.com/guides/auth-with-siwe)
* [Gnosis Pay onboarding](https://docs.gnosispay.com/guides/onboarding)
* [Embedded Wallet Guide](https://www.openfort.io/docs/products/embedded-wallet)
* [Gas Sponsorship](https://www.openfort.io/docs/configuration/gas-sponsorship)
