# Spend with Gnosis Pay

A [Gnosis Pay](https://gnosispay.com) card is a Visa debit card attached to a self-custodial [Safe](https://safe.global) on Gnosis Chain. This recipe makes an Openfort embedded wallet the **owner** of that account: it creates the Safe, configures its spending allowance, funds it with EURe, and spends within the limit — entirely on-chain, with no backend. The on-chain pieces use [`@gnosispay/account-kit`](https://github.com/gnosispay/account-kit).

With Openfort and Gnosis Pay together, your app can:

* Auto-create a self-custodial **EOA** wallet on login (password recovery — no seed phrase, no backend)
* Deploy a Gnosis Pay account (a Safe with **Roles** + **Delay** modules) the user fully owns
* Configure a spending allowance, fund it with EURe, and spend within the limit — all from React Native

:::warning
This recipe builds and runs the **on-chain account** a Gnosis Pay card spends from. Issuing the physical/virtual Visa card itself requires KYC (Sumsub) and a Gnosis Pay partnership, which can't happen inside an open sample. See the [Gnosis Pay onboarding flow](https://docs.gnosispay.com/onboarding-flow).
:::

<HoverCardLink
  title="View Sample Code"
  subtitle="GitHub Repository"
  description="Complete source code for the Gnosis Pay integration with an Openfort embedded wallet, Expo React Native, and account-kit."
  href="https://github.com/openfort-xyz/recipes-hub/tree/main/gnosis-pay"
  img={{
  src: "/img/icons/github-icon.svg",
  alt: "GitHub Icon",
  className: "rounded-none",
}}
  color="#333"
  external
/>

## How it works

A Gnosis Pay account is a Safe with two Zodiac modules — **Roles** (a scoped spending allowance) and **Delay** (a cooldown on sensitive changes). The embedded wallet is created as an **EOA** so it can be the Safe's sole owner and sign with plain ECDSA (a smart-account owner would need ERC-1271 contract signatures, which account-kit's payloads don't expect). The same wallet both **signs** account-kit's EIP-712 payloads (`eth_signTypedData_v4`) and **relays** the transactions (`eth_sendTransaction`).

```text
predictAccountAddress({ owner })            // deterministic Safe address (CREATE2)
  → populateAccountCreation({ owner })       // deploy the bare 1/1 Safe
  → populateAccountSetup(…, config, sign)    // add Roles + Delay + spending allowance
  → transfer EURe into the Safe              // fund the card
  → execTransactionWithRole(…)               // spend within the allowance
  → accountQuery(…)                          // read integrity status + accrued allowance
```

| Step | What happens | account-kit |
| ------- | ----------------------------------------------------------- | ----------------------- |
| Predict | Derive the account's deterministic Safe address from the owner | `predictAccountAddress` |
| Create  | Deploy the bare 1/1 Safe                                     | `populateAccountCreation` |
| Set up  | Attach the Roles + Delay modules and a spending allowance    | `populateAccountSetup`  |
| Fund    | Transfer EURe into the account Safe (a plain ERC-20 transfer) | —                       |
| Spend   | Owner calls the Roles module to draw EURe out, capped by the allowance | `execTransactionWithRole` |
| Inspect | Read the account's integrity status and accrued allowance   | `accountQuery`          |

## Getting started

:::note
* macOS with Xcode (iOS Simulator) or Android Studio — or the **Expo Go** app on a device
* An [Openfort account](https://dashboard.openfort.io)
* A small amount of **xDAI** (gas) and **EURe** (to load + spend) on Gnosis Chain
:::

::::steps
### Set up your project

Clone the recipe and install dependencies:

```bash
pnpx gitpick openfort-xyz/recipes-hub/tree/main/gnosis-pay openfort-gnosis-pay
cd openfort-gnosis-pay
pnpm install
```

The recipe uses **password** wallet recovery, so — unlike most embedded-wallet recipes — 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
4. (Optional) Create a **Gas Policy** on Gnosis Chain and note its **gas sponsorship ID** (`pol_...`)

### Configure your environment

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

```bash
OPENFORT_PUBLISHABLE_KEY=pk_test_...
OPENFORT_SHIELD_PUBLISHABLE_KEY=your-shield-publishable-key
PIMLICO_API_KEY=                                   # Optional — set to make every action gasless (no xDAI)
OPENFORT_FEE_SPONSORSHIP_ID=                       # Optional — leave blank to pay gas in xDAI
GNOSIS_RPC_URL=https://rpc.gnosischain.com         # Optional
```

### Configure the provider

The provider targets Gnosis Chain and creates the wallet as an EOA with password recovery — no encryption-session endpoint, so no backend:

```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, // EOA owner so it can sign for the Safe
    feeSponsorshipId: getFeeSponsorshipId(),
  }}
>
  {/* app */}
</OpenfortProvider>
```

On login the app auto-provisions the EOA, so there's no manual wallet step:

```tsx
// components/CardApp.tsx
const account = await ethereum.create({
  chainId: GNOSIS_CHAIN_ID,
  accountType: AccountTypeEnum.EOA,
  recoveryMethod: "password",
  recoveryPassword: DEMO_RECOVERY_PASSWORD,
});
await ethereum.setActive({
  address: account.address,
  chainId: GNOSIS_CHAIN_ID,
  recoveryMethod: "password",
  recoveryPassword: DEMO_RECOVERY_PASSWORD,
});
```

### Sign account-kit's typed data

account-kit returns each action as a relay-ready transaction and asks the owner to sign EIP-712 typed data through a library-agnostic callback. Adapt it to the embedded wallet's EIP-1193 provider, re-adding the `EIP712Domain` type that `eth_signTypedData_v4` requires (ethers-style typed data omits it):

```ts
// lib/eip1193.ts
export function eip1193SignTypedData(provider, owner) {
  return async ({ domain, primaryType, types, message }) => {
    const payload = JSON.stringify({
      domain,
      primaryType,
      types: { EIP712Domain: domainTypes(domain), ...types }, // re-add domain type
      message,
    });
    return provider.request({
      method: "eth_signTypedData_v4",
      params: [owner.toLowerCase(), payload],
    });
  };
}
```

:::tip
The SDK ships a `signTypedData(provider, address, typedData)` helper that does this for you — pass typed data straight from account-kit, ethers, or viem and it handles the `EIP712Domain` and `bigint` serialization.
:::

### Create and set up the account

`predictAccountAddress` gives the deterministic Safe address; `populateAccountCreation` deploys it; `populateAccountSetup` upgrades it into a Gnosis Pay account with a spending allowance. Relay each populated transaction from the owner EOA:

```ts
// lib/gnosisPay.ts
import {
  predictAccountAddress,
  populateAccountCreation,
  populateAccountSetup,
} from "@gnosispay/account-kit";
import { parseUnits } from "ethers";

const account = predictAccountAddress({ owner });

// 1) Deploy the 1/1 Safe
await sendTransaction(provider, owner, populateAccountCreation({ owner }));

// 2) Upgrade it into a Gnosis Pay account (Roles + Delay + allowance)
const setupTx = await populateAccountSetup(
  { account, owner, chainId: 100, nonce },
  {
    spender: owner, // in production: Gnosis Pay's Spender Safe
    receiver: owner, // in production: Gnosis Pay's Settlement Safe
    token: CARD_TOKEN.address, // EURe
    allowance: { period: 60 * 60 * 24, refill: parseUnits("100", 18) }, // 100 EURe / day
    delay: { cooldown: 180, expiration: 0 },
  },
  eip1193SignTypedData(provider, owner)
);
await sendTransaction(provider, owner, setupTx);
```

:::note
`accountQuery` returns `UnexpectedError` (not `SafeNotDeployed`) for a never-deployed account, because EVM calls to an empty address succeed with no data. The recipe checks `provider.getCode(account)` first to detect the "not created" stage, then queries integrity once the Safe exists.
:::

### Fund the card

Funding the card is a plain ERC-20 transfer of EURe into the account Safe:

```ts
// transfer EURe from the embedded wallet into the account Safe
const data = erc20.encodeFunctionData("transfer", [account, amount]);
await sendTransaction(provider, owner, { to: CARD_TOKEN.address, data });
```

### Spend within the allowance

No second Safe is needed. Setup assigned the owner the spending role, so the owner spends by calling the account's Roles module directly — the transfer goes through only up to the remaining allowance and reverts otherwise:

```ts
// lib/gnosisPay.ts
import { predictAddresses } from "@gnosispay/account-kit";

const roles = predictAddresses(account).roles;
const data = rolesInterface.encodeFunctionData("execTransactionWithRole", [
  CARD_TOKEN.address,
  0,
  erc20.encodeFunctionData("transfer", [owner, amount]), // to the scoped receiver
  0, // OperationType.Call
  SPENDING_ROLE_KEY, // keccak256("SPENDING_ROLE")
  true, // revert if over the allowance
]);
await sendTransaction(provider, owner, { to: roles, data });
```

:::tip
account-kit's `populateSpend` models Gnosis Pay's production design (a separate Spender Safe + delegate), which lets a backend or session key spend on the user's behalf. Both paths draw against the same Roles-module allowance — assign the role to that address in setup instead of the owner.
:::

### Run the application

```bash
pnpm run ios       # iOS Simulator
pnpm run android   # Android Emulator
pnpm start         # Expo Go on a device
```

Log in, and the app auto-creates your wallet and drops you on the dashboard. It needs a little **xDAI** for gas and **EURe** to load and spend — copy your wallet address from the dashboard, send a small amount on Gnosis Chain, then Create → Set up → Fund → Spend.
::::

## Gas

By default the embedded wallet is an EOA, so it relays its own transactions and pays gas in **xDAI** (cents on Gnosis Chain). The app shows a "fund your wallet for gas" prompt until it has a little xDAI.

To make every action **gasless**, set `PIMLICO_API_KEY`. A standard ERC-4337 paymaster sponsors *UserOperations*, not a plain EOA's `eth_sendTransaction`, so the recipe relays account-kit's transactions through a **Pimlico smart account owned by the embedded EOA**. That smart account becomes the sender, the spend role-member, and where you hold EURe; the EOA still signs everything, and the gas prompt disappears. See `lib/pimlico.ts`.

:::note
The paymaster only removes the gas cost — you still need **EURe** in the fund address to load and spend the card.
:::

## From sandbox to a real card

This recipe deploys a self-custodial account with the **same on-chain structure** Gnosis Pay uses, but it is **not** a registered card — it can't be tapped at a terminal. No wallet creation step or KYC is shown because none happens: the embedded EOA and its Safe are provisioned silently, with no identity check. Issuing a real card adds a regulated layer on top:

1. **KYC onboarding.** Users verify identity at [gnosispay.com](https://gnosispay.com) (via Sumsub). Gnosis Pay is the licensed issuer and a Visa principal member — the only party that can issue the card.
2. **Monerium e-money + IBAN.** EURe is [Monerium](https://monerium.com)'s regulated e-money; minting/redeeming it and provisioning the SEPA IBAN behind the card require a (KYC'd) Monerium account. Here EURe is just an ERC-20 you transfer in.
3. **Issuance + settlement.** After KYC, Gnosis Pay deploys the user's Safe (the same account-kit structure) and issues a Visa card linked to it. At point of sale, Gnosis Pay's Spender / Settlement Safes are the role-member that pulls EURe from the Safe within the allowance — the exact path this recipe replicates with your own relayer as the `spender`.

The account model, the Roles + Delay modules, the allowance, and the spend mechanics are identical between this sandbox and production. To go live you swap the self-deployed account for a Gnosis-Pay-onboarded one and point `spender` / `receiver` at Gnosis Pay's Safes. Start with the [Gnosis Pay onboarding flow](https://docs.gnosispay.com/onboarding-flow).

## Spendable tokens

The currencies a Gnosis Pay card can spend from its Safe ([reference](https://help.gnosispay.com/hc/en-us/articles/39563942614292-Spendable-Tokens-for-Your-Gnosis-Card-Safe)). The recipe settles in EURe by default:

| Token | Address                                        | Decimals |
| ----- | ---------------------------------------------- | -------- |
| EURe  | `0x420CA0f9B9b604cE0fd9C18EF134C705e5Fa3430`   | 18       |
| GBPe  | `0x8E34bfEC4f6Eb781f9743D9b4af99CD23F9b7053`   | 18       |
| USDCe | `0x2a22f9c3b484c3629090FeED35F17Ff8F88f76F0`   | 6        |

## Next steps

* [Gnosis Pay documentation](https://docs.gnosispay.com)
* [@gnosispay/account-kit](https://github.com/gnosispay/account-kit)
* [Embedded Wallet Guide](/docs/products/embedded-wallet)
* [Gas Sponsorship](/docs/configuration/gas-sponsorship)
