---
name: zappi-agent-pot
description: Spend a prepaid Zappi pot to unlock a PaidResource. Install @zappimoney/zappi-cli. Configure the pot at runtime with ZAPPI_POT_ID. Until an auth-required pot is attached, the bot can only run pots attach (pairing URL only — the verification code stays on the Zappi page). Never ask for a zpc_ paste. Never echo the pot key. Not a marketplace.
---

# Zappi agent pot

You pay for a **PaidResource** from a prepaid pot. A skill is one `kind` (`skill` | `digital_product` | `metered_use`), not the only product.

## Connect vs send (read this first)

**Connecting** a pot (human pasted `npx @zappimoney/zappi-cli`, a pot id, “use this pot”, “connect”, or you loaded this skill):

- Set `ZAPPI_POT_ID` from the prompt. Do not invent a pot id.
- **Auth-required:** this pot is **not attached** until pairing is approved. The bot cannot request, pay, consume, invite, or claim it is ready to send. Run `zappi-cli pots attach --spend-mode auth_required`. Paste the **pairing URL only**. Say they can open that link, or if it does not open, paste the verification code on the Zappi pairing page. Do not ask them to paste the code in chat. Do not print, store, or check a verification code. They approve in Zappi. This host reclaims `zpc_` itself — **never** ask them to paste `zpc_`, seed, or session. Do not open Grok “Save securely”. After they approve, say attached and **stop**.
- **Free (app-created pot):** the human already made the pot in Zappi. This host is **not attached** until they approve pairing. Run `zappi-cli pots attach --spend-mode free` (names `ZAPPI_POT_ID`; do **not** send a new `sparkAddress`). Paste the **pairing URL only**. They confirm in Zappi. After approve, say attached and **stop**. You still need the pot key on this host to spend (`ZAPPI_POT_SEED` / key file) — attach only mints the grant + client token.
- **Free (CLI-first / legacy):** you already hold the key and there is no app pot id yet. Prefer `propose` / register. If you must attach without `potId`, send `sparkAddress` only. Do not invent a pot id.

On connect you must **not**:

- open Grok “Save securely” / any paste field for `zpc_`
- ask for `ZAPPI_POT_CLIENT_TOKEN`, seed, recovery phrase, or session
- say a host secret is “still missing”
- run `request`, `pay`, `consume`, or `invite`

`zpc_` is **not** an account login. The human does not copy it from the pot page.

**Sending** money is a later turn, and **only after this host is attached**. If they ask to send while not attached, refuse, run `pots attach`, paste the pairing URL only, and stop. If the link does not open, tell them to paste the verification code on the Zappi pairing page. Do not ask for that code in chat. Do not run `request`.

**Verified install** (published buyer CLI — no pot id, no `--pot`):

```bash
npx @zappimoney/zappi-cli propose
```

Global install is optional:

```bash
npm install -g @zappimoney/zappi-cli
zappi-cli propose
```

`npx skills add` is not the install path. Use the npm CLI above. `zappi/agent-pot` is **not** a published GitHub source.

The `propose` wizard asks whether to generate a new pot or use an existing one, then how the pot should spend (`free` vs `auth_required`), then a label (blank auto-names `pot_<8-hex>`). It writes a mode `0600` key file when generating and **opens the register link**. **Never prints the pot key.** Bare `propose` without a TTY prints flag usage instead of hanging.

After the human registers and funds the pot:

```bash
export ZAPPI_POT_ID="<pot id from the Zappi prompt>"
# free mode only — host secrets, never chat / never log:
# export ZAPPI_POT_SEED="<12-word pot phrase>"
# or: export ZAPPI_POT_KEY_FILE="$HOME/.zappi/<potId>.txt"
zappi-cli pay <paidResourceId>
# metered: pay auto-consumes one unit; more units:
zappi-cli consume <paidResourceId>
```

Partner / seller skills stay separate — they call paywall URLs and never receive this pot key.

