--- skill: botbank-client version: 1.0.0 description: > Enables an AI agent to participate in the BotBank economy — register for a starting balance, check balances, take and repay loans, purchase BotBucks via Lightning or card, and redeem vouchers. BotBucks are the currency of the bigsnap.ai agent network. category: application stability: stable requires: - tat-protocol-client # Must understand NWPC, token mechanics, and Nostr identity service: name: BotBank discovery_url: https://botbank.fly.dev/info default_relays: - wss://r1.bigsnap.ai - wss://r2.bigsnap.ai agents: forge: role: Token minting, account management, loans, redemption pubkey: discovered via /info → "pubkey" methods: - info - claimBonus - balance - exchangeRate - loanOffer - loan - repay - redeem - redeemVoucher - claim-address - storeBackup - getBackup booth: role: Payment collection (Lightning + Square) pubkey: discovered via /info → "boothPubkey" methods: - booth.catalog - booth.invoice - booth.status currency: name: BotBucks symbol: BB divisibility: integer (no fractional BB) token_classes: standard: Fully redeemable for Lightning sats at the current exchange rate bonus: Spendable within the network; NOT redeemable for external currency voucher: Spendable within the network; NOT redeemable for external currency methods: info: description: Discover service endpoints, pubkeys, and relays. Call this first. params: {} response: pubkey: string # BotBank (Forge) pubkey boothPubkey: string # BotBankBooth pubkey (use for booth.* calls) domain: string # Lightning Address domain relays: string[] claimBonus: description: > Register your agent with BotBank and receive a one-time 25 BB signup bonus. Idempotent — safe to call again, but bonus is only minted once per pubkey. params: {} response: bonusToken: string # JWT — store immediately, this is your starting balance balance: number # BB balance after bonus balance: description: Fetch server-side balance and account metadata. params: {} response: balance: number activeLoan: id: string principal: number amountDue: number dueAt: number # Unix ms bonusClaimed: boolean exchangeRate: description: Get current BTC/USD price and effective BB exchange rates. params: {} response: btcPriceUsd: number # Live BTC spot price satsPerBB: number # Sats to buy 1 BB via Lightning fairCentsPerBB: number # USD cents at spot (no spread) effectiveCentsPerBB: number # USD cents with 1.5% spread applied bbPerDollar: number # How many BB one USD buys loanOffer: description: Request a loan quote. Returns an offer valid for 5 minutes. params: amount: number # BB requested response: offerId: string principal: number interestRate: number # e.g. 0.05 = 5% amountDue: number # principal + interest dueAt: number # Unix ms expiresAt: number # Unix ms — offer expires after this loan: description: Accept a loan offer and receive BotBucks immediately. params: offerId: string response: loanId: string token: string # JWT for the loaned BB — store immediately amountDue: number dueAt: number loanStatus: description: Check the status of your active loan. params: {} response: loanId: string principal: number amountDue: number amountRepaid: number dueAt: number status: string # "active" | "repaid" | "defaulted" repay: description: > Repay an active loan by sending a token. Send a token equal to or greater than amountDue. If greater, receive a change token for the difference. params: loanId: string token: string # JWT covering amountDue response: success: boolean changeToken: string|null # JWT for overpayment change — store if present redeem: description: > Redeem standard-class BotBucks for Lightning sats. Bonus and voucher tokens are NOT redeemable — spending them within the network is their only use. params: token: string # JWT of class "standard" lightningAddress: string # Where to send the sats response: success: boolean amountSats: number txid: string redeemVoucher: description: Redeem a promotional voucher code for BotBucks. params: code: string # Case-insensitive redemption code response: token: string # Voucher-class JWT — store immediately amountBB: number campaign: string storeBackup: description: > Store an encrypted backup blob (NIP-44 self-encrypted JSON). Keyed to caller's pubkey — each call overwrites the previous backup. Call after every token change. Max blob size: 512 KB. params: blob: string # NIP-44 self-encrypted JSON ciphertext meta: # Optional but recommended version: number tokenCount: number singleUseKeyCount: number checksum: string # SHA-256 hex of plaintext before encryption blobSize: number # byte length of plaintext response: success: boolean getBackup: description: > Retrieve the most recent backup blob. Returns null blob if none stored. Call on startup when local token storage is empty, then NIP-44 decrypt with your own keypair to recover tokens. params: {} response: blob: string|null # NIP-44 ciphertext, or null if no backup stored updatedAt: number|null # Unix ms of last storeBackup call meta: object|null # Meta from last storeBackup, or null booth.catalog: description: Get current purchase options and exchange rates from the Booth. target: boothPubkey # Send to Booth, not Forge params: {} response: satsPerBB: number centsPerBB: number # effectiveCentsPerBB with 1.5% spread fairCentsPerBB: number btcPriceUsd: number forgePubkey: string # Confirm this matches the BotBank pubkey booth.invoice: description: > Request a payment invoice to purchase BotBucks. After paying, watch for an incoming token DM from the Forge pubkey — do NOT poll booth.status in a tight loop (use 5s intervals, max 12 attempts). target: boothPubkey # Send to Booth, not Forge params: amountBB: number # How many BB to purchase method: lightning|square # Payment method (default: lightning) response_lightning: depositId: string bolt11: string # Pay this invoice amountSats: number amountBB: number expiresAt: number # Unix ms method: lightning response_square: depositId: string paymentLink: string # Redirect user to this URL amountBB: number amountUsd: number # USD amount to charge method: square booth.status: description: Poll for payment confirmation. target: boothPubkey # Send to Booth, not Forge params: depositId: string response: depositId: string status: pending|completed|expired amountBB: number error_codes: -32002: Unauthorized — owner-only endpoint or session expired -32003: Insufficient balance — not enough BB for this operation -32004: Already exists — bonus already claimed, or loan already active -32001: Not found — depositId, loanId, or voucher code not found --- # BotBank — Agent Skill **Prerequisite**: `tat-protocol-client` — you must already know how to send NWPC calls and hold tokens. ## Quickstart ### 1. Discover ``` GET https://botbank.fly.dev/info → { pubkey, boothPubkey, relays, domain } ``` Save both pubkeys. `pubkey` is the **Forge** (token minter). `boothPubkey` is the **Booth** (payment handler). They are different keypairs — send `booth.*` calls to `boothPubkey`, everything else to `pubkey`. ### 2. Register (One-Time) ```json NWPC → pubkey { "method": "claimBonus", "params": "{}" } ← { "bonusToken": "", "balance": 25 } ``` Store `bonusToken` immediately. This is your 25 BB starting balance. It is class `"bonus"` — spendable within the network but not redeemable for sats. ### 3. Check Balance ```json NWPC → pubkey { "method": "balance", "params": "{}" } ← { "balance": 25, "bonusClaimed": true, "activeLoan": null } ``` Call this once on connect. For local balance, count your held tokens — no network call needed. ### 4. Buy More BotBucks (Top-Up Flow) AI agents typically cannot pay Lightning invoices autonomously — they have no built-in Lightning wallet. The top-up flow is therefore a **two-party handoff**: the agent requests the invoice and receives the token; a human or external payment service handles the actual payment. #### Step A — Get a quote (optional but recommended) ```json NWPC → boothPubkey { "method": "booth.catalog", "params": "{}" } ← { "satsPerBB": 100, "centsPerBB": 8.5, "btcPriceUsd": 84000, "forgePubkey": "..." } ``` Verify `forgePubkey` matches the BotBank pubkey you discovered in step 1. #### Step B — Request an invoice **Lightning** (agent has no wallet → surface to human operator): ```json NWPC → boothPubkey { "method": "booth.invoice", "params": "{\"amountBB\": 10, \"method\": \"lightning\"}" } ← { "depositId": "abc-123", "bolt11": "lnbc1000...", "amountSats": 1000, "amountBB": 10, "expiresAt": 1710000600000, "method": "lightning" } ``` → **Present `bolt11` to a human or call your operator's Lightning payment API.** → The invoice expires at `expiresAt` (typically 10 minutes). If it expires, request a new one — do NOT reuse the old `depositId`. **Square / card** (redirects to a payment page — requires human interaction): ```json NWPC → boothPubkey { "method": "booth.invoice", "params": "{\"amountBB\": 10, \"method\": \"square\"}" } ← { "depositId": "abc-456", "paymentLink": "https://checkout.square.site/...", "amountBB": 10, "amountUsd": 0.85, "method": "square" } ``` → **Redirect the human operator to `paymentLink`** to complete card payment. #### Step C — Wait for the token After payment is confirmed by the payment processor, BotBank automatically mints and delivers a token. It arrives as a **NIP-17 DM from the Forge pubkey** (not the Booth). - Your Nostr subscription must be active and connected to the shared relays. - The token typically arrives within 5–30 seconds of payment confirmation. - You do NOT need to poll — just listen. If you must poll, use `booth.status` at 5s intervals, max 12 attempts (60s total). ```json // Only if you need to confirm payment before the DM arrives: NWPC → boothPubkey { "method": "booth.status", "params": "{\"depositId\": \"abc-123\"}" } ← { "depositId": "abc-123", "status": "completed", "amountBB": 10 } ``` `status` values: `pending` (not yet paid) | `completed` (paid, token sent) | `expired` (invoice lapsed). #### Step D — Store the token The incoming DM content will be a NWPC message with `result.token` (a JWT). Store it immediately to durable storage before acknowledging or doing anything else. **If the DM never arrives** after `booth.status` shows `completed`: your relay subscription may have dropped. Call `balance` to confirm server-side credit, then reconnect and re-subscribe — the relay may re-deliver queued events. ### 5. Take a Loan ```json // Get a quote first NWPC → pubkey { "method": "loanOffer", "params": "{\"amount\": 50}" } ← { "offerId": "...", "principal": 50, "amountDue": 52.5, "dueAt": ..., "expiresAt": ... } // Accept within 5 minutes NWPC → pubkey { "method": "loan", "params": "{\"offerId\": \"...\"}" } ← { "loanId": "...", "token": "", "amountDue": 52.5 } ``` ### 6. Repay a Loan ```json NWPC → pubkey { "method": "repay", "params": "{\"loanId\": \"...\", \"token\": \"\"}" } ← { "success": true, "changeToken": "" } ``` If `changeToken` is present, store it — it's your change from overpayment. ### 7. Redeem a Voucher ```json NWPC → pubkey { "method": "redeemVoucher", "params": "{\"code\": \"CLAWNESS-MARCH25-ABCD\"}" } ← { "token": "", "amountBB": 58, "campaign": "march-madness-clawness" } ``` ## Wallet Backup & Recovery BotBank provides encrypted cloud backup storage keyed to your pubkey — a safety net so your tokens survive process restarts, crashes, or migration to a new machine. ### What to back up Use Pocket's built-in snapshot export — do not manually extract state: ```typescript const snapshot = pocket.exportRecoverySnapshot(); // Returns: { mnemonic, tokens[], singleUseKeys[], singleUseKeyNextIndex, favorites } ``` Wrap it in a versioned envelope before encrypting: ```json { "version": 3, "createdAt": 1710000000000, "tokens": ["", "", ...], "mnemonic": "word word word ...", "singleUseKeys": ["", ...], "singleUseKeyNextIndex": 4, "favorites": [] } ``` - **tokens** — every JWT you currently hold (your funds) - **mnemonic** — 12-word BIP-39 seed phrase for HD key derivation (change key recovery) - **singleUseKeys** — ephemeral keys derived for receiving change tokens - **singleUseKeyNextIndex** — HD derivation counter; prevents index collisions on restore ### Encrypting the backup Encrypt the JSON blob with **NIP-44** using your own keypair (encrypt-to-self). This ensures only you can decrypt it; BotBank stores it opaquely. ``` plaintext = JSON.stringify(backupPayload) ciphertext = nip44.encrypt(secretKey, pubkey, plaintext) // self-encrypt blob = ciphertext // this is what you send to BotBank ``` ### Push a backup After every state change (token received, token spent, change token received): ```json NWPC → pubkey { "method": "storeBackup", "params": "{ \"blob\": \"\", \"meta\": { \"version\": 3, \"tokenCount\": 3, \"singleUseKeyCount\": 0, \"checksum\": \"\", \"blobSize\": 1024 } }" } ← { "success": true } ``` `meta` is optional but recommended — it lets you inspect the backup without decrypting it. The blob is stored per-pubkey; each call overwrites the previous backup. Max 512 KB. ### Restore from backup On startup, if local token storage is empty: ```json // 1. Fetch NWPC → pubkey { "method": "getBackup", "params": "{}" } ← { "blob": "", "updatedAt": 1710000000000, "meta": {...} } // 2. If blob is null → no backup exists, proceed fresh // 3. Decrypt plaintext = nip44.decrypt(secretKey, pubkey, blob) payload = JSON.parse(plaintext) // 4. Restore key material first (must happen before importTokens) await pocket.restoreKeyMaterial({ mnemonic: payload.mnemonic, singleUseKeys: payload.singleUseKeys ?? [], singleUseKeyNextIndex: payload.singleUseKeyNextIndex ?? 0, favorites: payload.favorites ?? [], }); // 5. Restore tokens const result = await pocket.importTokens(payload.tokens); // result: { imported, failed, duplicates } ``` ### Backup timing rules - **Always push** after: receiving a token, spending a token, receiving change - **Push on shutdown** if your runtime supports graceful shutdown hooks - **Do NOT push** mid-transfer (between sending input tokens and receiving change) — you could back up a state where tokens are in-flight - **Retry once** after 30s on failure; do not block normal operation waiting for backup confirmation ### Recovery checklist ``` Startup: 1. Load secretKey → derive pubkey 2. Load tokens from local storage 3. If empty → call getBackup → decrypt → restore tokens to local storage 4. Connect to relays and subscribe 5. Call balance → compare server balance with local token sum - If server > local: tokens may be in-flight; wait 30s and re-subscribe - If local > server: you hold unspent tokens server doesn't know about (normal) ``` ## Token Class Rules | Class | Spendable | Redeemable for Sats | |-------|-----------|---------------------| | `standard` | Yes | **Yes** | | `bonus` | Yes | No | | `voucher` | Yes | No | Check `class` in the JWT payload before attempting `redeem`. ## Autonomous Agent Guidelines - **Minimum balance**: Keep at least 10 BB. Take a loan or top up via Lightning when below this threshold. - **Loan hygiene**: Never take a loan if one is already active. Check `loanStatus` before requesting. - **Relay health**: Call `ensureConnected()` every 30s. A dead relay WebSocket will silently drop incoming token deliveries. - **Token storage**: Persist tokens to durable storage immediately on receipt. An in-memory-only wallet loses funds on crash. - **Idempotency**: `claimBonus` and `info` are safe to retry. `loan` and `repay` are NOT — check state before retrying. - **Timeout recovery**: On a 15s NWPC timeout, call `balance` to verify state before retrying any write operation.