# Private payments with Unlink

A supplier issues invoices to your company; you settle them privately. This recipe pays each invoice through [Unlink](https://unlink.xyz)'s ZK shielded pool on **Monad testnet**, so the on-chain link between your treasury and the supplier is broken. The payer's wallet is an Openfort embedded **EOA** with **passkey** recovery, and the flow is fully **non-custodial** — the spending key is derived in the browser and never leaves it.

With Openfort and Unlink together, your app can:

* Auto-create a self-custodial **EOA** wallet on login (passkey recovery — no seed phrase, no backend encryption session)
* Hold both a public balance and an Unlink **shielded balance** on Monad testnet
* Pay an invoice **privately** (an Unlink withdraw — the funder stays hidden, gas paid by the relayer) or **publicly** (a normal, traceable transfer), toggled side by side

:::note
This is the **non-custodial (browser) Unlink model**: `account.fromMetaMask` derives the Unlink keys from the embedded EOA's signature. The backend only authenticates the Openfort session and talks to the Unlink admin API to register the user and mint short-lived authorization tokens — it never signs. See Unlink's [custody models](https://docs.unlink.xyz/custody-models).
:::

<HoverCardLink
  title="View Sample Code"
  subtitle="GitHub Repository"
  description="Complete source code for the private invoice payments recipe: Openfort embedded EOA + passkey, a non-custodial Unlink client, and an Express backend, on Monad testnet."
  href="https://github.com/openfort-xyz/recipes-hub/tree/main/private-payments"
  img={{
  src: "/img/icons/github-icon.svg",
  alt: "GitHub Icon",
  className: "rounded-none",
}}
  color="#333"
  external
/>

## How it works

The embedded wallet is created as an **EOA** so it can own and sign for an Unlink account. It funds a shielded balance (the Unlink faucet drips shielded USDC for the demo; in production you would deposit from the treasury EOA), then withdraws to the supplier for each invoice. A withdraw reveals the destination and amount but **not** which private account funded it.

```text
Openfort embedded EOA (payer treasury, Monad testnet)
        │  fund private balance          ← shielded USDC enters the pool
        ▼
   Unlink shielded pool  ───────────────────────────────────────────┐
        │  withdraw(amount → supplier)    ← relayer settles; the       │
        ▼                                   source account is hidden    ▼
  Shenzhen Supply Co (supplier EOA)                          (one withdraw per invoice)
```

| Step | What happens | Where |
| ------- | ----------------------------------------------------------- | ----------------------- |
| Sign in | Email OTP, then create an EOA secured by a passkey | `@openfort/react` (frontend) |
| Register | Derive the Unlink account from the EOA and register it | `account.fromMetaMask` → backend admin |
| Fund | Drip shielded USDC into the private balance | `client.faucet.requestPrivateTokens` |
| Pay (private) | Withdraw the invoice amount to the supplier — funder hidden | `useUnlink().withdraw` |
| Pay (public) | Plain ERC-20 `transfer` from the EOA — traceable, costs gas | `wagmi` `useSendTransaction` |

The backend exposes only two routes — `POST /api/unlink/register` and `POST /api/unlink/authorization-token` — both gated by the Openfort session token. The browser client attaches that bearer to those calls only; Engine requests carry their own short-lived authorization token.

## Getting started

:::note

* Node 18+ and `pnpm`
* An [Openfort account](https://dashboard.openfort.io) with **Monad testnet** and the **EOA** account type enabled
* An [Unlink account](https://dashboard.unlink.xyz) — a Monad-testnet project, an API key, and the token address its faucet is configured for
  :::

::::steps

### Set up your project

Clone the recipe and install dependencies:

```bash
pnpx gitpick openfort-xyz/recipes-hub/tree/main/private-payments openfort-private-payments
cd openfort-private-payments
```

The recipe is split into `backend/` (an Express service that holds the Unlink admin key) and `frontend/` (a Vite + React app — the payer console and supplier panel).

### 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_...`) and **Secret Key** (`sk_test_...`) from **Developers → API Keys**
3. Copy your **Shield Publishable Key** from the **Shield** section
4. Make sure **Monad testnet** is enabled and the **EOA** account type is available

### Configure your Unlink credentials

In the [Unlink Dashboard](https://dashboard.unlink.xyz):

1. Create a project for **Monad testnet**
2. Create an **API key** (server-only — copy it immediately)
3. From **Tokens**, copy the token address the faucet is configured for (verified with a USDC test token, 18 decimals)

### Configure your environment

Create `backend/.env.local`:

```bash
PORT=3020
CORS_ORIGINS=http://localhost:5181

OPENFORT_SECRET_KEY=sk_test_...

UNLINK_API_KEY=...
UNLINK_ENVIRONMENT=monad-testnet
```

Create `frontend/.env`:

```bash
VITE_OPENFORT_PUBLISHABLE_KEY=pk_test_...
VITE_OPENFORT_SHIELD_KEY=...
VITE_API_BASE_URL=http://localhost:3020
VITE_UNLINK_ENVIRONMENT=monad-testnet
VITE_UNLINK_TOKEN=0x...   # your Monad-testnet token address
VITE_MONAD_RPC_URL=https://testnet-rpc.monad.xyz
```

### Run it

Start the backend and frontend in separate terminals:

```bash
# Terminal 1 — backend
cd backend
pnpm install
pnpm dev

# Terminal 2 — frontend
cd frontend
pnpm install
pnpm dev
```

Open [http://localhost:5181](http://localhost:5181). Sign in with email, create a wallet with a passkey, **fund the private balance**, issue an invoice from the supplier panel, and **pay it privately** — the supplier sees the funds arrive with the source hidden. Flip the toggle to **Public** to contrast a normal, traceable transfer.

::::