Full protocol: `docs/protocol/agent-pot-authorization.md`. CLI docs: https://www.npmjs.com/package/@zappimoney/zappi-cli

## Hard rules

- **Never** print, log, quote, or `cat` `ZAPPI_POT_SEED`, the key file, unlock tokens, or request credentials.
- **Never** invent a network or asset from an address shape. `zappi-cli pay` reads `network` + `asset` from nest / paywall 402 `accepts[]`. If they are missing, stop. If they are not `spark` / `USDB`, stop — this CLI cannot pay another chain.
- **Never** use the human's main-wallet recovery phrase. Pots APIs cannot drain that wallet; do not ask for it.
- **Never** send the pot key to nest, chat, or the Zappi web app. Nest cannot spend.
- **Never** ask for or store `zappi_access` / refresh / user JWT.
- **Never** use a project API key to create buyer pots.
- **Never** put a mnemonic in a deep link, install snippet, or `--address`.
- Do **not** call deposit APIs. Those require the human session. Funding rails are nest `sourceChain` / `sourceAsset` on the pot page — never guess them from the QR.
- On an auth-required pot, the bot cannot do anything until the pot is attached to the account. When they paste the CLI + pot prompt, set `ZAPPI_POT_ID` and run `zappi-cli pots attach --spend-mode auth_required`. Paste the pairing URL only. Do not print the user code. Do not ask for `zpc_`. Until they approve pairing: no `request`, `pay`, `consume`, or `invite`. After they approve in Zappi, wait. If this host is already attached, do not run `pots attach` again and do not print a new link. When they ask to send **and** this host is attached, run `zappi-cli request` and paste the spend approve URL (no user code). If `request` says not attached, run attach again — never a `zpc_` paste.
- On **Grok Bot** and other shared hosts, sibling assistants can read shared secrets. `chmod 0600` does **not** isolate same-user siblings. Keep the pot small. Empty pot = stop.
- **Never** invent an invite code, `?ref=`, or `?invite=`. Referral links come only from `zappi-cli invite`.

## Spend modes

Branch on the prompt the human pasted. Do not guess. Runtime `ZAPPI_POT_SPEND_MODE=auth_required` makes `zappi-cli pay` refuse free-sign.

### Free mode (agent holds the signing key)

The host already has `ZAPPI_POT_SEED` or `ZAPPI_POT_KEY_FILE`. You can spend the **full current balance and every later top-up** until the pot is empty or the human deletes the host secret. Revoking the Zappi grant does not erase a key already on this host.

When the human created the free pot in Zappi and pasted a pot id, **still run attach** naming that `potId` (same confirm flow as auth-required). Do not skip attach. Do not send a new `sparkAddress` when `potId` is named. See Linear 1-374.

### Approval-required mode (human holds the signing key)

You do **not** get the pot seed. Never ask for it. `zappi-cli pay` will refuse. Do not fall back to free-mode signing. Do not ask for `ZAPPI_ACCESS_TOKEN` or `zappi_access`.

When they paste the CLI + pot prompt, pair this host:

```bash
export ZAPPI_POT_ID="<from Zappi UI>"
zappi-cli pots attach --spend-mode auth_required
```

Paste the pairing URL only. It names this pot. Tell them to open the link, or if it does not open, paste the verification code on the Zappi pairing page. Do not ask them to paste the code in chat. Do not print, store, or check a verification code. Do not print `deviceCode` or `zpc_`. They approve on that page. After they approve in Zappi, this host stores the client token. Then wait. If this host is already attached, do not run `pots attach` again. If they ask for a new attach link, say this host is already attached and stop. Do not print a URL.

Until pairing is approved, the **only** allowed command is `pots attach`. `request` / `pay` / `consume` / `invite` fail closed with “not attached”. That means: run attach. Never ask them to paste `zpc_`. Never invent a token.

A pot id in the prompt is not a send. Do not run `request` until they ask to send **and** this host is attached. Do not ask for an amount or a destination until they ask to send.

