@zappimoney/zappi-cli
v0.4.0
Published
Buyer CLI for Zappi agent pots — propose register links, pay ResourcePaywall (402/settle), consume metered grants.
Readme
@zappimoney/zappi-cli
Buyer CLI (zappi-cli) for prepaid Zappi agent pots. Propose a register deep link, pay a nest PaidResource (HTTP 402 → sign USDB from the pot → settle), then consume metered grant units.
Install
npm (recommended):
npm install -g @zappimoney/zappi-cli
zappi-cli --helpnpx (no install):
npx @zappimoney/zappi-cli --helpFrom GitHub (global):
npm i -g github:armmosikyan66/zappi-cli
zappi-cli --helpClone + link (local development):
git clone https://github.com/armmosikyan66/zappi-cli.git
cd zappi-cli
npm install
npm link
zappi-cli --helpRequires Node.js ≥ 20.9. The package compiles TypeScript to dist/ on install (prepare); the zappi-cli bin runs compiled JS via bin/cli.js (no experimental flags).
Commands
Global flag: --json prints machine-readable JSON (never includes pot seeds, pot client tokens, unlock tokens, or a guest secret). need_choice and a refusal exit 0. allowance_required and an uncertain allowance pull exit 1.
A person or an agent uses these commands to create a pot, deposit, and send. zappi-cli login saves the session. zappi-cli guest starts a free pot without an account and does not print the secret.
| Command | What it does |
| --- | --- |
| whoami | Account email, or guest mode from ~/.zappi/agent.json |
| login / logout | Save or revoke this login |
| guest | Start a free pot without an account |
| guest claim --pot <id> | Move that pot onto the account |
| pots | List pots on this login. --origin or --spend-mode, or a project API key, keeps the wallet list |
| pots create | Create a pot. The only question is the name: --label or --skip-name. There is no spend mode and no per-pot limit |
| pots connect --pot <name-or-id> | Connect a free pot. --default moves the default |
| pots disconnect --pot <name-or-id> | Remove it from this login |
| pots pair | Approve URL for an auth-required pot |
| pots balance | Balance |
| pots challenge --address <spark> | Address challenge |
| deposit rails / deposit assets / deposit networks / deposit address | Deposit catalog and address |
| pots spender [pubkey] | Register the allowance spender. Omit the key to run zappi-allowance-spender pubkey |
| pots send | Send. --amount-cents, --to, optional --asset, --chain, --memo, --pot, --idempotency-key, --no-wait. A signed allowance pulls with zappi-allowance-spender; otherwise the result is an approve URL |
| pots send-status <requestId> | Read one send |
pots connect is not pots attach. pots send is not send. deposit address is not deposit-address.
propose on a terminal asks before it generates or registers: existing vs new, spend mode, Spark network (unless SPARK_NETWORK is set), app origin (unless ZAPPI_APP_ORIGIN / NEXT_PUBLIC_SITE_URL / --origin is set), and a label. A blank label asks you to confirm auto pot_<unique> or type a custom name. Flags: --mode free|auth_required, --origin, --label.
zappi-cli login # optional: sign this terminal into Zappi
zappi-cli propose # on the agent host (wizard)
zappi-cli pay <resourceId> # free pot: agent signs after the pot is funded
zappi-cli request --amount-cents 100 --to <spark-address>
zappi-cli consume <resourceId> [--units N]
zappi-cli invite # Nest invite URL for this pot
zappi-cli sign <requestId> # local signer: human confirms in the terminal
zappi-cli sign <requestId> --yes # agent with a shell| Command | What it does |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| login | Optional, human. Opens the Zappi sign-in screen (email or passkey). Saves a user session in ~/.zappi/credentials.json (0600). Wallet commands use it when ZAPPI_ACCESS_TOKEN is unset. Never prints the session. Propose and pay do not need it. |
| whoami | Prints the email of the saved login. With a guest file and no login, reports guest mode and does not print the secret. |
| logout | Revokes that session and deletes the credentials file and the guest file. |
| propose | Host setup on the agent host. Wizard: existing or generate → spend mode → network → app origin → label (blank confirms auto pot_<unique>) → opens the register link. Writes a mode 0600 key file when generating. Never prints the pot key. |
| pay | Free pot. GET resource → 402 → accepts[0].network + asset must be spark / USDB → sign pot USDB → settle { sparkTxHash, potId }. Other rails fail closed. Metered auto-consumes one unit (--no-consume to skip). ZAPPI_POT_SPEND_MODE=auth_required refuses this and points at request. |
| request | Auth-required pot. No TTY. POST …/spend-requests with x-zappi-pot-client and prints only the approve URL (?panel=pots&spend=, no code=). --json includes that URL and never the zpc_ token. Does not sign. |
| consume | Agent spend. POST …/consume with X-Zappi-Unlock-Token and { units } (default 1). |
| invite | Recommend Zappi. GET /api/invite/pots/$ZAPPI_POT_ID/link (no session, no pot key). Prints the Nest invite URL. Fails closed if the flag is off or the pot has no code — it never invents one. |
| sign | Local signer. Spawns zappi-signer sign <requestId>. Without --yes, a human confirms in the terminal. |
| signer-status | Local signer. Spawns zappi-signer status <requestId>. |
| provision, prove-address, policy, backup, restore | Local signer. Same subcommand, plus your remaining arguments. |
Local signer
sign, signer-status, provision, prove-address, policy, backup, and restore start the zappi-signer binary and forward the terminal (stdin, stdout, and stderr) and the child exit code. This CLI does not sign, does not ask for a mnemonic, and does not drop a second sign for the same request. The signer confirms on its own terminal, and idempotency is the signer's job.
zappi-cli sign <requestId> [--yes] [--signer <path>]
zappi-cli signer-status <requestId> [--signer <path>]
zappi-cli provision [args…]
zappi-cli prove-address [args…]
zappi-cli policy [args…]
zappi-cli backup [args…]
zappi-cli restore [args…]The binary is --signer <path>, otherwise ZAPPI_SIGNER_BIN, otherwise zappi-signer on PATH. If that file is missing, the command prints a short hint that names ZAPPI_SIGNER_BIN and exits non-zero. --signer is not passed to the child. --yes is passed only when you passed it.
Without --yes, a human confirms in the terminal. An agent with a shell passes --yes. A host without a shell shows zappi sign <requestId>.
The propose wizard
npx @zappimoney/zappi-cli proposeDo you already have a pot, or should this host generate a new one?
◉ Generate a new pot (create a fresh key on this host)
○ Use an existing pot (I already registered one)
(↑/↓ to move, ENTER to select)With no flags and no app-origin env, the same run then asks spend mode, app origin, and label before it generates a key or opens the register link. Spark is MAINNET.
- Radio-circle menu: the selected row shows a green ◉, others a hollow
○; ↑/↓ move, ENTER confirms — Generate a new pot is the default. Digits (1/2) andj/kalso work. (SetNO_COLOR=1to disable the green.) - Spend mode — Auth not required (
free) vs Auth required (auth_required). - Network — MAINNET. Unset
SPARK_NETWORKis MAINNET. Any other value is rejected. New pots use Spark account index 1. - App origin — staging
https://dev.zappi.money(pair withhttps://api-dev.zappi.money), productionhttps://zappi.money, localhttp://localhost:3000, or a custom http(s) URL. Asked only whenZAPPI_APP_ORIGIN,NEXT_PUBLIC_SITE_URL, and--originare all unset. Staging is the first row and the silent default. - Existing pot → paste the public pot address from your agent (validated for the network you chose; blank cancels).
- Label — both modes. A name you type is used as-is. A blank answer asks you to confirm auto
pot_<8-hex>or enter a custom name. Auto, or a custom name left blank, acceptspot_<unique>. - Key file prompt: blank uses
~/.zappi/…txt. If you paste a folder (e.g.~/Documents/zappi/packages), the CLI writes the.txtinside that folder instead of crashing withEISDIR. - The browser step opens the origin you chose (ENTER / auto-open /
cto copy). Ctrl+C quits cleanly. - Bare
proposewithout a terminal (no TTY) prints flag usage instead of hanging. - Flags (
--generate,--address,--mode,--origin,--label, …) are for CI. When those flags fully specify the run, nothing is prompted. A non-TTY flag run keeps the published defaults (free, MAINNET,https://dev.zappi.money, APIhttps://api-dev.zappi.money) and does not hang.
Flow (propose → fund → pay or request → consume)
- Propose — run on the agent host. The CLI generates the pot key there and opens a register URL. Human signs in to Zappi and taps Register.
- Fund — human funds the pot in the Zappi app. The CLI does not call deposit APIs.
- Pay (free pot) — after
ZAPPI_POT_ID+ZAPPI_POT_SEED(or key file) are set, pay a PaidResource. Nest returns an unlock bearer once on first settle. The CLI withholds that token from stdout/logs. Metered resources auto-consume one unit in the samepay(opt out with--no-consume). - Request (auth-required pot) —
ZAPPI_POT_CLIENT_TOKEN+zappi-cli requestprints one approve URL. The human approves in Zappi. This command does not sign and does not ask for a session token. - Consume — further metered units on a free pot:
zappi-cli consume <id>withZAPPI_UNLOCK_TOKENset as a host secret.
Exact (url_once) resources stop after settle; use unlockUrl when nest returns it. Empty pot = stop. Do not fall back to the main wallet.
Fail closed (agents)
If a CLI command fails (env / apiUrl mismatch, not attached, etc.): stop and report. Forbidden: reading ~/.zappi/pot-client-*, inventing fetch/curl/Node with zpc_, or calling Nest with attach secrets. Pot balance for attached pots: zappi-cli balance --pot <id> (CLI sends x-zappi-pot-client to Nest). Use zappi-cli doctor to compare saved login apiUrl vs the resolved API — no secrets printed.
Environment
| Variable | Role |
| -------------------- | --------------------------------------------------------------- |
| ZAPPI_POT_ID | Required for pay and request. Public pot id from the Zappi prompt. |
| ZAPPI_POT_CLIENT_TOKEN | zpc_ for request (host secret), or ~/.zappi/pot-client-*.txt after attach. Missing = not attached. Never a CLI flag. |
| ZAPPI_ATTACH_DEVICE_CODE | Attach reclaim secret (RFC 8628 device_code) from pots attach create (host secret). Wins over ~/.zappi/attach-device-<requestId>.txt. Never print. |
| ZAPPI_POT_SEED | Preferred pot spend key for free pay (host secret). |
| ZAPPI_POT_KEY_FILE | Fallback mode-0600 key file if the seed env is unset. |
| ZAPPI_API_URL | Nest origin. Default https://api-dev.zappi.money. Local: http://localhost:3011. Production: https://api.zappi.money. |
| ZAPPI_PAYWALL_BASE | Optional paywall origin override (wins over ZAPPI_API_URL). |
| ZAPPI_UNLOCK_TOKEN | Unlock bearer for consume (preferred over --unlock-token). |
| ZAPPI_POT_SPEND_MODE | Runtime spend mode. auth_required refuses pay and tells the agent to run request. Unset / free for agent-held pots. |
| ZAPPI_APP_ORIGIN | Web origin for links the human opens. Default https://dev.zappi.money. Follows the API when unset. Local: http://localhost:3000. Production: https://zappi.money. |
| SPARK_NETWORK | MAINNET (default). Any other value is rejected. Account index is 1. |
| ZAPPI_PROJECT_API_KEY | Project API key for server-to-server wallet routes (balance, pots, withdraw, …). |
| ZAPPI_ACCESS_TOKEN | User access JWT. Wins over zappi-cli login. |
| ZAPPI_CREDENTIALS_FILE | Override for the login file. Default ~/.zappi/credentials.json (0600). |
| ZAPPI_CLI_GUEST_FILE | Override for the guest secret file. Default ~/.zappi/agent.json (0600). Never printed. |
| ZAPPI_COOKIE | Optional cookie header forwarded for session auth (e.g. zappi_access=…). |
| ZAPPI_USER_AGENT | Optional user-agent forwarded for session auth. |
| ZAPPI_SIGNER_BIN | Path to the zappi-signer binary. Used when --signer is omitted. Unset uses zappi-signer on PATH. |
--unlock-token exists for one-off calls. Scripts should use ZAPPI_UNLOCK_TOKEN so the bearer does not land in ps / shell history. Env wins when both are set.
Staging dogfood
Unset ZAPPI_API_URL and ZAPPI_APP_ORIGIN already use this pair. http://dev.zappi.money redirects to https://dev.zappi.money.
export ZAPPI_POT_ID='<pot id>'
# set ZAPPI_POT_SEED / ZAPPI_POT_CLIENT_TOKEN / ZAPPI_UNLOCK_TOKEN as host secrets — never echo / never commit
npx @zappimoney/zappi-cli propose --generate --label Staging --mode free
# SPARK_NETWORK unset is MAINNET. Account index is 1.
# human registers + funds in the staging app
zappi-cli pay '<paidResourceId>'
# metered: pay already consumed one unit; more:
zappi-cli consume '<paidResourceId>' --units 1Local nest is explicit: ZAPPI_API_URL=http://localhost:3011 and ZAPPI_APP_ORIGIN=http://localhost:3000. Production is ZAPPI_API_URL=https://api.zappi.money and ZAPPI_APP_ORIGIN=https://zappi.money. Set those two together.
CI vs live dogfood
- CI (
npm test): mocked HTTP + mocked Spark. Covers 402 → settle{ sparkTxHash, potId }→ consume, empty pot fail-closed, 402network/assetrequired (refuse non-spark/USDBbefore sign), secret redaction (including BIP-39),ZAPPI_POT_SPEND_MODE=auth_requiredrefusing CLI free-sign, andrequestprinting a bare approve URL (nocode=, nozpc_). No live Spark spend / no paywall network. - Staging dogfood (this section): human registers + funds in the app, then
pay/consumeagainstapi-dev. Never commit seeds. --jsonis the CLI trace contract:command,potId,sparkTxHash,network,asset,unlockTokenReceived(boolean).request --jsonaddsapproveUrl,requestId,amountCents, anddestinationAddress.pots attach/attach-statusJSON usesdeviceCodeReceived/potClientTokenReceivedbooleans — never rawdeviceCodeorzpc_. It never includes the mnemonic,ZAPPI_POT_SEED, orzpu_…/zpc_…values.
Install has no --pot flag. Runtime pot id is ZAPPI_POT_ID after the human registers.
Develop
npm install
npm test # tsc → node --test dist/ (mocked HTTP + Spark)
npm run buildpackage-lock.json resolves @zappimoney/zappi-sdk from the npm registry (0.3.8). A clean checkout does not use a sibling ../zappi-sdk link.
Labels: active pot names are unique per user (case-insensitive), not across the app. Nest returns 409 AGENT_POT_LABEL_EXISTS if that user already has the name.
Pot attach (device code / 1-203)
zappi-cli pots attach --pot <potId> --spend-mode auth_required creates a pending pairing for that pot (1-554).
- Pot is required for auth-required: pass
--pot <potId>or setZAPPI_POT_ID. If both are set and differ, the command fails closed. The pot id is always sent to Nest aspotId. - Nest must confirm the pot: the create response (
approveUrlpot=or arequestedPotIdfield) must name the same pot, or no link is printed and no device code is stored. Polling and reclaim fail closed if the request or the approval names a different pot; the client token is then not stored.
Nest returns deviceCode once to the agent host and an approveUrl that carries only the request id. The verification code is not in that response and not in the URL.
- Host secret: the CLI writes
deviceCodeto~/.zappi/attach-device-<requestId>.txt(mode0600). You can also setZAPPI_ATTACH_DEVICE_CODEfor reclaim. Never echo / never commit. - Human: open/print
approveUrlonly. - Poll: public
GET …/pots/attach/:requestIdis status-only (neverzpc_). - Reclaim: after
approved, the CLI callsPOST …/pots/attach/:requestId/credentialswith headerX-Zappi-Device-Codeand storeszpc_under~/.zappi/pot-client-<requestId>.txt(0600). SetZAPPI_POT_CLIENT_TOKENfrom that file. - Output: pretty/plain show
zpc_… (withheld). JSON usesdeviceCodeReceived/potClientTokenReceivedbooleans — never raw secrets. attach-status <requestId>reclaims the same way when a device code is available from env or file.
Reclaim path locked to Nest tip df7aafc / zappi-nest#82: POST …/pots/attach/:requestId/credentials + X-Zappi-Device-Code.
Hard rules
- Never print, log,
echo, orset -xZAPPI_POT_SEED,ZAPPI_POT_CLIENT_TOKEN,ZAPPI_ATTACH_DEVICE_CODE, the key file, attach-device / pot-client files under~/.zappi/, orZAPPI_UNLOCK_TOKEN. - Never ask for
ZAPPI_ACCESS_TOKEN,zappi_access, azpc_paste, the pot seed, a verification code, or a recovery phrase. Pair withpots attach(URL only). Paste therequestapprove URL for a send. - Until an auth-required pot is attached,
request,pay,consume, andinvitefail closed. The only bot command ispots attach. - Never pass a recovery phrase as
--addressor put one in a deep link. - Nest never holds the pot key. Cap v1 is an empty pot.
- Do not invent a payment chain from an address. Paywall 402 must include
accepts[0].networkandaccepts[0].asset; this CLI only paysspark/USDB. - Do not invent an invite code.
zappi-cli inviteprints a Nest URL or fails closed. - Seller / project API is out of scope for this package.
- Create, deposit, and send for this login are
pots create,deposit address, andpots send. A chat can still use the remote MCP server.propose,pay,request,pots attach, andsendstay for host setup and the wallet.