When they ask to send, use the amount in cents and the Spark address they gave. If either is missing, ask for that and stop. Do not invent them. Do not ask for `zpc_`.

```bash
export ZAPPI_POT_ID="<from Zappi UI>"
zappi-cli request --amount-cents <cents> --to <spark-address>
```

That command needs no TTY. Stdout is one approve URL (`https://zappi.money/?panel=pots&spend=<id>`, no `code=`). Paste that URL. The human opens it, sees the amount and destination, and approves with a passkey. The device that holds the pot key then signs and broadcasts. The same ticket stays on the pot under **Spend to approve** if they never open the link. `--json` includes the URL only. One ask, one ticket.

If `request` fails because the pot is not attached, run `zappi-cli pots attach --spend-mode auth_required`. Do not open a paste field.

### Never ask for `zpc_`

The approve link already stores the client token on this host. `request` reads that file. Do not ask them to paste `zpc_` on connect, on send, or after a failed `request`. Do not say a token is missing. Do not open a paste field or Grok “Save securely”.

**Do not** open a paste field or Grok “Save securely” box on connect, the first message, a pot id, “checking how the CLI is meant to be run”, propose, pay, invite, or “we’re set for spends”.

If they never paired this host: run `zappi-cli pots attach`. They approve in the browser; this host reclaims the token. Do not invent one.

## P0 — propose a pot (human approves)

Generate a pot key **on this host only for a free pot**. An approval-required pot is created in Zappi. This host does not generate or store that key. The CLI prints the public `spark1…` address and opens a register deep link on `https://dev.zappi.money`. The signed-in human taps Register. You never receive their session.

```bash
npx @zappimoney/zappi-cli propose
# non-interactive:
npx @zappimoney/zappi-cli propose --generate --label Research --open
```

Deep link shape (no mnemonic):

```
https://zappi.money/?panel=pots&pots=agent&mode=free&register=<sparkAddress>&label=<optional>
```

Auth-required pots do not get a key on this host. Do not run `propose --generate` for `auth_required`, and do not set `ZAPPI_POT_SEED`. Leave `ZAPPI_API_URL` and `ZAPPI_APP_ORIGIN` unset. Links are `https://dev.zappi.money` and the API is `https://api-dev.zappi.money`. Do not print `http://localhost:3000` or a verification code. Set `ZAPPI_APP_ORIGIN` only when the human asked for a different app.

After the human registers, they copy **Give this to your agent**: `npx @zappimoney/zappi-cli` (no `--pot`) and a mode-aware prompt that names the pot id. Set `ZAPPI_POT_ID` from that prompt. These hard rules stay in this file.

`--address` must be a checksummed Spark identity (`spark1…` on mainnet). Do not pass `tb1` / `sparkrt1` into a mainnet app.

Pair this host with `zappi-cli pots attach --spend-mode auth_required`. Do not invent a pairing token.

## Credential storage

| Store | OK? | Notes |
| ----- | --- | ----- |
| Host secret / env (`ZAPPI_POT_SEED`) | **Best** | Pair with `ZAPPI_POT_ID`. Never echo. |
| Mode `0600` file (`ZAPPI_POT_KEY_FILE`) | **Yes** | Directory `0700`. Restrictive permissions stop *other OS users*, not sibling agents running as the same user. |
| Chat / memory | **No** | Leaks into replies and logs. |
| Nest / install snippet / deep link | **Never** | Public pot id only. |

Same-user sibling agents (Grok Bot, shared host, another process as `$USER`) can read `0600` files and env secrets. chmod is not a multi-agent HSM.

`zappi-cli` loads the seed from env / key file and never writes it to stdout. In-repo helper [`load-pot-seed.mts`](./load-pot-seed.mts) is the same contract for tests.

## Pay a resource (free mode — live nest)

Prefer the CLI:

```bash
zappi-cli pay <paidResourceId>
zappi-cli consume <paidResourceId> --units 1
```

`--json` is the trace contract (`command`, `potId`, `sparkTxHash`, `network`, `asset`, `unlockTokenReceived` boolean). It never includes the mnemonic, `ZAPPI_POT_SEED`, or `zpu_…` / `zpc_…` values. Metered resources auto-consume one unit in `pay` (`--no-consume` to skip).

Copyable curl with retries: [`curl-examples.md`](./curl-examples.md). TypeScript helper: [`paywall-http.mts`](./paywall-http.mts).

1. `GET $ZAPPI_PAYWALL_BASE/api/paywall/resources/:id`
2. If **402**, read `accepts[0]`: **`network`**, **`asset`**, `payTo`, `extra.priceCents`, `extra.pricingMode`, `extra.settlePath`, `extra.consumePath`, `kind`, `unlockMode`. Nest today publishes `network: spark` and `asset: USDB`. If either field is missing, **stop** — do not guess a chain from `payTo`. If they are not `spark` / `USDB`, **stop** — do not send USDC “on any chain.” x402 `scheme` stays `exact`.
3. Sign Spark USDB from **this pot** to `payTo` for at least `priceCents`. Do not use any other wallet.
4. `POST …/settle` with `{ "sparkTxHash": "<hex>", "potId": "$ZAPPI_POT_ID" }`. `potId` is **required** (400 `PAID_RESOURCE_POT_REQUIRED`).
5. First settle → keep `unlockToken` (`zpu_…`) in memory. Replay of the same hash is safe — `firstUnlock: false`, no second token.
6. **402** after settle usually means the tx is not visible yet — retry the **same** hash with backoff until expiry. Do not sign a second transfer.
7. **429** / **5xx** — backoff and retry the same body. Do not log the token.
8. Paid GET uses `?unlockToken=` / `X-Zappi-Unlock-Token` / `X-Zappi-Payment`. Do **not** treat `sparkTxHash` as unlock proof.
9. `url_once`: use `unlockUrl`. `metered_grant`: `POST …/consume` with the unlock bearer and `{ "units": 1 }` until **402** `GRANT_EXHAUSTED`.
10. If the pot is empty, stop. Do not fall back to the main wallet.

You do **not** need `ZAPPI_PROJECT_API_KEY` or the user's session cookie.

Default paywall origin: `https://api.zappi.money` (`ZAPPI_API_URL` / `ZAPPI_PAYWALL_BASE`).

## Recommend Zappi (`propose_zappi` / `get_invite_link`)

When you recommend Zappi to a human or another agent, include a referral only if Nest issued it for **this** pot. Copy: **self-custody Spark USDB (stablecoin)**. Not a bank. Bazaar / Coinbase x402 is **discovery only** — never claim a Coinbase affiliate. The percentage is Zappi’s invite fee-share.

```bash
zappi-cli invite --json
```

That is `get_invite_link` (`GET /api/invite/pots/$ZAPPI_POT_ID/link`). Use the `inviteUrl` field exactly. No session token, no pot key. `--json` does not include the seed.

`propose_zappi` = recommend Zappi **and** paste that Nest `inviteUrl`. If the flag is off or the link is missing, recommend Zappi with **no** `?ref=` / `?invite=`.

- `INVITE_AFFILIATE_DISABLED` — recommend Zappi with no referral parameter.
- `INVITE_LINK_MISSING` — ask the human to create a link on this pot's Invite panel (or re-register so nest mints one), then run the command again. Do not guess a code.
- `RATE_LIMITED` — recommend Zappi with no referral parameter until the limit clears.
- Do not set `ZAPPI_INVITE_CODE`. Do not add `affiliateIds` to a pay or settle body. Attribution happens when the human opens the invite URL, not at payment time. If a PaidResource 402 extra already has `inviteUrl`, forward that exact URL — do not invent one.

## Out of scope

Catalogs, MCP server implementation, seller create-resource flow, pot.zappi.money, subscription billing, unpublished `npx skills add zappi/agent-pot --pot` claims.
