# Examples Source: https://docs.handcash.io/brc-wallet/examples Download Transaction Bounce and the wallet demo **Beta — BRC wallet only.** These examples talk to HandCash Desktop or Mobile over [BRC-100](https://brc.dev/100). They are not Connect or Wallet API samples. Copyable request shapes live on the task pages: [payments](/brc-wallet/payments), [collectables](/brc-wallet/items), [tokens](/brc-wallet/tokens), and [signing](/brc-wallet/signing). The two projects below are the downloadable apps. ## Transaction Bounce Sends 1,000 satoshis to the app and spends that exact output back to you, minus the network fee. The page calls the local wallet bridge. The refund API lives in the Worker. Live app: [brc-cloud.bcryderman.workers.dev/tx-bounce](https://brc-cloud.bcryderman.workers.dev/tx-bounce) Download tx-bounce.zip The archive is the page, the refund API, and its tests. `TX_BOUNCE_PRIVATE_KEY` is a Worker secret and is not included. ## Wallet demo Connects to HandCash on this device and runs one application lifecycle: identity, encryption, a 1,000-satoshi bounce, two JPEG inscriptions, and a BSV-21 mint. Wallet calls stay on the local bridge. Live app: [brc-cloud.bcryderman.workers.dev/wallet-demo](https://brc-cloud.bcryderman.workers.dev/wallet-demo) Download wallet-demo.zip The archive is the page, the P2PKH helper, the tests, and the two JPEG bodies. The bounce step uses the Transaction Bounce API; that service key is not included. # Feature map Source: https://docs.handcash.io/brc-wallet/feature-map The BRC wallet path to integrate today — auth, payments, items, and plain BSV-21 — and the Connect feature each call replaces **Beta — BRC wallet only.** Integrate the four rows under **Ship this**. The tables below them are the full Connect map, including calls this release forwards but has not proven. Request and response shapes follow [BRC-100](https://brc.dev/100). ## Read these as markdown Each task page is published as markdown. Hand an agent the `.md` URL, or open it yourself. | Task | Markdown | | - | - | | This map | [feature-map.md](https://docs.handcash.io/brc-wallet/feature-map.md) | | Getting started | [getting-started.md](https://docs.handcash.io/brc-wallet/getting-started.md) | | Permissions and errors | [permissions.md](https://docs.handcash.io/brc-wallet/permissions.md) | | Payments | [payments.md](https://docs.handcash.io/brc-wallet/payments.md) | | Collectables | [items.md](https://docs.handcash.io/brc-wallet/items.md) | | Tokens | [tokens.md](https://docs.handcash.io/brc-wallet/tokens.md) | | Signing | [signing.md](https://docs.handcash.io/brc-wallet/signing.md) | | Local bridge | [local-bridge.md](https://docs.handcash.io/brc-wallet/local-bridge.md) | | Every method | [interactions.md](https://docs.handcash.io/brc-wallet/interactions.md) | | Examples | [examples.md](https://docs.handcash.io/brc-wallet/examples.md) | ## How to read this page Connect (`@handcash/sdk`) is a cloud API: your server holds `appId` and `appSecret`, the user hands you an `authToken`, and HandCash executes on their behalf. The BRC wallet is the user's own keys on Desktop or Mobile: your app is identified by its origin, the user approves each capability once, and every payment, item, and token is a transaction your app describes and the wallet signs. That is why the right-hand column is short. Payments, items, and tokens are not separate APIs on the BRC wallet — they are `createAction`, `internalizeAction`, and `listOutputs` with different scripts, baskets, and tags. ```bash theme={null} npm install @bsv/sdk ``` ```typescript theme={null} import { WalletClient } from '@bsv/sdk' const wallet = new WalletClient('auto') // finds HandCash Desktop on loopback ``` `WalletClient` already implements the BRC-100 method names below. There is no HandCash SDK for the BRC wallet, and the same code runs against any conforming wallet. ## Ship this Hand these four to a developer integrating today. Each one has HandCash behavior in front of the wallet call: an origin gate, a prompt, a scope check, or a funds preflight. Desktop answers on `http://127.0.0.1:3321`. | | Call | Stop here | | - | - | - | | Auth | `isAuthenticated`, then `waitForAuthentication` | The origin is the session. There is no `authToken` and no app secret. | | Payments | `createAction`, `signAction`, `abortAction`, `internalizeAction`, `listActions`, `listOutputs` on basket `default` | Amounts are satoshis. Auto-pay and the BRC-73 cap cover plain payments only. Retry `CHANGE_CHAINING_REQUIRED`. | | Items | `listOutputs` on `p 1sat …`, `createAction` into basket `1sat`, `internalizeAction` with `basket insertion` | Always prompts. The app builds the ordinal script. Reads return only what the user granted. The contract is BRC-147, 150, 164, and 165; see [The 1Sat stack](#the-1sat-stack). | | Plain BSV-21 | `listOutputs` on `p bsv21 …`, `createAction` into basket `bsv21`, `internalizeAction` into `bsv21` | The app builds the BRC-162 script. Change back into `bsv21` is mandatory. Cosigned tips are not this path: the wallet will not cosign, and a plain unlock is refused. | Ordinary signing rides along with auth when a back end needs proof the wallet holds the key: `getPublicKey`, `createSignature`, `verifySignature`, `encrypt`, `decrypt`, `createHmac`, `verifyHmac`. A `createSignature` whose `protocolID` is `[2, 'wallet identity proof']` is checked against the calling origin. Any other protocol is signed as a normal BRC-42 signature. Two HandCash conveniences are safe behind a `404` check: `getBalance` (otherwise sum basket `default`) and `getClaimedCloudHandle` (otherwise the identity key). Leave the rest of this page for later. Certificates, identity discovery, and key-linkage reveals are forwarded to the wallet and have no HandCash proof that a third-party call succeeds. BRC-230 returns `404`. Market, migration, and handle writes return `403` to every other origin. ## Authentication and identity | Connect (`@handcash/sdk`) | BRC wallet (BRC-100) | Notes | | - | - | - | | `sdk.getRedirectionUrl()` → user signs in → `authToken` | `wallet.isAuthenticated()` then `wallet.waitForAuthentication()` | No redirect, no token. The wallet shows one Authorize prompt for your origin and remembers it per account. | | `sdk.getAccountClient(authToken)` | Nothing | The connection is the origin. Every call carries `originator`; there is no per-user client to construct. | | `Connect.getCurrentUserProfile()` → handle, paymail, display name, avatar | `wallet.getPublicKey({ identityKey: true })` | The stable user identity is the identity public key. The handle is a HandCash extension: `getClaimedCloudHandle` on the bridge, feature-detect it. Email and avatar are not exposed. | | `Connect.getPermissions()` | Nothing to poll | Grants are per origin and per method group. A call the user has not approved prompts; a refused call returns `403 ACTION_DENIED` or `PERMISSION_DENIED`. | | App secret on your server | Nothing | There is no server secret. Your app never holds anything the user did not sign. | Task page: [Permissions and scopes](/brc-wallet/permissions) · [permissions.md](https://docs.handcash.io/brc-wallet/permissions.md) ## Payments | Connect | BRC wallet | Notes | | - | - | - | | `Connect.pay({ receivers: [{ destination: '$handle', sendAmount }] })` | `wallet.createAction({ outputs: [{ lockingScript, satoshis, outputDescription }] })` | You supply the locking script. For a P2PKH recipient: `new P2PKH().lock(address).toHex()`. Paying another HandCash or BRC identity uses BRC-29 derivation, see the task page. | | `denominationCurrencyCode: 'USD'` | Not available | Amounts are satoshis. Convert on your side before you build the output. | | Multiple `receivers` in one `pay` | Multiple `outputs` in one `createAction` | One transaction, one prompt. | | `description` shown in the HandCash app | `description` and each `outputDescription` | These are what the user reads in the approval prompt. Keep them specific. | | `Connect.getSpendableBalances()` | `wallet.listOutputs({ basket: 'default', limit: 1000 })` and sum `satoshis` | Portable. `POST /getBalance` on the HandCash bridge returns the same number as a convenience; feature-detect it. | | `Connect.getBalances()` (multi-currency) | Not available | The BRC wallet holds BSV. Fiat display is the app's concern. | | `Connect.getExchangeRate()` | Not available | Use your own rate source. | | `Connect.getPaymentDetails({ transactionId })` | `wallet.listActions({ labels: [...], includeOutputs: true })` | Label every action you create; labels are how you find your own transactions later. | | Receiving a payment: nothing to do, HandCash credits the handle | `wallet.internalizeAction({ tx: atomicBeef, outputs: [{ protocol: 'wallet payment', paymentRemittance }] })` | The payer or your service hands the wallet the Atomic BEEF. Do not mark a delivery complete until `accepted` is true. | | Silent payments after one permission grant | Auto-pay, plus a `spendingAuthorization` cap in your web `manifest.json` (BRC-73) | User-controlled and per origin. Expect `CHANGE_CHAINING_REQUIRED` on rapid successive payments and retry with a short backoff. | | Staging a transaction your app finishes | `createAction` with `unlockingScriptLength` and `options.signAndProcess: false`, then `signAction({ reference, spends })` or `abortAction` | No Connect equivalent. This is how an app spends its own contract outputs. | Task page: [Payments and actions](/brc-wallet/payments) · [payments.md](https://docs.handcash.io/brc-wallet/payments.md) ## Items (collectables) | Connect Items | BRC wallet | Notes | | - | - | - | | `Connect.getItemsInventory({ body })` | `wallet.listOutputs({ basket: 'p 1sat all', includeTags: true })` | Reads go through a scoped view grant. `totalOutputs` is what the user let your app see, not the whole wallet. A view returns tags and `originVerified`; the wallet forces `includeCustomInstructions` off so provenance BEEF never rides an inventory. | | `collectionId` filter | `basket: 'p 1sat collection', tags: ['collection:'], tagQueryMode: 'all'` | Same idea for `p 1sat app`, `p 1sat creator`. | | Item `id` / `origin` | `origin:` tag and `customInstructions.origin`; the held row is the `id:` tag | Origin is the genesis outpoint, stable across owners. The tip is the current outpoint a transfer spends. | | `rarity`, `color`, `attributes`, `imageUrl` | `customInstructions` JSON your app writes at mint | The wallet reads `name`, `app`, `collectionId`, `content`, `creator`, `provenance`. Anything else is yours to define and render. | | `HandCashMinter.createItemsOrder()` (cloud mint) | `wallet.createAction({ outputs: [{ satoshis: 1, basket: '1sat', tags: ['ordinal', 'name:…', 'app:…', 'collection:…'], customInstructions }] })` | Your app builds the ordinal inscription script; the wallet funds and signs. Always prompts, auto-pay never covers items. | | `HandCashMinter.createCollectionOrder()` | A `collection:` tag on each item | There is no collection object. The collection is the tag plus your `collectionId` in `customInstructions`. | | v2 `transfer` / send item to a handle | `createAction` spending the held tip with label `` `p 1sat input id ${rowId}` `` and one 1-sat output to the recipient script | Supply `inputBEEF`. A recipient script for a BRC identity uses BRC-29 derivation like a payment. | | Receiving an item: automatic | `wallet.internalizeAction({ outputs: [{ protocol: 'basket insertion', insertionRemittance: { basket: '1sat', tags, customInstructions } }] })` | Keep `customInstructions` to identity fields; the wallet rebuilds BRC-150 provenance itself. | | `HandCashMinter.burnAndCreateItemsOrder()` | A transfer whose output is a protocol-valid terminal spend your app constructs | No burn method. Never turn a failed item path into a plain payment. | | Lock / unlock item | Not available | There is no custodian to hold a lock. A locked state is app-side or a covenant script you design. | | Item verified badge | BRC-150 v2 `provenance` in `customInstructions` | Wallet-verified. Missing or oversized proofs show as unproven, never truncated. | ### The 1Sat stack "Items" on the BRC wallet is four BRCs riding on `listOutputs`, `createAction`, and `internalizeAction`. The bridge advertises which ones it implements in `oneSat.brcs`, so an app can check before it builds an inscription. | BRC | What it fixes | Where it shows up on the wire | | - | - | - | | [BRC-147](https://github.com/HandCash/HANDCASH-DESKTOP/blob/master/docs/bsva/brcs/tokens/0147.md) | The storage basket `1sat`, the tag vocabulary, and the `customInstructions` schema (`origin`, `content`, `name`, `app`, `provenance`) | Every output you mint or receive into `1sat` | | [BRC-150](https://github.com/HandCash/HANDCASH-DESKTOP/blob/master/docs/bsva/brcs/tokens/0150.md) | Offline tip → origin proof, version 2 | `customInstructions.provenance`; the wallet writes it on send and verifies it on receive | | BRC-164 | A stable held-row key, `id:`, that names one row without exposing inventory | Stamped on the tags when a tip enters custody; used by `p 1sat id` reads and spend labels | | [BRC-165](https://github.com/bsv-blockchain/BRCs/pull/229) | The permission grammar: `p 1sat ` for reads, `p 1sat input id ` for spends | `basket` on `listOutputs`; `labels` on `createAction` | Storage and permission are separate. Held collectables live in `1sat`; your app never names that basket on a read. `listOutputs({ basket: '1sat' })` returns `400 USE_P1SAT_SCOPE`. **View rules.** The scope is one of `all`, `collection`, `app`, `creator`, `id`. The value is always a tag, never part of the basket name. A non-`all` scope with no matching tag, a bare `p 1sat`, an unknown scope, or a value embedded in the basket name fails closed. Extra tags narrow a request but cannot widen it: the wallet post-filters to the granted axis even when you send `tagQueryMode: 'any'`. `app:` and `creator:` are different axes. `p 1sat id` with one `id:` tag resolves without a prompt, which makes it the right call for a detail screen. A view returns tags plus the wallet's `originVerified` verdict. Provenance BEEF is not a view payload; the wallet never hands a 700-row inventory its proofs. Fetch one row with `include: 'entire transactions'` when you need the transaction itself. **Spend rules.** Every label `p 1sat input id ` must resolve to exactly one held row, and that row's outpoint must appear in `inputs`, or the call fails with `400 INVALID_P1SAT_SPEND` before any prompt. Each item spend is approved per action. Pay and auto-pay grants never cover item view or item spend. **What the bridge advertises.** `GET /health` and `GET /manifest.json` both carry this block. Branch on it, not on the wallet's name. ```json theme={null} { "oneSat": { "brcs": ["147", "150", "164", "165"], "baskets": ["1sat"], "permissions": { "protocol": "p 1sat", "viewScopes": ["all", "collection", "app", "creator", "id"], "spendLabel": "p 1sat input id " }, "provenanceVerify": ["v2"], "walletIdentityProof": { "version": 1, "methods": ["waitForAuthentication", "getPublicKey", "createSignature"], "protocolID": [2, "wallet identity proof"], "keyID": "identity-proof:", "counterparty": "anyone", "challenge": { "domain": "handcash-wallet-identity-proof", "encoding": "canonical-json-utf8", "maxTtlMs": 300000, "minNonceBits": 128 } } } } ``` `provenanceVerify: ['v2']` means the wallet shows an item as verified only on a complete BRC-150 v2 proof. BRC-156 soft-latch was withdrawn and is not advertised. Task page: [Collectables](/brc-wallet/items) · [items.md](https://docs.handcash.io/brc-wallet/items.md) ## Tokens (BSV-21) | Connect | BRC wallet | Notes | | - | - | - | | No fungible token API in Connect | `wallet.listOutputs({ basket: 'p bsv21 all' })` or `basket: 'p bsv21 id', tags: ['bsv21:']` | Balances are the sum of `amt:` across held tips per token id. There is no per-token balance method; `basket: 'bsv21'` directly returns `400 USE_PBSV21_SCOPE`. | | — | `createAction` with a BRC-162 / BSV-21 output, basket `bsv21`, tags `bsv21`, `op:`, `sym:`, `amt:`, `dec:` | Mint and transfer are the same call with different scripts. Amount lives in the lock, the output is 1 satoshi, change back to `bsv21` is mandatory. | | — | `internalizeAction` with `basket insertion` into `bsv21` | The named output must be 1 sat, pay the wallet, and decode to the same token id and amount. Every output paying you in a self-send is custody. | | — | Cosigned tips | Not the integrator path. The wallet's own send refuses them with `cosigner_required`, and the bridge will not produce the cosigner signature. An app spends one only by supplying that unlock itself. | | — | Provenance | BRC-176 walk from tip to a fixed-supply deploy; `issuerAttested` is a Sigma signature check, not a supply audit. | Task page: [Tokens](/brc-wallet/tokens) · [tokens.md](https://docs.handcash.io/brc-wallet/tokens.md) ## Signing and encryption | Connect | BRC wallet | Notes | | - | - | - | | Not offered by Connect | `wallet.getPublicKey({ protocolID, keyID, counterparty })` | App-scoped derived keys, BRC-42 / BRC-43. | | — | `wallet.createSignature`, `wallet.verifySignature` | Prompts once per origin for the protocol. | | — | `wallet.encrypt`, `wallet.decrypt`, `wallet.createHmac`, `wallet.verifyHmac` | Data the user can read on any BRC-100 wallet holding the same keys. | | — | `acquireCertificate`, `listCertificates`, `proveCertificate`, `relinquishCertificate` | On the wire, not proven. Forwarded to the wallet. Do not ship a certificate flow on this release. | | — | `discoverByIdentityKey`, `discoverByAttributes`, `revealCounterpartyKeyLinkage`, `revealSpecificKeyLinkage` | On the wire, not proven. No HandCash test shows a third-party discover or linkage call succeeding. | Task page: [Signing and encryption](/brc-wallet/signing) · [signing.md](https://docs.handcash.io/brc-wallet/signing.md) ## Social, business, and embedded wallets | Connect / Wallet API | BRC wallet | Notes | | - | - | - | | `Connect.getPublicUserProfiles({ handles })`, friends list | Not available | Friends are a HandCash cloud graph. The BRC wallet identifies people by identity key. Resolve handles with your own directory or the cloud API. | | Business Wallet payouts | Your own BRC-100 wallet instance running `createAction` | A server that pays users is itself a BRC-100 wallet, not a HandCash product. | | Wallet API (WaaS): create wallets by email, `/v1/waas/*` | Not applicable | WaaS embeds wallets you control. The BRC wallet is the user's. If you need to create accounts for users, stay on Wallet API. | | Sign in with Google via HandCash | Not applicable | The user unlocks their own wallet. | ## Errors | Connect `error.code` | BRC wallet | Notes | | - | - | - | | `INSUFFICIENT_FUNDS` | `400 INSUFFICIENT_FUNDS`, `INSUFFICIENT_OR_STALE_FUNDS` | Confirmed and confirming balance still short. | | — | `CHANGE_CHAINING_REQUIRED` | Pending change covers the ask. Retry shortly. | | `PERMISSION_DENIED` | `403 PERMISSION_DENIED`, `403 ACTION_DENIED` | The user has not granted the capability, or declined this prompt. | | `Permission denied` on items | `403 ITEM_VIEW_DENIED`, `403 TOKEN_VIEW_DENIED` | View grants are separate for collectables and tokens. | | `INVALID_DESTINATION` | Script validation on your side | The wallet signs the locking script you give it. | | Invalid token → re-authenticate | `NOT_AUTHENTICATED` → `waitForAuthentication` | | | — | `WALLET_LOCKED`, `OFFLINE_PAYMENTS_DISABLED`, `DOUBLE_SPENT` | Device state. A timeout is not a failure — check `listActions` by label before retrying a payment. | | — | `400 USE_P1SAT_SCOPE`, `400 INVALID_P1SAT_SPEND`, `403 MARKET_ORIGIN_DENIED` | Item scope grammar and HandCash-host-only methods. | Full list with HTTP statuses: [Permissions and scopes](/brc-wallet/permissions) · [permissions.md](https://docs.handcash.io/brc-wallet/permissions.md). ## How BRC-100 grows Connect grew by adding endpoints: `pay`, then `getItemsInventory`, then `createItemsOrder`, each a new SDK function with its own shape. BRC-100 does not grow that way. The method list is fixed and small; new functionality arrives in three layers, and each layer has a different portability guarantee. ``` Layer 3 Wallet-specific methods getBalance, getClaimedCloudHandle, market* HandCash only, feature-detect Layer 2 Protocols carried by Layer 1 BRC-29, 1Sat (147/150/164/165), BSV-21/162/176 Portable to any wallet that speaks the protocol Layer 1 BRC-100 core 28 methods, getVersion reports the interface Portable to every BRC-100 wallet ``` ### Layer 1: the core interface The wallet dispatches the BRC-100 method set as published, and `getVersion` names the interface it speaks. `@bsv/sdk` `WalletClient` covers this layer. Ship the groups marked ready. The others are forwarded and unproven in this release. | Group | Methods | For integrators | | - | - | - | | Session | `isAuthenticated`, `waitForAuthentication` | Ready | | Transactions | `createAction`, `signAction`, `abortAction`, `listActions`, `internalizeAction` | Ready | | Outputs | `listOutputs`, `relinquishOutput` | Ready. Reads of items and tokens use the `p` baskets, not the storage basket. | | Keys | `getPublicKey` | Ready. `revealCounterpartyKeyLinkage` and `revealSpecificKeyLinkage` are forwarded and unproven. | | Crypto | `encrypt`, `decrypt`, `createHmac`, `verifyHmac`, `createSignature`, `verifySignature` | Ready | | Chain | `getVersion`, `getNetwork`, `getHeight`, `getHeaderForHeight` | `getVersion` at connect time. The other three are forwarded. | | Certificates | `acquireCertificate`, `listCertificates`, `proveCertificate`, `relinquishCertificate` | Not this release | | Identity discovery | `discoverByIdentityKey`, `discoverByAttributes` | Not this release | ### Layer 2: protocols that ride on the core This is where features live. A payment, a collectable, and a fungible token are the same three calls with different scripts, baskets, tags, labels, and `customInstructions`. A wallet that adds support for a new protocol adds no methods; it learns to build, index, and verify a new kind of output. | Feature | Protocol | Where it lands in the call | | - | - | - | | Pay another identity | BRC-29 | `lockingScript` derived from the recipient's identity key; `paymentRemittance` on receive | | Derived keys, per-app | BRC-42 / BRC-43 | `protocolID`, `keyID`, `counterparty` on every key method | | Transaction proofs | BRC-62 Atomic BEEF | `inputBEEF` on `createAction`, `tx` on `internalizeAction` | | Silent spending cap | BRC-73 | `spendingAuthorization` in your web `manifest.json` | | Collectables | 1Sat ordinals: BRC-147 basket profile, BRC-150 provenance, BRC-164 held-row key, BRC-165 permission grammar | Basket `1sat`, tags `ordinal`, `origin:`, `name:`, `app:`, `collection:`, `creator:`, `id:`; read scope `p 1sat `; spend label `p 1sat input id `. See [The 1Sat stack](#the-1sat-stack). | | Fungible tokens | BSV-21 with BRC-162 value locks, BRC-176 provenance | Basket `bsv21`, tags `bsv21:`, `amt:`, `op:`, `sym:`, `dec:` | | Certificates | BRC-52 | Forwarded, not proven. Leave off the integrator path until a third-party call is shown succeeding. | | Catalog and overlay packs | BRC-230 | Not in this release. Shipping on `feature/brc-230-index-expansion`; the methods return `404 INDEX_EXPANSION_UNAVAILABLE` today | The permission grammar is part of this layer too. `p 1sat collection` and `p bsv21 id` are baskets in the BRC-100 sense — the wallet interprets them as scoped views. Another wallet may use a different grammar, so treat the scope string as a HandCash convention and the underlying `listOutputs` call as portable. ### Layer 3: methods HandCash adds These travel over the same loopback bridge, use the same JSON envelope, and are not in BRC-100. Feature-detect them and keep a portable fallback. | Method | Purpose | Portable alternative | | - | - | - | | `getBalance` | Spendable satoshis in one call | Sum `listOutputs` on basket `default` | | `getClaimedCloudHandle` | The `$handle` this device has claimed | Identity key, plus your own handle directory | | Wallet identity proof (a convention, not a method) | Prove to your back end that this wallet holds an identity key | Already portable: `getPublicKey` + `createSignature` under `protocolID [2, 'wallet identity proof']`, `keyID 'identity-proof:'`, `counterparty 'anyone'`. The manifest's `oneSat.walletIdentityProof` publishes the challenge rules. | Methods reserved for HandCash hosts refuse any other origin: market (`createMarketListingAdvert`, `createMarketPurchaseIntent`, `purchaseMarketListing`, `createCancelMarketListingAdvert`, `getTokenIcon` → `403 MARKET_ORIGIN_DENIED`), migration (`getLegacyAddress`, `refreshLegacyAddress`, `listMigrationTxids`) and handle administration (`claimCloudHandle`, `clearClaimedCloudHandle`) → `403 MIGRATION_ORIGIN_DENIED`, and `createAdminIdentityProof` (HandCash operations only). The origin is taken from the browser's `Origin` header first, then an `originator` header for non-browser clients. That is the identity every grant is keyed to. ### Discovering what a wallet supports Do this at connect time, once, and branch on the result rather than on the wallet's brand. | Probe | What it tells you | | - | - | | `GET http://127.0.0.1:3321/health` | The bridge is up. Returns `{ ok, service: 'handcash-brc100', oneSat }` | | `GET http://127.0.0.1:3321/manifest.json` | `babbage.trust` (wallet name and identity public key) and `babbage.oneSat` | | `oneSat.brcs` | Protocols the item path implements: `['147', '150', '164', '165']`. Full block under [The 1Sat stack](#the-1sat-stack). | | `oneSat.permissions` | View-scope grammar (`p 1sat` with `all`, `collection`, `app`, `creator`, `id`) and the spend-label template | | `oneSat.provenanceVerify` | Proof versions the wallet will mark verified: `['v2']` | | `oneSat.walletIdentityProof` | Version, methods, `protocolID`, `keyID` template, challenge rules for identity proofs | | `POST /getVersion` | The BRC-100 interface version | | `POST /` | `404 Unsupported BRC-100 method: ` — the signal to fall back | Probe extensions only after `waitForAuthentication` has resolved, so a probe can never be the thing that prompts the user. ```typescript theme={null} const BRIDGE = 'http://127.0.0.1:3321' async function walletProfile(wallet: WalletClient) { const health = await fetch(`${BRIDGE}/health`).then((r) => r.json()) const { version } = await wallet.getVersion() await wallet.waitForAuthentication() const balance = await fetch(`${BRIDGE}/getBalance`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' }) const hasGetBalance = balance.status !== 404 return { version, oneSat: health.oneSat, hasGetBalance } } async function spendableSats(wallet: WalletClient, hasGetBalance: boolean) { if (hasGetBalance) { const res = await fetch(`${BRIDGE}/getBalance`, { method: 'POST', body: '{}' }) return (await res.json()).satoshis as number } const { outputs } = await wallet.listOutputs({ basket: 'default', limit: 1000 }) return outputs.reduce((sum, o) => sum + o.satoshis, 0) } ``` ### The rule for app authors Ship auth, payments, items, and plain BSV-21, plus ordinary signing when a back end must check the key. Pick those protocols from Layer 2. Use `getBalance` and `getClaimedCloudHandle` only behind a `404` check. Leave certificates, identity discovery, key-linkage reveals, and BRC-230 off the integration until this repo shows a third-party call succeeding. That is the point of not shipping a HandCash SDK for the BRC wallet: the calls above are BRC-100, and the rest is not ready to wrap. ## Transport | | Connect | BRC wallet | | - | - | - | | Where the call goes | `https://cloud.handcash.io` | Desktop: `http://127.0.0.1:3321` (HTTPS `:2121` also listens) | | Who runs it | Your server | The user's browser or native app | | Mobile | Same cloud API | Same wallet core; a public deep-link transport is not yet a contract | Task page: [Local bridge](/brc-wallet/local-bridge) · [local-bridge.md](https://docs.handcash.io/brc-wallet/local-bridge.md). Live examples: [Transaction Bounce](https://brc-cloud.bcryderman.workers.dev/tx-bounce) and the [wallet demo](https://brc-cloud.bcryderman.workers.dev/wallet-demo). # Getting started Source: https://docs.handcash.io/brc-wallet/getting-started Start building against the HandCash BRC wallet (beta) — Desktop / Mobile, not Connect or WaaS **Beta — BRC wallet only.** These steps apply to apps that integrate with **HandCash Desktop or Mobile** over [BRC-100](https://brc.dev/100). They do **not** apply to Connect (`authToken` + `@handcash/sdk`) or Wallet API (`/v1/waas/*`). ## Prerequisites 1. **HandCash Desktop** (or Mobile) installed and unlocked with a funded wallet. 2. A web or native app that can reach the local BRC-100 bridge (Desktop) or a conforming wallet client. 3. Familiarity with BRC-100 method names (`createAction`, `listOutputs`, `internalizeAction`, …) from [brc.dev/100](https://brc.dev/100). ## Choose the right HandCash surface | You need… | Use | | - | - | | Existing HandCash **cloud** users authorize your app | [Connect](/v3/getting-started) | | Embedded wallets you create for **your** users (email) | [Wallet API (WaaS)](/wallet-api/getting-started) | | Talk to the user’s **self-custodial BRC wallet** on device | **This section (BRC wallet)** | ## First connection (Desktop) On Desktop, HandCash runs a **local bridge** on loopback so apps can call BRC-100 methods with user permission: * Primary: `https://127.0.0.1:2121` * Fallback: `http://127.0.0.1:3321` See **[Local bridge](/brc-wallet/local-bridge)** for discovery, origins, permissions, structured errors, and TLS fallback. Typical first call after the user has unlocked the wallet and approved your origin: 1. Discover the bridge with `POST /getVersion`. 2. Check `isAuthenticated`; call `waitForAuthentication` when needed. 3. Call wallet methods (e.g. balance via `getBalance`, payments via `createAction`). Exact JSON shapes follow BRC-100; HandCash’s permission UI gates origins and method scopes. You can also open [App Lab](https://brc-cloud.bcryderman.workers.dev/app-lab) with Desktop unlocked and run each method from the browser. ## Choose an interaction | You want to… | Page | | - | - | | See the path to ship, and how each Connect feature maps | [Feature map](/brc-wallet/feature-map) · [feature-map.md](https://docs.handcash.io/brc-wallet/feature-map.md) | | Connect, read balance, send BSV, stage or sign, receive, list history | [Payments and actions](/brc-wallet/payments) | | Read inventory, mint, transfer, or receive 1Sat items | [Collectables](/brc-wallet/items) | | Read balances, mint, transfer, or receive BSV-21 tokens | [Tokens](/brc-wallet/tokens) | | Derive keys, sign, encrypt, or handle certificates | [Signing and encryption](/brc-wallet/signing) | | Understand prompts, grants, scoped baskets, and error codes | [Permissions and scopes](/brc-wallet/permissions) | | See every method and its permission level | [Wallet interactions](/brc-wallet/interactions) | Mobile uses the same wallet core, but the loopback HTTP URLs on this page are a Desktop transport. Mobile app/deep-link transport is not yet a public contract. ## Next * **[Payments and actions](/brc-wallet/payments)** — the first calls most apps make * **[Wallet interactions](/brc-wallet/interactions)** — complete capability and permission map * **[Local bridge](/brc-wallet/local-bridge)** — ports, origins, and connecting from a web app * **[Overview](/brc-wallet/overview)** — how this section relates to Connect and WaaS # Wallet interactions Source: https://docs.handcash.io/brc-wallet/interactions Everything an app can ask the HandCash BRC wallet to do, and where each task is documented **Beta — BRC wallet only.** These methods apply to **HandCash Desktop / Mobile** over [BRC-100](https://brc.dev/100). They are not Connect (`authToken` + `@handcash/sdk`) or Wallet API (`/v1/waas/*`). Apps talk to the wallet with standard BRC-100 methods. Payments, items, and tokens are not separate APIs — they are transactions your app describes and the user approves. Connect, read balance, send BSV, stage and sign, receive, list history Read inventory, mint, transfer, and receive 1Sat items Read balances, mint BSV-21, transfer, and receive tokens Identity key, derived keys, signatures, encryption, HMAC, certificates Origins, prompts, auto-pay, scoped baskets, error codes, deadlines Loopback ports, discovery, transport errors ## Overview | Capability | Methods | | - | - | | Discovery | `getVersion`, `getNetwork`, `getHeight`, `getHeaderForHeight` | | Connection | `isAuthenticated`, `waitForAuthentication` | | Keys | `getPublicKey` | | Transactions | `createAction`, `signAction`, `abortAction`, `internalizeAction`, `listActions` | | Items and tokens | `listOutputs`, `relinquishOutput` — see [Collectables](/brc-wallet/items) and [Tokens](/brc-wallet/tokens) | | Cryptography | `encrypt`, `decrypt`, `createHmac`, `verifyHmac`, `createSignature`, `verifySignature` | | Certificates | `acquireCertificate`, `listCertificates`, `proveCertificate`, `relinquishCertificate` — forwarded, not the integrator path this release | | Identity discovery | `discoverByIdentityKey`, `discoverByAttributes`, `revealCounterpartyKeyLinkage`, `revealSpecificKeyLinkage` — forwarded, not the integrator path this release | Payload shapes follow [BRC-100](https://brc.dev/100). ## Permissions at a glance | Level | What the user sees | Methods | | - | - | - | | Public | Nothing | `getVersion`, `getNetwork`, `getHeight`, `getHeaderForHeight`, `isAuthenticated` | | Connect | One prompt per app origin | `waitForAuthentication` | | Approval | A prompt describing the exact transaction, signature, or disclosure | `createAction`, `signAction`, `internalizeAction`, `relinquishOutput`, `createSignature`, `encrypt`, `decrypt`, certificate and key-linkage methods | | Scoped view | A prompt naming the items or tokens requested | `listOutputs` on item and token baskets. Index baskets return `404` on this release. | Grants are bound to your origin and stored per wallet account. Plain BSV payments can skip the prompt when the user enabled auto-pay; item and token mints, sends, and releases always prompt. Full model — origin identity, auto-pay and spending limits, scoped basket grammar, every error code, and request deadlines: **[Permissions and scopes](/brc-wallet/permissions)**. ## HandCash extensions These travel over the same bridge but are not part of BRC-100, so feature-detect them: | Method | Purpose | | - | - | | `getBalance` | Spendable BSV in satoshis. Fall back to summing basket `default` on `404`. | | `getClaimedCloudHandle` | The wallet's verified handle binding. Fall back to the identity key on `404`. | Index and overlay packs (`installIndexExpansion`, `syncIndexExpansion`, `removeIndexExpansion`, `listIndexExpansions`, `listIndexExpansionEntries`, `overlayLookup`) return `404 INDEX_EXPANSION_UNAVAILABLE` on this release. ## Reserved for HandCash hosts Market, migration, and account-administration methods are origin-gated and return `403` to other apps: `createMarketListingAdvert`, `createMarketPurchaseIntent`, `verifyMarketListingProvenance`, `purchaseMarketListing`, `getMarketSettlementReceipt`, `createCancelMarketListingAdvert`, `getMarketListingStatus`, `markMarketListingPublishFailed`, `getTokenIcon`, `getLegacyAddress`, `refreshLegacyAddress`, `listMigrationTxids`, `claimCloudHandle`, `clearClaimedCloudHandle`, and `createAdminIdentityProof`. Build on `createAction` and `internalizeAction` instead — they are portable across BRC-100 wallets. ## Next steps * [Feature map](/brc-wallet/feature-map) · [feature-map.md](https://docs.handcash.io/brc-wallet/feature-map.md) * [Payments and actions](/brc-wallet/payments) * [Collectables](/brc-wallet/items) * [Tokens](/brc-wallet/tokens) * [Signing and encryption](/brc-wallet/signing) * [Permissions and scopes](/brc-wallet/permissions) — prompts, grants, and error codes * [Local bridge](/brc-wallet/local-bridge) — transport and discovery # Collectables Source: https://docs.handcash.io/brc-wallet/items Read, mint, transfer, receive, and verify 1Sat items with the BRC wallet **Beta — BRC wallet only.** Desktop / Mobile over [BRC-100](https://brc.dev/100), not Connect Items. ## The model A collectable is a **one-satoshi ordinal output**. The wallet holds it in storage basket `1sat`, and your app addresses it three different ways depending on what you are doing: | Concept | Value | Used for | | - | - | - | | Origin | The genesis outpoint, `_0` | Stable identity of the item across owners | | Tip | The current unspent one-satoshi outpoint | The thing a transfer spends | | Row id | A wallet-assigned key, tag `id:` | Naming one held row in reads and spends | Your app builds the inscription script. The wallet funds the transaction, signs it, files the output, and shows the user what they are approving. Item calls always prompt — auto-pay never covers them. Prerequisites: a [connected origin](/brc-wallet/permissions#connect-an-origin) and, for reads, an item view grant. ## Read inventory Reads use scoped permission baskets, never the storage basket. See [Permissions and scopes](/brc-wallet/permissions#scoped-inventory-views) for the full grammar and grant behaviour. | Basket | Required tag | | - | - | | `p 1sat all` | none | | `p 1sat collection` | `collection:` | | `p 1sat app` | `app:` | | `p 1sat creator` | `creator:` | | `p 1sat id` | `id:` | ### All items ```typescript theme={null} const inventory = await wallet.listOutputs({ basket: 'p 1sat all', includeCustomInstructions: true, limit: 50, offset: 0 }); console.log(`${inventory.totalOutputs} rows visible to this app`); ``` `totalOutputs` is what your grant exposes, not the wallet's true holdings. A user who approved two collections sees two collections here, with HTTP `200` and no error. ### One collection ```typescript theme={null} const robots = await wallet.listOutputs({ basket: 'p 1sat collection', tags: ['collection:robots'], tagQueryMode: 'all', includeCustomInstructions: true, limit: 50 }); ``` ### One exact row, with its transaction ```typescript theme={null} const item = await wallet.listOutputs({ basket: 'p 1sat id', tags: [`id:${rowId}`], tagQueryMode: 'all', includeCustomInstructions: true, include: 'entire transactions', limit: 1 }); ``` A specific row id resolves without a prompt, which makes it the right call for a detail screen after the user already picked an item. Tags are always returned for item reads. ## Item vocabulary ### Tags | Tag | Meaning | Written by | | - | - | - | | `ordinal` | Protocol marker | Your app; the wallet adds it on its own imports | | `origin:.` | Genesis outpoint | Your app | | `name:` | Display name, shown in prompts | Your app | | `app:` | Owning app, up to 40 characters | Your app | | `collection:` | Collection, up to 80 characters | Your app | | `creator:` | Creator, a view filter axis only | Your app | | `content:` | Shared media output for derivative items | Your app | | `id:` | Held-row key | **The wallet**, unless you supply one | An item with none of `name`, `app`, `collection`, `content`, or `id` is invisible to third-party apps, so always tag at least a name. ### `customInstructions` Send a JSON string. The wallet reads these fields: | Field | Meaning | | - | - | | `origin` | Genesis outpoint in `_` form | | `name` | Display name | | `app` | Owning app id | | `collectionId` | Collection binding used when matching view grants | | `content` | Shared media outpoint for derivative items | | `creator` | Creator, used as a view-matching fallback | | `provenance` | BRC-150 authenticity proof — wallet-written on send, see below | ## Mint an item An item mint is a `createAction` with **no inputs** and at least one one-satoshi output in an item basket: ```typescript theme={null} const { txid } = await wallet.createAction({ description: 'Mint Robot #42', labels: ['1sat', 'item', 'app:robot-foundry'], outputs: [ { lockingScript: ordinalInscriptionScript, satoshis: 1, outputDescription: 'Robot #42 collectable', basket: '1sat', tags: ['ordinal', 'app:robot-foundry', 'collection:robots', 'name:Robot #42'], customInstructions: JSON.stringify({ name: 'Robot #42', app: 'robot-foundry', collection: 'robots', mediaType: 'image/png' }) } ] }); ``` The wallet stamps a row id onto the output tags so you can address this exact item later. Baskets and tags index the item; they never create the inscription, so validate the script against the 1Sat protocol before you send it. The user sees a **Mint item** prompt with your description and the item name from the `name:` tag. Refusal returns `403 ACTION_DENIED`; funding problems return `400 INSUFFICIENT_FUNDS` or `INSUFFICIENT_OR_STALE_FUNDS`. ## Transfer an item A transfer spends the held tip and creates the next one. Name the held row with a spend label so the wallet knows which item you mean: ```typescript theme={null} const { txid } = await wallet.createAction({ description: 'Send Robot #42', labels: ['1sat', 'send-collectable', `p 1sat input id ${rowId}`], inputBEEF: itemInputBeef, inputs: [ { outpoint: heldItemOutpoint, inputDescription: 'Robot #42 1Sat tip', unlockingScript: itemUnlockingScript } ], outputs: [ { lockingScript: recipientItemScript, satoshis: 1, outputDescription: 'Robot #42 transfer' } ] }); ``` The label grammar is exactly `p 1sat input id `. Each label must resolve to exactly one held row **and** that row's outpoint must appear in `inputs`, or the call fails with `400 INVALID_P1SAT_SPEND` before the user is ever prompted. Supply `inputBEEF` for the tip's source transactions. If you send `unlockingScriptLength` instead of a script, you get a staged transaction back and must finish that exact reference with [`signAction`](/brc-wallet/payments#stage-and-sign-an-action) — the wallet does not hold your unlock recipe and never exposes a root private key. Covenant-locked tips cannot be spent with a plain P2PKH unlock; the wallet refuses rather than guessing. If a tip has gone missing on chain, that is a wallet-side abandon in HandCash, not something an app resolves with `relinquishOutput`. ### Burning There is no burn method for apps. A burn is a transfer whose output is a protocol-valid terminal spend that your app constructs. Never turn a failed item path into a plain payment. ## Receive an item ```typescript theme={null} const { accepted } = await wallet.internalizeAction({ tx: atomicBeef, description: 'Receive Robot #42', labels: ['1sat', 'receive-collectable'], outputs: [ { outputIndex: 0, protocol: 'basket insertion', insertionRemittance: { basket: '1sat', tags: ['ordinal', `origin:${originTxid}.${originVout}`, 'name:Robot #42', 'app:robot-foundry'], customInstructions: JSON.stringify({ origin: `${originTxid}_${originVout}`, name: 'Robot #42', app: 'robot-foundry' }) } } ] }); ``` `protocol` must be the string `basket insertion`, and the remittance basket is the storage basket `1sat`. Keep `insertionRemittance.customInstructions` small — identity fields only. The receive path caps it near 1,000 characters, so a full BRC-150 proof does not fit. The wallet rebuilds and verifies provenance locally instead. The wallet validates the transaction before accepting it. Do not tell the sender a delivery succeeded until this call resolves. The user sees a **Receive item** prompt the first time; after that a stored receive grant can carry it. ## Authenticity HandCash shows an item as verified only when a complete **BRC-150 v2** proof checks out. The proof is a local object in `customInstructions.provenance`: ```typescript theme={null} type Provenance = { v: 2; origin: string; // genesis outpoint tip: string; // current outpoint path: string[]; // tip → origin lineage beefB64: string; // BEEF covering every hop contentType?: string; }; ``` What the wallet checks: the path starts at the tip and ends at the origin, every hop is an exact parent spend, one-satoshi continuity holds throughout, and the origin carries a valid ordinal envelope. | Outcome | What the user sees | | - | - | | Complete BRC-150 proof | Verified | | Missing or incomplete proof | Unproven, with identity resolved by an indexer | Two rules matter for app authors. Provenance is **local metadata** — it does not ride along on a P2PKH lock, so a recipient's wallet reconstructs it. And an oversized proof is **omitted, never truncated**: the wallet's cap is 400,000 characters of BEEF, above which the item stays unproven rather than carrying a partial lineage. ## Release a row ```typescript theme={null} await wallet.relinquishOutput({ basket: 'p 1sat all', output: heldItemOutpoint }); ``` This drops the wallet's record of the output. It is not a burn, it moves no coins, and it prompts as **Release item**. Use `p 1sat all` here: scoped forms that require a tag cannot carry one on this call. ## Not available to apps | Attempt | Result | | - | - | | `listOutputs({ basket: '1sat' })` | `400 USE_P1SAT_SCOPE` | | A persistent "all items" grant | Converted to a filtered collection and app ceiling | | Auto-pay covering an item mint, send, or release | Always prompts | | Wallet-side burn or abandon | HandCash UI only | | Market listing and purchase methods | HandCash hosts only, `403 MARKET_ORIGIN_DENIED` | | BRC-156 soft-latch | Removed; authenticity is BRC-150 only | ## Next steps * [Tokens](/brc-wallet/tokens) * [Permissions and scopes](/brc-wallet/permissions) * [Signing and encryption](/brc-wallet/signing) # Local bridge Source: https://docs.handcash.io/brc-wallet/local-bridge Connect a web or local app to the HandCash BRC wallet Desktop bridge (beta) **Beta — HandCash BRC wallet (Desktop).** Bridge ports, TLS, and origin allowlists can change. This page is for the **local BRC-100 bridge**, not cloud Connect or Wallet API. ## Endpoints When HandCash Desktop is running and unlocked, apps reach the wallet on loopback: | Role | URL | | - | - | | Primary | `https://127.0.0.1:2121` | | Fallback | `http://127.0.0.1:3321` | Use the primary HTTPS endpoint when available; fall back to `3321` if the primary is unreachable (some environments reject the local certificate). ## Ownership * **Desktop** owns the local BRC-100 bridge and the permission UX. * Your app must present a clear **origin** the wallet can approve or deny. * Method payloads should match [BRC-100](https://brc.dev/100). HandCash-specific migrate / claim helpers may be origin-gated to HandCash hosts. ## Discovery Use `POST /getVersion` with `{}` as the body. This is the BRC-100 substrate probe used by wallet clients: ```typescript theme={null} const response = await fetch('https://127.0.0.1:2121/getVersion', { method: 'POST', headers: { 'Content-Type': 'application/json', originator: location.host, }, body: '{}', }) ``` Also available: | Endpoint | Purpose | | - | - | | `GET /health` | Lightweight listener health and advertised HandCash capabilities. | | `GET /manifest.json` | Wallet identity, trust metadata, and the `oneSat` capability block: implemented 1Sat BRCs, view scopes, spend label, provenance versions, identity-proof rules. The block is documented on the [feature map](/brc-wallet/feature-map#the-1sat-stack). | | `POST /{method}` | One JSON BRC-100 method call. | `GET /health` does not prove that the renderer is unlocked and ready to answer wallet calls. Use `getVersion` for wallet discovery. ## Responses and errors Successful methods return their BRC-100 JSON result with HTTP `200`. Errors are JSON: ```json theme={null} { "status": "error", "code": "WALLET_LOCKED", "description": "Unlock HandCash to use the BRC-100 interface." } ``` Common bridge errors: | HTTP | Code | Meaning | | - | - | - | | `401` | `NOT_AUTHENTICATED` | Connect this origin with `waitForAuthentication`. | | `403` | `PERMISSION_DENIED` / `ACTION_DENIED` | The user denied connection or the specific action. | | `400` | `INSUFFICIENT_FUNDS` | Spendable local wallet state cannot fund the action. | | `400` | `USE_P1SAT_SCOPE` / `USE_PBSV21_SCOPE` | Use a permission basket, not an internal inventory basket. | | `503` | `WALLET_LOCKED` | Desktop is running but the wallet is locked. | | `503` | `OFFLINE_PAYMENTS_DISABLED` | A mutating payment was refused while offline. | Do not retry a mutating request blindly after a transport timeout: the wallet may have completed it after the caller disconnected. Reconcile by label, transaction ID, or application protocol state first. ## Deadlines The bridge holds your request while the user decides: roughly **120 seconds** for reads and prompts, **300 seconds** for spends. Past that you get `503 WALLET_BRIDGE_TIMEOUT` for a read or `503 WALLET_BRIDGE_PENDING` for a spend, and `WALLET_BRIDGE_PENDING` may still complete afterwards. If your HTTP client disconnects, the wallet cancels the pending prompt. Other transport failures are `503 WALLET_BRIDGE_UNAVAILABLE` when the wallet window cannot answer and `500 HTTP_BRIDGE_ERROR` for anything else. ## Origin and permission rules Your origin identifies your app, and the wallet gates methods in tiers: public discovery, a one-time connection, per-action approval, and separate scoped grants for item, token, and catalog reads. Market, migration, handle-claim, and administrative helpers are restricted to HandCash-owned hosts. Full rules, grant behaviour, and every permission error code: **[Permissions and scopes](/brc-wallet/permissions)**. ## Checklist for a first integration 1. User installs and unlocks **HandCash Desktop**. 2. Your app probes `getVersion` on `2121`, then `3321`. 3. It calls `isAuthenticated`, then `waitForAuthentication` if needed. 4. The user grants your origin and any action-specific permissions. 5. Your app calls standard BRC-100 methods and handles structured errors. 6. It never treats cloud Connect `authToken` or WaaS access keys as substitutes for this path. ## Related * **[Getting started](/brc-wallet/getting-started)** * **[Wallet interactions](/brc-wallet/interactions)** * **[Payments and actions](/brc-wallet/payments)** * **[Permissions and scopes](/brc-wallet/permissions)** * **[Overview](/brc-wallet/overview)** * Protocol: [BRC-100](https://brc.dev/100) * Live board: [App Lab](https://brc-cloud.bcryderman.workers.dev/app-lab) # Overview Source: https://docs.handcash.io/brc-wallet/overview Beta developer docs for the HandCash BRC wallet (Desktop + Mobile) — BRC-100 apps, not Connect or Wallet API (WaaS) **Beta.** This section documents the **HandCash BRC wallet** (Desktop and Mobile). APIs, bridge ports, and permission UX may change while the product is in beta. Prefer [BRC-100](https://brc.dev/100) as the protocol source of truth; treat these pages as HandCash-specific guidance on top of it. ## What this section is for These pages are for developers building against the **HandCash BRC wallet** — the self-custodial Open BSV wallet that ships as: | Part | Role | | - | - | | **Desktop** | Electron shell + local [BRC-100](https://brc.dev/100) bridge | | **Mobile** | Capacitor shell over the same wallet UI core | | **Your app** | Talks to the wallet over the local bridge (or a conforming BRC-100 client) | It is **not** documentation for: | Product | Docs | | - | - | | **HandCash Connect** (cloud OAuth / `@handcash/sdk`) | [v3 getting started](/v3/getting-started) | | **Wallet API (WaaS)** (`/v1/waas/*`) | [Wallet API getting started](/wallet-api/getting-started) | If your integration still uses Connect auth tokens or WaaS access keys, stay in those sections. Switch here when you target the user’s **local BRC wallet** (identity key, baskets, `createAction` / `internalizeAction`, permissions). ## Mental model * The user holds keys in HandCash Desktop or Mobile. * Your app requests wallet methods over **BRC-100**. * On Desktop, HandCash exposes a **local HTTP bridge** (loopback) so web and native apps can call those methods with origin-gated permissions. * Custody stays with the user. Cloud messagebox / handle resolve are convenience services, not required for payments to succeed. ## In this section * **[Feature map](/brc-wallet/feature-map)** — what to integrate today (auth, payments, items, plain BSV-21), and the Connect call each one replaces. [feature-map.md](https://docs.handcash.io/brc-wallet/feature-map.md) * **[Getting started](/brc-wallet/getting-started)** — prerequisites, when to use the BRC wallet vs Connect / WaaS * **[Wallet interactions](/brc-wallet/interactions)** — the map: every app-facing method and permission level * **[Payments and actions](/brc-wallet/payments)** — connect, balance, send BSV, stage and sign, receive, history * **[Collectables](/brc-wallet/items)** — read inventory, mint, transfer, and receive 1Sat items * **[Tokens](/brc-wallet/tokens)** — read balances, mint BSV-21, transfer, and receive * **[Signing and encryption](/brc-wallet/signing)** — keys, signatures, encryption, HMAC, certificates * **[Permissions and scopes](/brc-wallet/permissions)** — origins, prompts, auto-pay, scoped baskets, error codes * **[Local bridge](/brc-wallet/local-bridge)** — connect to Desktop on loopback, discover the wallet, and handle errors * **[Examples](/brc-wallet/examples)** — download Transaction Bounce and the wallet demo ## Protocol references * [BRC-100](https://brc.dev/100) — wallet ↔ application interface * [BRC-46](https://brc.dev/46) — output baskets * [BRC-29](https://brc.dev/29) — peer payments (when paying another HandCash / BRC identity) ## Try it [HandCash App Lab](https://brc-cloud.bcryderman.workers.dev/app-lab) is a live BRC-100 widget board that talks to the local wallet bridge on this device. # Payments and actions Source: https://docs.handcash.io/brc-wallet/payments Connect to the BRC wallet, read balance, send BSV, stage transactions, and receive payments **Beta — BRC wallet only.** Desktop / Mobile over [BRC-100](https://brc.dev/100), not Connect or Wallet API. ## Setup ```bash theme={null} npm install @bsv/sdk ``` ```typescript theme={null} import { WalletClient } from '@bsv/sdk'; // Finds the user's local wallet, including the HandCash Desktop bridge. const wallet = new WalletClient('auto'); ``` Every call carries your origin so the wallet can show the user who is asking. See [Local bridge](/brc-wallet/local-bridge) to call the same methods over plain HTTP. ## Connect ```typescript theme={null} const { authenticated } = await wallet.isAuthenticated(); if (!authenticated) { // Opens the connection prompt in HandCash. await wallet.waitForAuthentication(); } ``` `isAuthenticated` never prompts, so use it to decide whether to show a connect button. ## Read balance `getBalance` is a HandCash convenience method: ```typescript theme={null} const response = await fetch('http://127.0.0.1:3321/getBalance', { method: 'POST', headers: { 'Content-Type': 'application/json', originator: location.host }, body: '{}' }); const { satoshis } = await response.json(); ``` For a portable version, total the funding outputs the wallet holds: ```typescript theme={null} const funding = await wallet.listOutputs({ basket: 'default', limit: 1000 }); const satoshis = funding.outputs.reduce((total, output) => total + output.satoshis, 0); ``` ## Send a payment Build the recipient locking script with your Bitcoin library, then let the wallet fund, sign, and broadcast: ```typescript theme={null} import { P2PKH, WalletClient } from '@bsv/sdk'; const wallet = new WalletClient('auto'); const { txid } = await wallet.createAction({ description: 'Coffee order 7842', labels: ['coffee-order'], outputs: [ { lockingScript: new P2PKH().lock(recipientAddress).toHex(), satoshis: 25_000, outputDescription: 'Coffee order' } ] }); ``` The user sees your `description`, the amount, and your app name. Keep descriptions specific — they are what the user is agreeing to. ## Stage and sign an action Use two steps when your app supplies its own unlocking script, such as spending an application contract: ```typescript theme={null} const staged = await wallet.createAction({ description: 'Claim contract payout', inputs: [ { outpoint: contractOutpoint, inputDescription: 'Game contract', unlockingScriptLength: 108 } ], outputs: [ { lockingScript: nextContractLockingScript, satoshis: 5_000, outputDescription: 'Next contract state' } ], options: { signAndProcess: false } }); const { reference } = staged.signableTransaction!; const { txid } = await wallet.signAction({ reference, spends: { 0: { unlockingScript: contractUnlockingScript } } }); ``` Any input with `unlockingScriptLength` instead of `unlockingScript` produces a `signableTransaction` you must finish with `signAction`. The wallet never invents your contract's unlock. ### Abort a staged action ```typescript theme={null} await wallet.abortAction({ reference }); ``` Abort or sign — never abort and then build a second transaction spending the same input. ## Receive a payment When another party hands your app a transaction in Atomic BEEF format: ```typescript theme={null} const { accepted } = await wallet.internalizeAction({ tx: atomicBeef, description: 'Tournament prize', labels: ['tournament-payout'], outputs: [ { outputIndex: 0, protocol: 'wallet payment', paymentRemittance: { derivationPrefix, derivationSuffix, senderIdentityKey } } ] }); ``` The wallet validates the transaction before it accepts it. Do not mark a delivery complete until this call succeeds. ## List history ```typescript theme={null} const history = await wallet.listActions({ labels: ['coffee-order'], labelQueryMode: 'all', includeLabels: true, includeOutputs: true, limit: 25, offset: 0 }); console.log(`${history.totalActions} matching actions`); ``` Labels are how you find your own transactions later, so label every action your app creates. ## Error handling ```typescript theme={null} try { await wallet.createAction(action); } catch (error) { // WALLET_LOCKED, NOT_AUTHENTICATED, PERMISSION_DENIED, ACTION_DENIED, // INSUFFICIENT_FUNDS, OFFLINE_PAYMENTS_DISABLED console.error(error); } ``` A timeout is not a failure — the wallet may finish an action after your request drops. Check `listActions` by label before retrying a payment. Every code, with its HTTP status, is listed in [Permissions and scopes](/brc-wallet/permissions#errors). ## Next steps * [Collectables](/brc-wallet/items) * [Tokens](/brc-wallet/tokens) * [Signing and encryption](/brc-wallet/signing) * [Permissions and scopes](/brc-wallet/permissions) # Permissions and scopes Source: https://docs.handcash.io/brc-wallet/permissions How the BRC wallet identifies your app, which calls prompt the user, and how scoped item, token, and catalog views work **Beta — BRC wallet only.** Desktop / Mobile over [BRC-100](https://brc.dev/100). Three independent gates decide whether a call succeeds: 1. **Origin** — is your app connected at all? 2. **Action approval** — did the user approve this specific transaction, signature, or disclosure? 3. **Scoped view** — did the user grant a view of the items, tokens, or catalog you asked for? A connection grants none of the others, and an item view never implies a token view. ## How the wallet identifies your app The wallet resolves your identity per request: 1. The browser-supplied `Origin` header, reduced to its host. 2. Otherwise the `originator` header, parsed as a URL and reduced to its host. 3. Otherwise no origin, which is the wallet's own internal path — not available to apps. Hosts are lowercased with `www.` stripped, so `https://App.Example.com/x` and `https://www.app.example.com` are the same app. `localhost` and `127.0.0.1` are different apps. Grants are stored per wallet account, so switching accounts in HandCash starts from a clean slate. Always send `originator` when you are not in a browser. Never claim another site's origin. ## Method tiers | Tier | Behaviour | Methods | | - | - | - | | Public | No connection, no prompt | `getVersion`, `getNetwork`, `getHeight`, `getHeaderForHeight`, `GET /health` | | Silent | Answers without prompting | `isAuthenticated` | | Connect | Opens the connection prompt once per origin | `waitForAuthentication` | | Action | Needs a connection, then approval per call | `createAction`, `signAction`, `internalizeAction`, `relinquishOutput`, `relinquishCertificate`, `acquireCertificate`, `proveCertificate`, `createSignature`, `encrypt`, `decrypt`, `revealCounterpartyKeyLinkage`, `revealSpecificKeyLinkage`, `removeIndexExpansion` | | Connected | Needs a connection, no extra prompt | `getPublicKey`, `getBalance`, `abortAction`, `listActions`, `verifySignature`, `createHmac`, `verifyHmac`, `listCertificates`, `discoverByIdentityKey`, `discoverByAttributes`, `listIndexExpansions` | | Scoped view | Needs a connection, then a view grant | `listOutputs` on item, token, or index baskets; `installIndexExpansion`, `syncIndexExpansion`, `listIndexExpansionEntries`, `overlayLookup` | ## Connect an origin ```typescript theme={null} const { authenticated } = await wallet.isAuthenticated(); if (!authenticated) { const result = await wallet.waitForAuthentication(); if (!result.authenticated) return; // The user declined. } ``` `isAuthenticated` never prompts, so it is safe on page load. A declined connection returns `{ authenticated: false }` with HTTP `200` from `waitForAuthentication`; any other method on an unconnected origin returns `401 NOT_AUTHENTICATED`, and a declined origin returns `403 PERMISSION_DENIED`. Grants persist across restarts. The user can disconnect your app in HandCash settings, which also clears its auto-pay and spending limit. While the wallet is locked, everything except `GET /health` and `GET /manifest.json` returns `503 WALLET_LOCKED`. ## Action approval These always prompt, every time, and are never covered by Pay or auto-pay: * Item mints, item sends, item releases * Token mints, token sends, token releases * `createSignature`, `encrypt`, `decrypt`, key-linkage reveals, certificate acquisition and disclosure These can proceed without a prompt once the user has opted in: | Call | Skips the prompt when | | - | - | | `createAction` / `signAction` for plain BSV | The user enabled auto-pay for your origin and the payment is inside the limit | | `internalizeAction` for plain BSV | The user left "accept funds automatically" on, which is the default at connect | | `internalizeAction` for an item or token | The user previously approved a receive from your origin | | `proveCertificate` | Within 20 seconds of connecting, or already approved this session | Concurrent duplicate requests from one origin may share a single prompt, except for `createSignature`, `createAdminIdentityProof`, and market mutations, which always get their own. ### Auto-pay and spending limits Auto-pay is the user's setting, not an API. When they enable it, the default ceiling is **10 USD per 24 hours**, and each payment must also fit under that per-payment cap. Spending is counted from your origin's payment activity, and item activity never counts against it. Your manifest can propose a monthly cap instead, which the wallet reads from `metanet.groupPermissions.spendingAuthorization` (Babbage's `babbage.groupPermissions` is also accepted): ```json theme={null} { "metanet": { "groupPermissions": { "spendingAuthorization": { "amount": 50000, "description": "Monthly game purchases" } } } } ``` `amount` is satoshis per UTC calendar month. The wallet fetches `manifest.json` from your origin while the connect prompt is already open and gives up after 1.5 seconds, so keep it fast and cacheable. A granted monthly cap replaces the USD window, but it never enables silent payments on its own — the user must still turn auto-pay on. ## Scoped inventory views Reads of items, tokens, and catalog packs go through permission baskets, not storage baskets. The form is `p `, and filter values go in tags rather than in the basket name. | Basket | Scope | Required tag | | - | - | - | | `p 1sat all` | Every collectable the wallet holds | none | | `p 1sat collection` | One or more collections | `collection:` | | `p 1sat app` | One or more apps | `app:` | | `p 1sat creator` | One or more creators | `creator:` | | `p 1sat id` | Exact wallet rows | `id:` | | `p bsv21 all` | Every token | none | | `p bsv21 id` | Specific tokens | `bsv21:` or `id:` | | `p index read` | A catalog pack's cache | pack resolved from the basket | Scope tokens are lowercase and exact. Storage baskets (`1sat`, `bsv21`) are wallet-internal and refused here. Where each form belongs: | Field | Use | | - | - | | `listOutputs({ basket })` | The scoped `p …` basket above | | `createAction` output `basket` | The storage basket: `1sat` or `bsv21` | | `internalizeAction` `insertionRemittance.basket` | The storage basket: `1sat` or `bsv21` | | `relinquishOutput({ basket })` | `p 1sat all` or `p bsv21 all` — scoped forms that need a tag cannot carry one here | ### What a grant actually returns * A grant is a **ceiling**, and the wallet filters results down to it. A narrower grant returns fewer outputs with HTTP `200`, not an error. * Only a refusal at the prompt produces `403 ITEM_VIEW_DENIED` or `403 TOKEN_VIEW_DENIED`. * Third-party apps never hold a persistent "all" grant. Asking for `p 1sat all` turns into a filtered grant over the collections and apps the user approved, so plan for a subset and paginate. * `p 1sat id` with a specific row id is a narrow lookup and resolves without a prompt. * Item view and token view are separate grants. Holding one and calling the other prompts again. * Rows with no identifying metadata are never returned to third-party apps. ### Catalog packs Catalog packs (index expansions) let your app read a shared index offline and query a live overlay through it: | Method | Grant | | - | - | | `listIndexExpansions` | Connection only | | `installIndexExpansion` | Install approval, recorded per pack | | `syncIndexExpansion` | Sync approval, reusing the install grant | | `listIndexExpansionEntries` | Read approval per pack | | `overlayLookup` | Overlay approval per pack or lookup service | | `removeIndexExpansion` | Action approval | Reading a pack through `listOutputs` uses `p index read `; using `p index install` or `p index sync` there returns `400 INVALID_INDEX_BASKET`. ## Errors | HTTP | Code | Meaning | | - | - | - | | `503` | `WALLET_LOCKED` | Desktop is running but locked. | | `401` | `NOT_AUTHENTICATED` | Call `waitForAuthentication` first. | | `403` | `PERMISSION_DENIED` | The user declined the connection. | | `403` | `ACTION_DENIED` | The user declined this transaction or signature. | | `403` | `ITEM_VIEW_DENIED` / `TOKEN_VIEW_DENIED` | The user declined the inventory view. | | `403` | `INDEX_INSTALL_DENIED` / `INDEX_SYNC_DENIED` / `INDEX_READ_DENIED` / `OVERLAY_LOOKUP_DENIED` | The user declined a catalog request. | | `403` | `MARKET_ORIGIN_DENIED` / `MIGRATION_ORIGIN_DENIED` | Host-restricted method called from another origin. | | `409` | `ACTION_IN_PROGRESS` | The same market action is already running. | | `400` | `USE_P1SAT_SCOPE` / `USE_PBSV21_SCOPE` | You sent a storage basket where a scoped basket belongs. | | `400` | `INVALID_P1SAT_SCOPE` / `INVALID_PBSV21_SCOPE` | The scope token is not one of the allowed values. | | `400` | `MISSING_P1SAT_SCOPE_TAG` / `MISSING_PBSV21_SCOPE_TAG` | The scope needs at least one matching tag. | | `400` | `UNSUPPORTED_P_BASKET` | Only the `1sat`, `bsv21`, and `index` schemes exist. | | `400` | `INVALID_INDEX_BASKET` | Use the index methods, or `p index read `. | | `400` | `INVALID_P1SAT_SPEND` | A `p 1sat input id` label did not resolve to one held row that the action spends. | | `400` | `INSUFFICIENT_FUNDS` / `INSUFFICIENT_OR_STALE_FUNDS` / `DOUBLE_SPENT` / `CHANGE_CHAINING_REQUIRED` | The spend failed its pre-flight check. | | `503` | `OFFLINE_PAYMENTS_DISABLED` | A mutating payment was refused while offline. | ## Deadlines The bridge holds a request while the user decides: about **120 seconds** for reads and prompts, and about **300 seconds** for spends. Past that you get `503 WALLET_BRIDGE_TIMEOUT` for a read or `503 WALLET_BRIDGE_PENDING` for a spend. `WALLET_BRIDGE_PENDING` does not mean failure — the wallet may still complete the action. Reconcile with `listActions` by label, or by protocol state, before you retry anything that mutates. If your HTTP client disconnects, the wallet cancels the pending prompt. ## Designing for the prompt The prompt is the user's whole view of your request, so: * Write a specific `description`; the wallet shows it verbatim. * Give every input and output a real `inputDescription` / `outputDescription`. * Expect the wallet window to take focus, and do not block your UI on a modal of your own. * Item and token previews come from wallet-held data. A preview you supply is not displayed, so it cannot be used to misrepresent an asset. ## Reserved for HandCash hosts Market, migration, handle-claim, and operations methods check the calling host and refuse everyone else: | Group | Methods | Refusal | | - | - | - | | Market | `createMarketListingAdvert`, `createMarketPurchaseIntent`, `verifyMarketListingProvenance`, `purchaseMarketListing`, `getMarketSettlementReceipt`, `createCancelMarketListingAdvert`, `getMarketListingStatus`, `markMarketListingPublishFailed`, `getTokenIcon` | `403 MARKET_ORIGIN_DENIED` | | Migration | `getLegacyAddress`, `refreshLegacyAddress`, `listMigrationTxids` | `403 MIGRATION_ORIGIN_DENIED` | | Handle claim | `claimCloudHandle`, `clearClaimedCloudHandle` | `403 MIGRATION_ORIGIN_DENIED` | | Operations | `createAdminIdentityProof` | `400` with a restriction message | Build on `createAction`, `signAction`, and `internalizeAction` instead. Those are portable to any BRC-100 wallet. ## Next steps * [Payments and actions](/brc-wallet/payments) * [Collectables](/brc-wallet/items) * [Tokens](/brc-wallet/tokens) * [Local bridge](/brc-wallet/local-bridge) # Signing and encryption Source: https://docs.handcash.io/brc-wallet/signing Derive app-scoped keys, sign and verify data, encrypt records, and work with certificates **Beta — BRC wallet only.** Desktop / Mobile over [BRC-100](https://brc.dev/100). The wallet holds the keys. Your app names a **protocol** and a **key ID**, and the wallet derives a key for that context, uses it, and returns the result. No private key ever leaves the wallet. Every cryptographic call takes the same three fields: | Field | Meaning | | - | - | | `protocolID` | `[securityLevel, protocolName]` — level `2` scopes the key to your app and counterparty | | `keyID` | Your identifier for this key inside the protocol, such as `invoice:7842` | | `counterparty` | `'self'`, `'anyone'`, or a specific identity public key | Use `'anyone'` when third parties must verify the result, `'self'` for records only your app and user read, and a specific key for pairwise data. ## Read the identity key ```typescript theme={null} const { publicKey: identityKey } = await wallet.getPublicKey({ identityKey: true }); ``` This is the user's stable identity across apps. Use it as the account handle in your backend. ## Derive an app-scoped key ```typescript theme={null} const protocolID: [number, string] = [2, 'robot foundry documents']; const { publicKey } = await wallet.getPublicKey({ protocolID, keyID: 'invoice:7842', counterparty: 'anyone' }); ``` Publish this public key with your data so anyone can verify signatures later. ## Sign and verify data ```typescript theme={null} const data = [...new TextEncoder().encode('Invoice 7842 approved')]; const { signature } = await wallet.createSignature({ protocolID, keyID: 'invoice:7842', counterparty: 'anyone', data }); const { valid } = await wallet.verifySignature({ protocolID, keyID: 'invoice:7842', counterparty: 'anyone', data, signature }); ``` Sign a hash instead of the payload when the data is large or private: ```typescript theme={null} const { signature } = await wallet.createSignature({ protocolID, keyID: 'invoice:7842', counterparty: 'anyone', hashToDirectlySign: sha256Bytes }); ``` Each signature is its own prompt. Batch what you can into one signed document rather than asking the user repeatedly. ## Encrypt a record ```typescript theme={null} const context = { protocolID: [2, 'robot foundry private records'] as [number, string], keyID: 'record:7842', counterparty: 'self' as const }; const { ciphertext } = await wallet.encrypt({ ...context, plaintext: [...new TextEncoder().encode('private payload')] }); const { plaintext } = await wallet.decrypt({ ...context, ciphertext }); ``` Store the ciphertext wherever you like — your server can hold data it cannot read. ## Authenticate a record with HMAC ```typescript theme={null} const { hmac } = await wallet.createHmac({ ...context, data }); const { valid } = await wallet.verifyHmac({ ...context, data, hmac }); ``` Encryption gives confidentiality; HMAC gives integrity. Use the one you actually need, not both by reflex. ## Certificates Forwarded on this release, and not part of the [integrator path](/brc-wallet/feature-map). No HandCash test shows a third-party certificate call succeeding. Leave this off an integration until that changes. Acquisition depends on your certifier's [BRC-52](https://brc.dev/52) flow. Once the user holds a certificate, reading and disclosing it is portable: ```typescript theme={null} const held = await wallet.listCertificates({ certifiers: [certifierIdentityKey], types: [certificateType], limit: 20, offset: 0 }); const proof = await wallet.proveCertificate({ certificate: held.certificates[0], fieldsToReveal: ['name', 'country'], verifier: verifierIdentityKey }); ``` `proveCertificate` prompts with the exact fields being revealed, so request the fewest that your check requires. ## Discover identities Forwarded on this release, and not part of the [integrator path](/brc-wallet/feature-map). The same applies to `revealCounterpartyKeyLinkage` and `revealSpecificKeyLinkage`. ```typescript theme={null} const byKey = await wallet.discoverByIdentityKey({ identityKey: counterpartyIdentityKey, limit: 10 }); const byAttributes = await wallet.discoverByAttributes({ attributes: { country: 'CA' }, limit: 20 }); ``` Results are certificates other parties hold, not wallet contents. Treat them as claims that are only as good as their certifier. ## Next steps * [Payments and actions](/brc-wallet/payments) * [Collectables](/brc-wallet/items) * [Permissions and scopes](/brc-wallet/permissions) # Tokens Source: https://docs.handcash.io/brc-wallet/tokens Read, mint, transfer, receive, and verify BSV-21 fungible tokens with the BRC wallet **Beta — BRC wallet only.** Desktop / Mobile over [BRC-100](https://brc.dev/100). ## The model A fungible holding is a set of **one-satoshi tips**, each carrying an integer amount in base units. The wallet keeps them in storage basket `bsv21`, and a balance is the sum of the tips it holds for one token id. | Concept | Value | | - | - | | Token id | The deploy output's outpoint, normalized as `_` | | Tip | A one-satoshi output carrying an amount of that token | | Amount | An integer in base units, as a string — apply `dec` only for display | The fungible-token path is **BSV-21 with BRC-162 binary value locks**. It provides the scaling model for token balances and transfers; there is no separate 1Sat fungible-token product surface. Token calls always prompt, and token view is a separate grant from item view. ## Read balances | Basket | Required tag | | - | - | | `p bsv21 all` | none | | `p bsv21 id` | `bsv21:` | ```typescript theme={null} const tokens = await wallet.listOutputs({ basket: 'p bsv21 all', includeCustomInstructions: true, limit: 50, offset: 0 }); ``` ```typescript theme={null} const robot = await wallet.listOutputs({ basket: 'p bsv21 id', tags: [`bsv21:${tokenId}`], tagQueryMode: 'all', includeCustomInstructions: true, limit: 20 }); ``` Sum the tips yourself; there is no per-token balance method. A third-party grant is an **allowlist of token ids**, so asking for `all` later never widens it — the user must approve each new token. `listOutputs({ basket: 'bsv21' })` returns `400 USE_PBSV21_SCOPE`. ## Token vocabulary ### Tags | Tag | Meaning | | - | - | | `bsv21` | Protocol marker | | `bsv21:` | Token id, the preferred form | | `amt:` | Face value of this tip in base units | | `op:` | `deploy+mint`, `deploy+auth`, `mint`, `auth`, `transfer`, or `burn` | | `sym:` | Ticker, lowercased, up to 32 characters | | `dec:<0-18>` | Display decimals | | `icon:` | Icon inscription output | | `issuer:` | Compressed issuer public key | | `cosign:` | Cosigner gate | | `id:` | Wallet-assigned row key — **not** the token id | ### `customInstructions` A JSON mirror of the protocol operation, which the wallet uses to index and display the tip: ```json theme={null} { "p": "bsv-20", "op": "transfer", "id": "", "amt": "2500", "sym": "ROBOT", "dec": 2, "issuer": "", "icon": "" } ``` `p` must be `bsv-20` for the mirror to parse. `amt` is required for any balance-bearing operation, `dec` may be `0`–`18`, and `maxSupply` is accepted as local remittance metadata only — it is not part of the on-chain BRC-162 output. Authority-only operations (`deploy+auth`, `auth`) carry no holdings and are not counted as balance. Symbol and decimals are **display metadata** carried by the deploy and the remittance. They are not a consensus rule, so never trust a ticker as a unique identifier — the token id is the identity. ## Mint a token An identity-backed mint is a `createAction` with an output in basket `bsv21` whose operation is `deploy+mint`, `deploy+auth`, or `mint`: ```typescript theme={null} const { txid } = await wallet.createAction({ description: 'Deploy and mint ROBOT', labels: ['bsv21', 'handcash-mint-token'], outputs: [ { lockingScript: bsv21DeployMintScript, // BRC-162 binary value lock satoshis: 1, outputDescription: 'ROBOT deploy and mint', basket: 'bsv21', tags: ['bsv21', 'op:deploy+mint', 'sym:ROBOT', 'amt:1000000', 'dec:2'], customInstructions: JSON.stringify({ p: 'bsv-20', op: 'deploy+mint', sym: 'ROBOT', amt: '1000000', dec: '2' }) } ] }); ``` When the user approves, HandCash recognizes the mint as identity-backed and enriches it: * It writes the wallet's identity key into the `issuer:` tag and `customInstructions`. * It appends a **Sigma** signature to the deploy locking script when the action's first input is an output the wallet can attest to. Wallets that later see that signature show the token as issuer-attested. * It reuses an existing icon for the same issuer and symbol when you did not supply one. The wallet does **not** build the script from your tags. A deploy with an amount of zero is an authority lock and is refused on the value path, and a mint that spends existing tips without `inputBEEF` fails rather than proceeding blind. The prompt is titled **Mint token** and shows the symbol, the supply, and that Pay and auto-pay do not cover it. ## Transfer tokens ```typescript theme={null} const { txid } = await wallet.createAction({ description: 'Send 25 ROBOT', labels: ['bsv21', 'send-token'], inputBEEF: tokenInputBeef, inputs: selectedTips.map((tip) => ({ outpoint: tip.outpoint, inputDescription: 'ROBOT tip', unlockingScript: tip.unlockingScript })), outputs: [ { lockingScript: recipientTransferScript, satoshis: 1, outputDescription: 'ROBOT transfer' // No sender basket: this tip belongs to the recipient. }, { lockingScript: changeTransferScript, satoshis: 1, outputDescription: 'ROBOT change', basket: 'bsv21', tags: ['bsv21', `bsv21:${tokenId}`, 'amt:7500', 'sym:ROBOT', 'dec:2'], customInstructions: JSON.stringify({ p: 'bsv-20', op: 'transfer', id: tokenId, amt: '7500', dec: 2 }) } ] }); ``` Invariants the wallet enforces, and your app must respect: 1. **Conservation per token id** — base units out never exceed base units in. Never round. 2. **Change is mandatory** when the selected tips exceed the amount you are sending. Dropping it destroys value. 3. **Every tip is one satoshi.** 4. **The payee output carries no sender basket**; only your change output goes back into `bsv21`. 5. **Parent bodies must be provable.** Supply `inputBEEF` covering the spent tips, or verification fails closed. 6. **Cosigned tips do not spend with a plain unlock**, and covenant-locked tips are refused outright. Failures are named rather than silent: insufficient units, a zero-amount authority spend, an invalid recipient, missing source transactions, conservation mismatches, and `BRC-176 prove failed` all surface as errors instead of a downgraded transaction. ## Burn tokens A burn destroys base units: it spends tips, emits **no** payee token output, and optionally returns change to the holder. The satoshis behind the burned tips are recovered to the wallet. Burn amounts must be positive integers. ## Receive tokens ```typescript theme={null} const { accepted } = await wallet.internalizeAction({ tx: atomicBeef, description: 'Receive 25 ROBOT', labels: ['bsv21', 'receive-token'], outputs: [ { outputIndex: 2, protocol: 'basket insertion', insertionRemittance: { basket: 'bsv21', tags: ['bsv21', `bsv21:${tokenId}`, 'amt:2500', 'sym:ROBOT', 'dec:2'], customInstructions: JSON.stringify({ p: 'bsv-20', op: 'transfer', id: tokenId, amt: '2500', sym: 'ROBOT', dec: 2 }) } } ] }); ``` The named output must be one satoshi, must pay the wallet, and must decode as a BRC-162 value lock matching the same token id and amount. Anything else is refused with a named reason such as an ambiguous token output or no token tip paying this wallet. ## Verification Token provenance is a BRC-176 walk: from a tip back to a fixed-supply deploy, checking that inputs cover outputs for that token id in every transaction along the way. It fails closed on a missing parent body, an over-transfer, a cycle, or an authority output. What that means for your app: * A tip the wallet produced on a send carries a proven subject BEEF. * Listing a holding does **not** prove it. Treat `issuerAttested` as an attestation that a signature matches the claimed issuer's address, not as a full supply audit. * A token with no symbol is displayed by a truncated token id, so ship a `sym:` tag if you want a readable name. ## Release a tip ```typescript theme={null} await wallet.relinquishOutput({ basket: 'p bsv21 all', output: tokenOutpoint }); ``` This drops local tracking only — it does not spend or destroy the output — and prompts as **Release token**. ## Not available to apps | Attempt | Result | | - | - | | `listOutputs({ basket: 'bsv21' })` | `400 USE_PBSV21_SCOPE` | | `getTokenIcon` | HandCash hosts only, `403 MARKET_ORIGIN_DENIED` | | Auto-pay covering a token mint, send, or release | Always prompts | | Cosigned token sends through the wallet's own path | Not wired yet | | Treating a row `id:` tag as the token id | Wrong key; use `bsv21:` | ## Next steps * [Collectables](/brc-wallet/items) * [Permissions and scopes](/brc-wallet/permissions) * [Signing and encryption](/brc-wallet/signing) # Developer Dashboard Source: https://docs.handcash.io/developer-dashboard Dashboard to manage organization, applications, billing and wallet ## Environment Setup Walkthrough Follow this step-by-step guide to set up your environment and get started with the Handcash Developer Dashboard. ### 1. Sign Up and Create a team Begin by signing up for a Handcash account. Once registered, create your team and invite team members to collaborate. **Steps:** * Navigate to the [Sign Up](https://dashboard.handcash.io) page. * After logging in, click on "Create Team." * Enter your team details and invite team members via email. ### 2. Create a New Application With your team set up, the next step is to create a new application within the dashboard. ### 3. Navigate to Your Application Once your application is created, you can access its details and configure various settings. Application #### Credentials Access your `AppId` and `AppSecret` required to utilize Handcash services. Credentials * **Important:** Keep your `AppSecret` secure and do not share it publicly. #### Profile Edit and update the details of your application to ensure it aligns with your branding and requirements. *** ## Need Help? If you encounter any issues or have questions, don't hesitate to reach out to our [Support Team](mailto:support@handcash.com). *** # Manage payment request Source: https://docs.handcash.io/handcash-pay/manage-payment-request # HandCash Payment Requests API - Corrected Documentation Based on real-world implementation and testing. This documentation corrects errors found in the official docs. ## API Base URL ``` https://cloud.handcash.io/v3/paymentRequests ``` ## Authentication Headers **IMPORTANT**: Headers are case-sensitive. Use lowercase: ``` app-id: your-target-app-id app-secret: your-target-app-secret ``` ## Create Payment Request **Method**: `POST`\ **URL**: `https://cloud.handcash.io/v3/paymentRequests` ### Required Fields | Field | Type | Description | Notes | | - | - | - | - | | `product` | Object | Product information | **Required** | | `product.name` | String | Product name | **Required** | | `product.description` | String | Product description | Optional but recommended | | `product.imageUrl` | String | Product image URL | Optional but recommended for UX | | `instrumentCurrencyCode` | String | Blockchain currency | **Must be "BSV" or "MNEE"** (NOT USD) | | `currency` | String | Display/denomination currency | Use "USD", "EUR", "GBP", etc. | | `receivers` | Array | Payment recipients | **Required** | | `receivers[].destination` | String | HandCash handle or wallet ID | **Required** | | `receivers[].sendAmount` | Number | Amount to send | **Required** | | `expirationType` | String | Expiration type | **Required**: "limit", "never", or "onPaymentCompleted" | ### Common Mistakes in Official Docs 1. **WRONG**: `denominationCurrencyCode` **CORRECT**: `currency` 2. **WRONG**: `instrumentCurrencyCode: "USD"` **CORRECT**: `instrumentCurrencyCode: "BSV"` or `"MNEE"` only 3. **WRONG**: `receivers[].currencyCode` field **CORRECT**: Do NOT include `currencyCode` in receivers array ### Working Example ```bash theme={null} curl --request POST \ --url https://cloud.handcash.io/v3/paymentRequests \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --header 'app-id: your-app-id' \ --header 'app-secret: your-app-secret' \ --data '{ "product": { "name": "Premium Subscription", "description": "Monthly premium access", "imageUrl": "https://example.com/product.jpg" }, "instrumentCurrencyCode": "BSV", "currency": "USD", "receivers": [ { "destination": "yourhandle", "sendAmount": 0.01 } ], "expirationType": "never", "redirectUrl": "https://yourapp.com/success" }' ``` ### Response Format ```json theme={null} { "id": "507f1f77bcf86cd799439011", "paymentRequestUrl": "https://pay.handcash.io/507f1f77bcf86cd799439011", "paymentRequestQrCodeUrl": "https://pay.handcash.io/api/paymentPreview/qr/507f1f77bcf86cd799439011", "isEnabled": true, "product": { "name": "Premium Subscription", "description": "Monthly premium access", "imageUrl": "https://example.com/product.jpg" }, "paymentAmount": { "amount": 0.01, "currencyCode": "USD" }, "receivers": [ { "destination": "yourhandle", "sendAmount": 0.01 } ], "createdAt": 1650558683, "expirationType": "never" } ``` ## List Payment Requests **Method**: `GET`\ **URL**: `https://cloud.handcash.io/v3/paymentRequests` ### Query Parameters (Optional) * `status` - Filter by status (e.g., "pending", "completed") ### Example ```bash theme={null} curl --request GET \ --url https://cloud.handcash.io/v3/paymentRequests \ --header 'Accept: application/json' \ --header 'app-id: your-app-id' \ --header 'app-secret: your-app-secret' ``` ### Response Returns array of payment request objects in `items` array. ## Get Single Payment Request **Method**: `GET`\ **URL**: `https://cloud.handcash.io/v3/paymentRequests/{id}` **IMPORTANT**: In Next.js 16+, you must await params: ```typescript theme={null} export async function GET( request: Request, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params; // Use id... } ``` ### Example ```bash theme={null} curl --request GET \ --url https://cloud.handcash.io/v3/paymentRequests/507f1f77bcf86cd799439011 \ --header 'Accept: application/json' \ --header 'app-id: your-app-id' \ --header 'app-secret: your-app-secret' ``` ## Update Payment Request **Method**: `PUT`\ **URL**: `https://cloud.handcash.io/v3/paymentRequests/{id}` ### Updatable Fields * `product.name` * `product.description` * `product.imageUrl` * `redirectUrl` * `notifications.webhook.webhookUrl` * `notifications.webhook.customParameters` * `notifications.email` * `expirationInSeconds` * `decreaseRemainingUnits` (only for `expirationType: "limit"`) ### Example ```bash theme={null} curl --request PUT \ --url https://cloud.handcash.io/v3/paymentRequests/507f1f77bcf86cd799439011 \ --header 'Accept: application/json' \ --header 'Content-Type: application/json' \ --header 'app-id: your-app-id' \ --header 'app-secret: your-app-secret' \ --data '{ "product": { "name": "Updated Product Name" } }' ``` ## Delete Payment Request **Method**: `DELETE`\ **URL**: `https://cloud.handcash.io/v3/paymentRequests/{id}` **IMPORTANT**: The API returns an empty response body on success. Do NOT attempt to parse as JSON. ### Example ```bash theme={null} curl --request DELETE \ --url https://cloud.handcash.io/v3/paymentRequests/507f1f77bcf86cd799439011 \ --header 'app-id: your-app-id' \ --header 'app-secret: your-app-secret' ``` ### Handling Delete Response ```typescript theme={null} const response = await fetch(`https://cloud.handcash.io/v3/paymentRequests/${id}`, { method: 'DELETE', headers: { 'app-id': process.env.TARGET_APP_ID!, 'app-secret': process.env.TARGET_APP_SECRET!, }, }); if (!response.ok) { const text = await response.text(); throw new Error(text || 'Failed to delete'); } // Success - response body is empty return { success: true }; ``` ## Common Issues & Solutions ### Issue: "instrumentCurrencyCode must be one of \[MNEE, BSV]" **Solution**: Never use "USD" for `instrumentCurrencyCode`. Only use "BSV" or "MNEE". ### Issue: "denominationCurrencyCode is not allowed" **Solution**: The field is called `currency`, not `denominationCurrencyCode`. ### Issue: "receivers\[0].currencyCode is not allowed" **Solution**: Do not include `currencyCode` in the receivers array. ### Issue: "Unexpected end of JSON input" on DELETE **Solution**: The DELETE endpoint returns an empty body. Handle this by reading as text first: ```typescript theme={null} const text = await response.text(); const data = text ? JSON.parse(text) : { success: true }; ``` ### Issue: "id must be a valid MongoDB ObjectId" **Solution**: In Next.js 16+, dynamic route params must be awaited: ```typescript theme={null} const { id } = await params; // NOT: const { id } = params; ``` ## Field Mapping Guide | Official Docs | Actually Works | Type | | - | - | - | | `denominationCurrencyCode` | `currency` | String | | `instrumentCurrencyCode: "USD"` | `instrumentCurrencyCode: "BSV"` or `"MNEE"` | String | | `App-Id` (uppercase) | `app-id` (lowercase) | Header | | `App-Secret` (uppercase) | `app-secret` (lowercase) | Header | | `receivers[].currencyCode` | ❌ Do not include | N/A | ## Complete Working Implementation See the `payment-requests-package` folder for a complete, tested implementation including: * `app/api/payment-requests/route.ts` - POST and GET handlers * `app/api/payment-requests/[id]/route.ts` - GET, PUT, DELETE handlers * `components/payment-request-management.tsx` - Full UI component All code has been tested and confirmed working with the HandCash API. # Introduction Source: https://docs.handcash.io/introduction Handcash Developer Tools - v3 SDK, Items, Business Wallet, and Wallet Infrastructure ## Handcash Developer Tools Integrate Handcash into your applications. **`@handcash/sdk`** covers **Connect** (auth, payments, items). **Wallet API** (embedded wallets) uses REST `/v1/waas/*` — see [Wallet API getting started](/wallet-api/getting-started). The **BRC wallet** (Desktop / Mobile, beta) starts at the [feature map](/brc-wallet/feature-map) ([feature-map.md](https://docs.handcash.io/brc-wallet/feature-map.md)): auth, payments, items, and plain BSV-21. The rest of the section is under [BRC wallet](/brc-wallet/overview). Beta — integrate auth, payments, items, and plain BSV-21 Authentication, Payments, Items, Business Tools Build native wallet infrastructure into your application Set up your development environment ## Quick Start ```bash theme={null} npm install @handcash/sdk ``` ```typescript theme={null} import { getInstance, Connect } from '@handcash/sdk'; const sdk = getInstance({ appId: 'your-app-id', appSecret: 'your-app-secret' }); ``` # Burn and Create Items Source: https://docs.handcash.io/v2/handcash-connect/burn-and-create Burn existing items and create new ones in a single operation **⚠️ Legacy SDK Documentation** This documentation uses our older SDK (`@handcash/handcash-connect`), but the v3 SDK does not support this feature yet. # Burn and Create Items Burn existing items and create new items in a single atomic operation using the HandCash Connect SDK. ## Prerequisites * Business wallet credentials (`authToken`) * Collection ID for the new items * Items to burn (must be owned by the app) * Item metadata and media for new items ## Setup ```typescript theme={null} import { HandCashMinter } from '@handcash/handcash-connect'; const handCashMinter = HandCashMinter.fromAppCredentials({ appId: process.env.HANDCASH_APP_ID, authToken: process.env.HANDCASH_AUTH_TOKEN, // Business wallet token appSecret: process.env.HANDCASH_APP_SECRET }); ``` ## Burn and Create Operation Perform both burn and create operations atomically: ```typescript theme={null} const burnAndCreateOrder = await handCashMinter.burnAndCreateItemsOrder({ burn: { origins: [ 'item-origin-1', 'item-origin-2' ] }, issue: { referencedCollection: '', items: [ { name: 'New Item 1', rarity: 'Rare', quantity: 1, mediaDetails: { image: { url: 'https://example.com/item1.png', contentType: 'image/png' } } }, { name: 'New Item 2', rarity: 'Common', quantity: 1, mediaDetails: { image: { url: 'https://example.com/item2.png', contentType: 'image/png' } } } ] } }); console.log('Burn and create order:', burnAndCreateOrder.id); ``` ## Burn Only (No Create) To burn items without creating new ones, leave the `issue` parameter empty or omit it: ```typescript theme={null} const burnOrder = await handCashMinter.burnAndCreateItemsOrder({ burn: { origins: [ 'item-origin-1', 'item-origin-2' ] } // No issue parameter = burn only }); console.log('Burn order:', burnOrder.id); ``` ## Use Cases ### Open Card Pack ```typescript theme={null} async function openCardPack(packOrigin: string) { // Generate random cards const newCards = [ { name: 'Card 1', rarity: 'Common', quantity: 1, mediaDetails: { image: { url: 'https://example.com/card1.png', contentType: 'image/png' } } }, { name: 'Card 2', rarity: 'Rare', quantity: 1, mediaDetails: { image: { url: 'https://example.com/card2.png', contentType: 'image/png' } } } ]; // Burn pack and create cards in one operation const order = await handCashMinter.burnAndCreateItemsOrder({ burn: { origins: [packOrigin] }, issue: { referencedCollection: '', items: newCards } }); return order; } ``` ### Crafting System ```typescript theme={null} async function craftItem(materialOrigins: string[], resultItem: any) { const order = await handCashMinter.burnAndCreateItemsOrder({ burn: { origins: materialOrigins }, issue: { referencedCollection: '', items: [resultItem] } }); return order; } ``` ## Important Notes * The operation is atomic - both burn and create happen together or not at all * Use item origins (not IDs) for the burn operation * The `referencedCollection` must be an existing collection ID * If you omit the `issue` parameter, only the burn operation is performed * Order processing is asynchronous (typically takes less than 5 seconds) ## Error Handling ```typescript theme={null} try { const order = await handCashMinter.burnAndCreateItemsOrder({ burn: { origins: ['item-origin-1'] }, issue: { referencedCollection: '', items: [/* new items */] } }); } catch (error) { if (error.message.includes('Invalid collection')) { console.log('Collection ID is invalid'); } else if (error.message.includes('Item not found')) { console.log('One or more items to burn do not exist'); } else if (error.message.includes('Permission denied')) { console.log('Invalid business wallet credentials'); } } ``` # Burn an Item Source: https://docs.handcash.io/v2/handcash-connect/burn-item Permanently destroy items with HandCash Connect **⚠️ Legacy SDK Documentation** This documentation uses our older SDK (`@handcash/handcash-connect`), but the v3 SDK does not support this feature yet. # Burn an Item Permanently destroy items using the HandCash Connect SDK. ## Prerequisites * User must be connected with the `ITEMS_WRITE` permission * Your application must be the creator of the item ## Setup ```typescript theme={null} import { HandCashConnect } from '@handcash/handcash-connect'; const handCashConnect = new HandCashConnect({ appId: '', appSecret: '', }); const account = handCashConnect.getAccountFromAuthToken(token); ``` ## Burn Items Burn one or more items permanently: ```typescript theme={null} // Burn a single item await account.items.burnItems({ items: [{ id: '' }], }); // Burn multiple items await account.items.burnItems({ items: [ { id: '' }, { id: '' }, { id: '' } ], }); ``` ## Important Notes * Burning an item permanently destroys it - this action cannot be undone * Only the app that created the item can burn it * Use item IDs (not origins) for burning * Burning is useful for consumable items, crafting systems, or removing items from circulation ## Error Handling ```typescript theme={null} try { await account.items.burnItems({ items: [{ id: '' }], }); console.log('Item burned successfully'); } catch (error) { if (error.message.includes('Item not found')) { console.log('Item does not exist or user does not own it'); } else if (error.message.includes('Permission denied')) { console.log('User needs burn permission'); } } ``` # Collection Management Source: https://docs.handcash.io/v2/handcash-connect/collection-management Create and manage collections for organizing digital items **⚠️ Legacy SDK Documentation** This documentation uses our older SDK (`@handcash/handcash-connect`), but the v3 SDK does not support this feature yet. # Collection Management This guide demonstrates how to create and manage collections using the HandCash Connect SDK. Collections are used to organize your digital items. ## Setup ```typescript theme={null} import { HandCashMinter } from '@handcash/handcash-connect'; const handCashMinter = HandCashMinter.fromAppCredentials({ appId: process.env.HANDCASH_APP_ID, authToken: process.env.HANDCASH_AUTH_TOKEN, appSecret: process.env.HANDCASH_APP_SECRET }); ``` ## Create a Collection To create a collection, use the `createCollectionOrder` method: ```typescript theme={null} async function createCollection() { const collectionOrder = await handCashMinter.createCollectionOrder({ name: 'My Game Collection', description: 'A collection of digital items for my game', mediaDetails: { image: { url: 'https://example.com/collection-image.png', contentType: 'image/png', }, }, }); console.log('Collection order created:', collectionOrder.id); // Wait for collection to be created (typically takes less than 5 seconds) await new Promise(resolve => setTimeout(resolve, 5000)); // Get the created collection const items = await handCashMinter.getOrderItems(collectionOrder.id); const collectionId = items[0].id; console.log(`Collection Created. ID: ${collectionId}`); return collectionId; } ``` ### Collection Order Response ```typescript theme={null} interface CollectionOrderResponse { id: string; user: string; // Your business wallet ID app: string; type: 'collection'; status: 'pending' | 'completed' | 'failed'; pendingInscriptions: number; error?: string; } ``` ## Get Collection Details Retrieve details of an existing collection: ```typescript theme={null} async function getCollection(collectionId: string) { // Collections are items themselves, so you can retrieve them like any other item const collection = await handCashMinter.getItem(collectionId); console.log('Collection details:', collection); return collection; } ``` ## Strategy * **Create a single top-level collection** for your app to keep things organized * **Use item attributes** to categorize sub-types (rarity, season, set, etc.) within your collection * **Keep immutable metadata minimal** and reference off-chain metadata via URLs for flexibility * **Save your collection ID** after creation - you'll need it when creating items ## Best Practices 1. Create your collection once during app initialization and store the collection ID 2. Use descriptive names and descriptions for your collections 3. Provide high-quality images for your collection media 4. Use item attributes to organize items within a collection rather than creating multiple collections # Friends Source: https://docs.handcash.io/v2/handcash-connect/friends Retrieve and manage user friends with HandCash Connect (Fallback for v3) **⚠️ Legacy SDK Documentation** This documentation uses our older SDK (`@handcash/handcash-connect`), but the v3 SDK does not support this feature yet. # Friends The Friends API allows you to retrieve a list of the user's friends and their public profiles. This requires the `FRIENDS` permission. ## Prerequisites * User must have authorized your app with the `FRIENDS` permission * User account must be initialized with an authentication token ## Setup ```javascript theme={null} import HandcashConnect from '@handcash/handcash-connect'; const handCashConnect = new HandcashConnect({ appId: 'YOUR_APP_ID', appSecret: 'YOUR_APP_SECRET', }); const account = handCashConnect.getAccountFromAuthToken(''); ``` ## Get Friends Retrieves a list of the user's friends and their public profiles. ```javascript theme={null} async getFriends(): Promise ``` **Returns:** A promise that resolves to an array of public profiles of the user's friends. **Example:** ```javascript theme={null} const friends = await account.profile.getFriends(); console.log(`You have ${friends.length} friends.`); friends.forEach(friend => { console.log(`Friend: ${friend.displayName} (@${friend.handle})`); }); ``` ## Response Type Each friend in the array is a `UserPublicProfile` object with the following structure: | Field | Type | Description | | - | - | - | | id | string | Unique identifier for the user | | handle | string | User's HandCash handle | | paymail | string | User's paymail address | | displayName | string | User's display name | | avatarUrl | string | URL of the user's avatar image | | createdAt | Date | Date when the profile was created | **Example Response:** ```javascript theme={null} [ { id: "507f1f77bcf86cd799439011", handle: "satoshi", paymail: "satoshi@handcash.io", displayName: "Satoshi Nakamoto", avatarUrl: "https://example.com/avatar.png", createdAt: new Date("2023-01-01T00:00:00.000Z") }, { id: "507f1f77bcf86cd799439012", handle: "alice", paymail: "alice@handcash.io", displayName: "Alice", avatarUrl: "https://example.com/alice.png", createdAt: new Date("2023-02-15T00:00:00.000Z") } ] ``` ## Permissions The `getFriends()` method requires the `FRIENDS` permission. Users must grant this permission when authorizing your app. To check if a user has granted the `FRIENDS` permission: ```javascript theme={null} const permissionsInfo = await account.profile.getPermissionsInfo(); const hasFriendsPermission = permissionsInfo.items.includes('FRIENDS'); if (hasFriendsPermission) { const friends = await account.profile.getFriends(); } else { console.log('User has not granted FRIENDS permission'); } ``` ## Error Handling ```javascript theme={null} try { const friends = await account.profile.getFriends(); // Process friends list } catch (error) { if (error.message.includes('FRIENDS permission')) { console.error('User has not granted FRIENDS permission'); // Redirect user to re-authorize with FRIENDS permission } else { console.error('Error fetching friends:', error); } } ``` ## Use Cases * Display user's friends list in your application * Show friend activity or status * Enable friend-to-friend features * Build social features around HandCash connections ## Important Notes * The `FRIENDS` permission must be requested during the initial authorization flow * If a user hasn't granted the `FRIENDS` permission, the method will throw an error * Friends data is read-only - you cannot add or remove friends through the API * The friends list reflects the user's current HandCash friends # Lock and Unlock Items Source: https://docs.handcash.io/v2/handcash-connect/manage-items Lock and unlock items to prevent transfers (Fallback for v3) **⚠️ Legacy SDK Documentation** This documentation uses our older SDK (`@handcash/handcash-connect`), but the v3 SDK does not support this feature yet. # Lock and Unlock Items Prevent items from being transferred by locking them, or unlock them to allow transfers again. ## Prerequisites * User must be connected with the `ITEMS_WRITE` permission * Your application must be the creator of the item ## Setup ```typescript theme={null} import { HandCashConnect } from '@handcash/handcash-connect'; const handCashConnect = new HandCashConnect({ appId: '', appSecret: '', }); const account = handCashConnect.getAccountFromAuthToken(token); ``` ## Lock Items You can prevent an item from being transferred by locking it: ```typescript theme={null} const origin = '0a3eb965a039cb15e731e2b1b2a67b7c024e6a6b59c1f7c32a9cec1d6b5bb7e7_48'; await account.items.lockItems(origin); ``` ### Lock Multiple Items ```typescript theme={null} const origins = [ 'item-origin-1', 'item-origin-2', 'item-origin-3' ]; for (const origin of origins) { await account.items.lockItems(origin); } ``` ## Unlock Items Later on, you can unlock items to allow transfers again: ```typescript theme={null} const origin = '0a3eb965a039cb15e731e2b1b2a67b7c024e6a6b59c1f7c32a9cec1d6b5bb7e7_48'; await account.items.unlockItems(origin); ``` ### Unlock Multiple Items ```typescript theme={null} const origins = [ 'item-origin-1', 'item-origin-2', 'item-origin-3' ]; for (const origin of origins) { await account.items.unlockItems(origin); } ``` ## Get Locked Items Retrieve a list of locked items: ```typescript theme={null} const lockedItems = await account.items.getLockedItems({ from: 0, to: 10, fetchAttributes: true, }); console.log(`User has ${lockedItems.length} locked items`); ``` ### Parameters * `from`: Starting index (default: 0) * `to`: Ending index (default: 10) * `fetchAttributes`: Whether to include item attributes (default: false) ## Use Cases ### Lock Valuable Items ```typescript theme={null} async function lockValuableItems() { // Get user's inventory (use v3 Connect.getItemsInventory for this) // Filter for valuable items const valuableOrigins = [ 'legendary-item-origin-1', 'epic-item-origin-2' ]; for (const origin of valuableOrigins) { await account.items.lockItems(origin); } console.log(`Locked ${valuableOrigins.length} valuable items`); } ``` ### Unlock Before Transfer ```typescript theme={null} async function transferItem(itemOrigin: string, destination: string) { // Unlock item first await account.items.unlockItems(itemOrigin); // Then transfer const result = await account.items.transfer({ destinationsWithOrigins: [ { destination: destination, origins: [itemOrigin] } ] }); return result.transactionId; } ``` ## Important Notes * Locked items cannot be transferred until they are unlocked * Only the app that created the item can lock/unlock it * Locking is useful for preventing accidental transfers of valuable items * Use `getLockedItems()` to check which items are currently locked # Send an Item Source: https://docs.handcash.io/v2/handcash-connect/send-item Transfer items between users with HandCash Connect (Fallback for v3) **⚠️ Legacy SDK Documentation** This documentation uses our older SDK (`@handcash/handcash-connect`), but the v3 SDK does not support this feature yet. # Send an Item Transfer items between HandCash users or to external wallets using the HandCash Connect SDK. ## Prerequisites * User must be connected with the `ITEMS_WRITE` permission * Your application must be the creator of the item for transfers ## Setup ```typescript theme={null} import { HandCashConnect } from '@handcash/handcash-connect'; const handCashConnect = new HandCashConnect({ appId: '', appSecret: '', }); const account = handCashConnect.getAccountFromAuthToken(token); ``` ## Transfer Items Transfer items to another HandCash user or an external wallet: ```typescript theme={null} // Transfer to another HandCash user via handle const result = await account.items.transfer({ destinationsWithOrigins: [ { destination: 'satoshi', origins: ['27c02c976adbf0acb212b850ce0c0b1b796de0a646c93304f94f2dc3249cad25_33'] } ] }); // Transfer to an external wallet via address const externalResult = await account.items.transfer({ destinationsWithOrigins: [ { destination: '18jMjdXWXYFNU2waM8wCepTiUzuiQ8gZHt', origins: ['27c02c976adbf0acb212b850ce0c0b1b796de0a646c93304f94f2dc3249cad25_33'] } ] }); console.log('Transaction ID:', result.transactionId); ``` ### Transfer Multiple Items to Same Destination ```typescript theme={null} const result = await account.items.transfer({ destinationsWithOrigins: [ { destination: 'recipient-handle', origins: [ 'item-origin-1', 'item-origin-2', 'item-origin-3' ] } ] }); ``` ### Transfer Items to Multiple Destinations ```typescript theme={null} const result = await account.items.transfer({ destinationsWithOrigins: [ { destination: 'user1', origins: ['item-origin-1', 'item-origin-2'] }, { destination: 'user2', origins: ['item-origin-3'] } ] }); ``` ## Important Notes * For item transfers, ensure your application has the necessary permissions and is the creator of the item * The `origins` array contains item origin IDs (the unique identifier for each item instance) * You can transfer to HandCash handles or external Bitcoin addresses * The method returns a transaction ID upon successful transfer ## Error Handling ```typescript theme={null} try { const result = await account.items.transfer({ destinationsWithOrigins: [ { destination: 'recipient-handle', origins: ['item-origin-id'] } ] }); console.log('Transfer successful:', result.transactionId); } catch (error) { if (error.message.includes('Item not found')) { console.log('Item does not exist or user does not own it'); } else if (error.message.includes('Invalid destination')) { console.log('Recipient handle or address is invalid'); } else if (error.message.includes('Permission denied')) { console.log('User needs transfer permission'); } } ``` # Business Payouts Source: https://docs.handcash.io/v3/business-wallet/payouts Manage business payouts and automated payment systems ## Suggested Prompt ```text theme={null} Read this documentation then integrate business payouts from the v3 SDK: https://docs.handcash.io/v3/business-wallet/payouts ``` ## Business Wallet Authentication Business payouts require a business wallet authentication token. You need to obtain this token through the Handcash Developer Dashboard for your business wallet. ```typescript theme={null} import { getInstance, Connect } from '@handcash/sdk'; const sdk = getInstance({ appId: 'your-app-id', appSecret: 'your-app-secret' }); // Get business wallet client using business auth token const businessClient = sdk.getAccountClient(''); ``` ## Send Business Payouts ### Basic Payout ```typescript theme={null} // Send payout from business wallet const payout = await Connect.pay({ client: businessClient, body: { instrumentCurrencyCode: 'BSV', denominationCurrencyCode: 'USD', receivers: [ { destination: 'recipient@handcash.io', sendAmount: 0.01 } ] } }); ``` ### Bulk Payouts ```typescript theme={null} // Send multiple payouts const bulkPayout = await Connect.pay({ client: businessClient, body: { instrumentCurrencyCode: 'BSV', denominationCurrencyCode: 'USD', receivers: [ { destination: 'user1@handcash.io', sendAmount: 0.01 }, { destination: 'user2@handcash.io', sendAmount: 0.02 } ] } }); ``` ## Business Wallet Management ### Check Balance ```typescript theme={null} // Get business wallet balance const balance = await Connect.getSpendableBalances({ client: businessClient }); console.log('Business wallet balance:', balance); ``` ### Exchange Rates ```typescript theme={null} // Get current exchange rates const rates = await Connect.getExchangeRate({ client: businessClient, path: { currencyCode: 'USD' } }); console.log('USD to BSV rate:', rates.data?.rate); ``` ## Error Handling ```typescript theme={null} try { const payout = await Connect.pay({ client: businessClient, body: { instrumentCurrencyCode: 'BSV', denominationCurrencyCode: 'USD', receivers: [ { destination: 'recipient@handcash.io', sendAmount: 0.01 } ] } }); if (payout.data) { console.log('Payout successful:', payout.data); } } catch (error) { console.error('Payout failed:', error.message); } ``` # Authentication Source: https://docs.handcash.io/v3/connect/authentication Authenticate users and manage permissions with Handcash Connect # HandCash Authentication Integration Guide ## Prerequisites 1. **Create an application** in the [Developer Dashboard](https://dashboard.handcash.io) 2. **Configure callback URLs** in your application settings 3. **Get your credentials** (`AppId` and `AppSecret`) ## Configure Callback URLs Configure callback URLs in [dashboard.handcash.io](https://dashboard.handcash.io): 1. Select your application 2. Go to **Settings** or **Configuration** 3. Set: * **Authorization Success URL**: Where users redirect after authorization (e.g., `https://yourapp.com/auth/success`) * **Authorization Failed URL**: Where users redirect if they decline (e.g., `https://yourapp.com/auth/failed`) **Important:** Use HTTPS in production. For local development, `http://localhost` is allowed. **`@handcash/sdk` runs in Node.js, not the browser.** It imports `node:crypto` at module scope (to sign requests with the user's key), so browser bundlers will fail on it or force you to polyfill `crypto`. Keep `getInstance` / `getAccountClient` / `Connect.*` on your server, and do the `window.location.href` redirect from the client. If you need the authorization URL on the client, build it on your backend and pass it through. ## Key Generation ### Required Dependency ```json theme={null} "@noble/secp256k1": "2.2.3" ``` ### Generate Key Pair ```typescript theme={null} import * as secp256k1 from "@noble/secp256k1" export function generateAuthenticationKeyPair() { const privateKey = secp256k1.utils.randomSecretKey() const publicKey = secp256k1.getPublicKey(privateKey, true) return { privateKey: Buffer.from(privateKey).toString("hex"), publicKey: Buffer.from(publicKey).toString("hex"), } } ``` ## Authentication Flow ### Step 1: Generate Keys ```typescript theme={null} const { privateKey, publicKey } = generateAuthenticationKeyPair() ``` ### Step 2: Store Private Key Store the private key securely: **Client:** `localStorage`, `sessionStorage`, or in-memory\ **Server:** Database, Redis, session, or JWT ```typescript theme={null} // Client example sessionStorage.setItem('handcash_private_key', privateKey) // Server example await db.users.update(userId, { handcashPrivateKey: privateKey }) ``` ### Step 3: Redirect to HandCash Build the authorization URL with the SDK rather than hardcoding it. The host and path are owned by Handcash and can change — `getRedirectionUrl()` is the only thing that stays correct: ```typescript theme={null} // Server-side: the SDK cannot be bundled for the browser (see the warning above). const sdk = getInstance({ appId: 'YOUR_APP_ID', appSecret: 'YOUR_APP_SECRET' }) const authUrl = sdk.getRedirectionUrl({ publicKey }) // Client-side: perform the actual redirect. window.location.href = authUrl ``` `getRedirectionUrl()` currently produces `https://app.handcash.io/#/authorizeApp?publicKey=&appId=` (your parameters in the order you pass them, then `appId`). HandCash redirects to your **Authorization Success URL** when the user grants access. The user will log in and authorize your app on HandCash. **Recommended: Use State Parameter for CSRF Protection** For enhanced security, include a `state` parameter to prevent CSRF attacks. See the [State Parameter Security](#state-parameter-security) section below. ### Step 4: Handle Callback After authorization, HandCash redirects to your **Authorization Success URL**. Retrieve the stored private key and validate: ```typescript theme={null} import { getInstance, Connect } from '@handcash/sdk' const privateKey = sessionStorage.getItem('handcash_private_key') // Retrieve stored key const sdk = getInstance({ appId: 'YOUR_APP_ID', appSecret: 'YOUR_APP_SECRET' }) const client = sdk.getAccountClient(privateKey) // Validate by fetching user profile const { data: profile } = await Connect.getCurrentUserProfile({ client }) console.log('User authenticated:', profile.publicProfile.handle) ``` ### Step 5: Store Private Key for Session Associate the private key with the user's session: ```typescript theme={null} req.session.handcashPrivateKey = privateKey // or await db.users.update(userId, { handcashPrivateKey: privateKey }) ``` ## How It Works 1. Generate a private/public key pair 2. Send the public key to HandCash during authorization 3. User authorizes your app on HandCash 4. Use the private key for all API calls - it serves as your authentication credential 5. HandCash validates the private key matches the authorized public key ## Check User Permissions ```typescript theme={null} const { data } = await Connect.getPermissions({ client }) console.log('User granted permissions:', data?.items) ``` ## Error Handling ```typescript theme={null} try { const client = sdk.getAccountClient(privateKey) const profile = await Connect.getCurrentUserProfile({ client }) } catch (error) { if (error.message.includes('Invalid token')) { // Invalid private key, redirect to re-authenticate const { publicKey } = generateAuthenticationKeyPair() window.location.href = sdk.getRedirectionUrl({ publicKey }) } } ``` ## State Parameter Security **Recommended:** Use the `state` parameter to prevent CSRF attacks and ensure the callback originated from your app session. ### What This Means in Practice #### 1️⃣ App Generates State When Starting Connect Generate a random, unguessable string and store it in your app session: ```typescript theme={null} import crypto from 'crypto' // Generate random state const state = crypto.randomBytes(32).toString('hex') // Store in session (server-side example) req.session.handcashState = state // Or client-side (sessionStorage) sessionStorage.setItem('handcash_state', state) ``` Include the state in the redirect URL: ```typescript theme={null} const authUrl = sdk.getRedirectionUrl({ publicKey, state }) window.location.href = authUrl ``` #### 2️⃣ Wallet Requires State The HandCash wallet will: * Require `state` to be present in the authorization request * Echo the same `state` back in the callback URL * Treat `state` as proof that the key came from the same app session that initiated connect #### 3️⃣ Wallet Redirects Back with Same State After authorization, HandCash redirects to your callback URL with the state included: ``` https://yourapp.com/auth/success?userId=...&publicKey=...&state=STATE ``` #### 4️⃣ App Validates State Validate that the state matches the one stored in your session: ```typescript theme={null} // Server-side example app.get('/auth/success', async (req, res) => { const callbackState = req.query.state const sessionState = req.session.handcashState // Validate state matches if (callbackState !== sessionState) { return res.status(403).send('Invalid state parameter - possible CSRF attack') } // Clear state from session delete req.session.handcashState // Continue with authentication... const privateKey = req.session.handcashPrivateKey // ... rest of authentication flow }) ``` ```typescript theme={null} // Client-side example const urlParams = new URLSearchParams(window.location.search) const callbackState = urlParams.get('state') const sessionState = sessionStorage.getItem('handcash_state') if (callbackState !== sessionState) { console.error('Invalid state parameter - possible CSRF attack') return } // Clear state sessionStorage.removeItem('handcash_state') // Continue with authentication... ``` **If state mismatch → reject the request** - This indicates a potential CSRF attack. ## Security Best Practices * **Use state parameter** - Always include and validate `state` to prevent CSRF attacks * **Store private keys securely** - Use encrypted storage or secure sessions * **Use HTTPS** - Always use HTTPS for redirect URLs * **Validate private keys** - Check validity before making API calls * **Protect the private key** - Treat it as sensitive authentication data # Sign in with Google Source: https://docs.handcash.io/v3/connect/google-auth-proxy Offer Google sign-in through the HandCash auth proxy and bootstrap a Connect authorization without your own Google OAuth client ## What it does Your app can offer **Sign in with Google** without registering its own Google OAuth client. HandCash owns the Google/Auth0 integration and returns a login URL. Once Google authenticates the user, HandCash Connect authorizes your app and returns to the success URL registered in the Developer Dashboard. Your app never receives a Google authorization code, access token, refresh token, client ID, or client secret. This is a convenience entry point into the standard [Connect authentication](/v3/connect/authentication) flow — it produces a HandCash Connect authorization and does not grant your app access to the user's Google account. ## Flow ```mermaid theme={null} sequenceDiagram participant A as Your app participant P as HandCash auth proxy participant G as Auth0 / Google participant C as HandCash Connect A->>A: Create ephemeral secp256k1 key pair and CSRF state A->>P: GET /api/auth/google/app?appId&returnTo P-->>A: JSON { authUrl, appId } A->>G: Open authUrl G-->>P: Authenticated provider session P->>C: /connect?appId&autoAuthorize&publicKey&state C-->>A: Registered authenticationSuccessUrl A->>A: Validate state and establish app session ``` ## Prerequisites 1. Create a HandCash Connect app in the [Developer Dashboard](https://dashboard.handcash.io). 2. Set its authentication success URL to `https:///auth/callback`. 3. Configure your backend: ```bash theme={null} HANDCASH_APP_ID=... HANDCASH_APP_SECRET=... HANDCASH_MARKET_URL=https://handcash.io ``` Use `https://preprod-market.handcash.io` only with matching preproduction app credentials. ## Start the flow On your backend: 1. Create a cryptographically random, single-use CSRF value. 2. Create a one-time secp256k1 key pair. 3. Store the private key and CSRF record in `HttpOnly`, `SameSite=Lax` cookies. Use `Secure` outside local HTTP development and expire both within 10–30 minutes. 4. Build this **proxy-relative** return path: ```text theme={null} /connect ?appId= &autoAuthorize=true &publicKey= &state= ``` Then request: ```http theme={null} GET https://handcash.io/api/auth/google/app ?appId= &returnTo= Accept: application/json ``` The response is: ```json theme={null} { "authUrl": "https://handcash.io/api/auth/login?returnTo=...&connection=google-oauth2", "appId": "" } ``` Return `authUrl` to browser JavaScript. Never return the temporary private key. `returnTo` is a path on HandCash, **not your callback URL**. External and protocol-relative URLs are rejected to prevent open redirects. Connect obtains the eventual callback from your app's registered `authenticationSuccessUrl`. ## Open the login URL Open `authUrl` in a popup on desktop. On mobile—or when popup creation is blocked—use a top-level navigation. The proxy selects Auth0's `google-oauth2` connection and resumes `/connect` after login. `autoAuthorize=true` removes an unnecessary second click when policy permits it; it does not bypass Connect consent, signing-key, or 2FA requirements. ## Complete your callback At `GET /api/auth/callback` on your origin: 1. Compare returned `state` to the short-lived CSRF cookie. Fail closed on a missing, expired, reused, or mismatched value. 2. Require the temporary private-key cookie. 3. Validate the Connect authorization by loading the current user profile with `HANDCASH_APP_ID`, `HANDCASH_APP_SECRET`, and the temporary private key. 4. Establish your application session. 5. Delete the temporary key and CSRF cookies. The temporary private key is the Connect credential generated by your app. It is not a Google credential. ## Endpoint distinction | Endpoint | Intended caller | Result | | - | - | - | | `GET /api/auth/google/app` | Connected applications | JSON `{ authUrl, appId }` | | `GET /api/auth/google` | HandCash itself | `302` into HandCash's Google login | Applications must call the `/app` endpoint. ## Common failures | Symptom | Cause | | - | - | | HTML instead of JSON | Wrong Market host or `/app` endpoint is not deployed | | Callback has no temporary cookie | Success URL host differs from the initiating host (`www`, apex, or scheme) | | Flow returns to `/` | `returnTo` was absolute, protocol-relative, or rejected | | Login works but Connect does not authorize | App ID, dashboard URLs, signing key, or 2FA state is invalid | | HTTP 400 | `appId` was omitted | | HTTP 500 | Proxy-side Auth0 configuration is unavailable | ## Reference implementation Soundbase is the app-side reference: * `app/api/auth/google/route.ts` — start; * `app/api/auth/callback/route.ts` — validate and establish the session; * `contexts/auth-context.tsx` — popup and mobile fallback; * `lib/auth-utils.ts` — ephemeral key generation. HandCash Market implements the proxy at `src/app/api/auth/google/app/route.ts`. ## Security requirements * The proxy validates `returnTo` against its own origin. * Your app binds one state and one ephemeral key to one short-lived flow. * The private key never appears in a URL or client-readable storage. * Your callback validates the Connect credential before issuing a session. * The proxy never forwards Google tokens to your app. * Popup completion checks `event.origin`; popup closure alone is not proof of successful login. Once the callback completes you hold an ordinary Connect authorization — see [Authentication](/v3/connect/authentication) for permissions and session handling. # Payments Source: https://docs.handcash.io/v3/connect/payments Send and receive payments with Handcash Connect ## Suggested Prerequisites * User authentication completed * Business wallet setup (for payouts) * Payment processing infrastructure * Error handling system ## Suggested Prompt ```text theme={null} Read this documentation then integrate payments from the v3 SDK: https://docs.handcash.io/v3/connect/payments ``` ## Send a Payment ### Basic Payment ```typescript theme={null} import { getInstance, Connect } from '@handcash/sdk'; const sdk = getInstance({ appId, appSecret }); const client = sdk.getAccountClient(authToken); const { data, error } = await Connect.pay({ client, body: { instrumentCurrencyCode: 'BSV', denominationCurrencyCode: 'USD', receivers: [ { destination: 'recipient-handle', sendAmount: 0.01 } ] } }); if (error) { console.error('Payment failed:', error); } else { console.log('Payment successful:', data); } ``` ### Payment with Description ```typescript theme={null} const { data, error } = await Connect.pay({ client, body: { instrumentCurrencyCode: 'BSV', denominationCurrencyCode: 'USD', description: 'Payment for services', receivers: [ { destination: 'recipient-handle', sendAmount: 10.00 } ] } }); ``` ### Multiple Recipients ```typescript theme={null} const { data, error } = await Connect.pay({ client, body: { instrumentCurrencyCode: 'BSV', denominationCurrencyCode: 'USD', receivers: [ { destination: 'user1', sendAmount: 5.00 }, { destination: 'user2', sendAmount: 3.00 }, { destination: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', sendAmount: 2.00 } ] } }); ``` ## Check Balances ### Get Spendable Balance ```typescript theme={null} const { data: balances } = await Connect.getSpendableBalances({ client }); console.log('Spendable balances:', balances); ``` ### Get All Balances ```typescript theme={null} const { data: allBalances } = await Connect.getBalances({ client }); console.log('All balances:', allBalances); ``` ## Exchange Rates ```typescript theme={null} const { data: rate } = await Connect.getExchangeRate({ client: sdk.client, // Use static client for exchange rates path: { currencyCode: 'USD' } }); console.log('USD exchange rate:', rate); ``` ## Get Payment Details ```typescript theme={null} const { data: payment } = await Connect.getPaymentDetails({ client, path: { transactionId: 'transaction-id' } }); console.log('Payment details:', payment); ``` ## Error Handling ```typescript theme={null} try { const { data, error } = await Connect.pay({ client, body: { /* payment details */ } }); if (error) { switch (error.code) { case 'INSUFFICIENT_FUNDS': console.log('User has insufficient funds'); break; case 'INVALID_DESTINATION': console.log('Invalid recipient address'); break; case 'PERMISSION_DENIED': console.log('User denied payment permission'); break; default: console.log('Payment failed:', error.message); } } } catch (err) { console.error('Unexpected error:', err); } ``` ## Common Use Cases ### Tipping System ```typescript theme={null} async function sendTip(recipientHandle: string, amount: number) { const { data, error } = await Connect.pay({ client, body: { instrumentCurrencyCode: 'BSV', denominationCurrencyCode: 'USD', description: 'Tip', receivers: [{ destination: recipientHandle, sendAmount: amount }] } }); return { success: !error, data, error }; } ``` ### Batch Payments ```typescript theme={null} async function sendBatchPayments(recipients: Array<{handle: string, amount: number}>) { const { data, error } = await Connect.pay({ client, body: { instrumentCurrencyCode: 'BSV', denominationCurrencyCode: 'USD', description: 'Batch payment', receivers: recipients.map(r => ({ destination: r.handle, sendAmount: r.amount })) } }); return { success: !error, data, error }; } ``` # User Profiles Source: https://docs.handcash.io/v3/connect/profiles Manage user profiles and social features with Handcash Connect ## Suggested Prerequisites * User authentication completed * Profile permissions granted * Social features enabled * Database for storing profile data ## Suggested Prompt ```text theme={null} Read this documentation then integrate user profiles from the v3 SDK: https://docs.handcash.io/v3/connect/profiles ``` ## Get Current User Profile ```typescript theme={null} import { getInstance, Connect } from '@handcash/sdk'; const sdk = getInstance({ appId, appSecret }); const client = sdk.getAccountClient(authToken); const { data: profile } = await Connect.getCurrentUserProfile({ client }); console.log('User profile:', profile); ``` ### Profile Data Structure ```typescript theme={null} interface UserProfile { publicProfile: { id: string; handle: string; paymail: string; displayName: string; avatarUrl: string; createdAt: string; }; privateProfile: { email: string; }; } ``` ## Get Public Profiles by Handle ```typescript theme={null} const { data: profiles } = await Connect.getPublicUserProfiles({ client: sdk.client, // Static client for public data body: { handles: ['user1', 'user2', 'user3'] } }); profiles.forEach(profile => { console.log(`${profile.handle}: ${profile.displayName}`); }); ``` ## Get User's Friends ```typescript theme={null} const { data: friends } = await Connect.getPublicUserProfiles({ client, body: { handles: [] // Empty array gets user's friends } }); console.log(`User has ${friends.length} friends`); ``` ## Check User Permissions ```typescript theme={null} const { data: permissions } = await Connect.getPermissions({ client }); console.log('Granted permissions:', permissions); // Check specific permission const hasPayPermission = permissions.includes('PAY'); const hasProfilePermission = permissions.includes('USER_PUBLIC_PROFILE'); ``` ## Common Use Cases ### User Dashboard ```typescript theme={null} async function getUserDashboard(authToken: string) { const client = sdk.getAccountClient(authToken); const [profile, balances, permissions] = await Promise.all([ Connect.getCurrentUserProfile({ client }), Connect.getSpendableBalances({ client }), Connect.getPermissions({ client }) ]); return { user: profile.data, balance: balances.data, permissions: permissions.data }; } ``` ### Friend Lookup ```typescript theme={null} async function findFriends(handles: string[]) { const { data: profiles } = await Connect.getPublicUserProfiles({ client: sdk.client, body: { handles } }); return profiles.map(profile => ({ handle: profile.handle, displayName: profile.displayName, avatar: profile.avatarUrl })); } ``` ### Permission Check ```typescript theme={null} function checkPermission(permissions: string[], required: string): boolean { return permissions.includes(required); } // Usage const canPay = checkPermission(permissions, 'PAY'); const canReadProfile = checkPermission(permissions, 'USER_PUBLIC_PROFILE'); ``` ## Error Handling ```typescript theme={null} try { const { data: profile } = await Connect.getCurrentUserProfile({ client }); } catch (error) { if (error.message.includes('Invalid token')) { // Redirect to re-authenticate res.redirect(sdk.getRedirectionUrl()); } else if (error.message.includes('Permission denied')) { // Handle insufficient permissions console.log('User needs to grant profile permission'); } } ``` ## Profile Display Examples ### Simple Profile Card ```typescript theme={null} function ProfileCard({ profile }: { profile: UserProfile }) { return { handle: profile.publicProfile.handle, displayName: profile.publicProfile.displayName, avatar: profile.publicProfile.avatarUrl, email: profile.privateProfile.email }; } ``` ### Friends List ```typescript theme={null} function FriendsList({ friends }: { friends: UserProfile[] }) { return friends.map(friend => ({ handle: friend.publicProfile.handle, displayName: friend.publicProfile.displayName, avatar: friend.publicProfile.avatarUrl })); } ``` # User Account Creation Source: https://docs.handcash.io/v3/connect/user-creation Create user accounts on behalf of users with the v3 SDK ## Suggested Prerequisites * Business wallet credentials * Email verification system * User consent management * Secure key storage system ## Suggested Prompt ```text theme={null} Read this documentation then integrate user account creation from the v3 SDK: https://docs.handcash.io/v3/connect/user-creation ``` ## Create User Account ### Basic Account Creation ```typescript theme={null} import { getInstance, Connect } from '@handcash/sdk'; const sdk = getInstance({ appId: 'your-app-id', appSecret: 'your-app-secret' }); // Create user account with email verification const account = await Connect.createUserAccount({ client: sdk.client, email: 'user@example.com', handle: 'username', displayName: 'User Display Name' }); console.log('Account created:', account.userId); ``` ### Account Creation with Custom Verification ```typescript theme={null} // Create account with custom verification email const account = await Connect.createUserAccount({ client: sdk.client, email: 'user@example.com', handle: 'username', displayName: 'User Display Name', customVerificationEmail: { subject: 'Welcome to Our App!', body: 'Please verify your account by clicking the link below...' } }); ``` ## Verify Email Address ### Send Verification Code ```typescript theme={null} // Send verification code to user's email const verification = await Connect.sendVerificationCode({ client: sdk.client, email: 'user@example.com' }); console.log('Verification code sent to:', verification.email); console.log('Request ID:', verification.requestId); ``` ### Verify Code ```typescript theme={null} // Verify the code provided by user const verified = await Connect.verifyEmailCode({ client: sdk.client, email: 'user@example.com', verificationCode: '123456', requestId: 'request-id-from-send-verification' }); if (verified.success) { console.log('Email verified successfully'); } ``` ## Handle Management ### Check Handle Availability ```typescript theme={null} // Check if handle is available const availability = await Connect.checkHandleAvailability({ client: sdk.client, handle: 'desired-handle' }); if (!availability.available) { console.log('Handle is not available, try another one'); } ``` ## Complete User Creation Flow ```typescript theme={null} async function createCompleteUser(email: string, handle: string, displayName: string) { try { // 1. Check handle availability const handleCheck = await Connect.checkHandleAvailability({ client: sdk.client, handle: handle }); if (!handleCheck.available) { throw new Error('Handle not available'); } // 2. Create user account const account = await Connect.createUserAccount({ client: sdk.client, email: email, handle: handle, displayName: displayName }); console.log('Wallet created successfully'); console.log('User ID:', account.userId); console.log('Handle:', account.handle); // 3. Send verification email const verification = await Connect.sendVerificationCode({ client: sdk.client, email: email }); return { account, verification }; } catch (error) { console.error('Error creating user:', error); throw error; } } ``` ## Error Handling ```typescript theme={null} try { const account = await Connect.createUserAccount({ client: sdk.client, email: 'user@example.com', handle: 'username', displayName: 'User Name' }); } catch (error) { if (error.message.includes('Handle already exists')) { console.log('Handle is already taken'); } else if (error.message.includes('Invalid email')) { console.log('Email format is invalid'); } else if (error.message.includes('Insufficient permissions')) { console.log('App needs user creation permissions'); } else { console.log('Unexpected error:', error.message); } } ``` # Getting Started Source: https://docs.handcash.io/v3/getting-started Complete guide to using the Handcash SDK v3 # Handcash SDK v3 The Handcash SDK v3 (`@handcash/sdk`) is a fully-featured Node.js library that provides seamless integration with Handcash services. It includes Connect, Items, Wallet API, and Business Wallet functionality. ## Quick Start ### Installation ```bash theme={null} npm install @handcash/sdk ``` ### Basic Setup ```typescript theme={null} import { getInstance, Connect } from '@handcash/sdk'; const sdk = getInstance({ appId: 'YOUR_APP_ID', appSecret: 'YOUR_APP_SECRET', }); // Get user client after authentication const client = sdk.getAccountClient(''); ``` ## Available Features ### 🔗 **Connect** - User Authentication & Payments * User authentication and profile management * Send and receive payments * Check balances and exchange rates * Manage user permissions ### 🎮 **Items** - Digital Items & NFTs * Create and manage digital items * Transfer items between users * Burn and craft items * Inventory management ### 💼 **Business Wallet** - Business Operations * Manage business payouts * Create and manage business wallets * Automated payment systems ### 🏪 **Wallet API** - User Wallet Creation * Create wallets for users with email verification * Manage user accounts programmatically * Custom verification flows ## Next Steps Choose a feature from the navigation menu to get started with specific functionality. Each section includes: * **Suggested Prompt** to help you find what you need * **Simple examples** for common use cases * **Complete API reference** for advanced usage # Create Items Source: https://docs.handcash.io/v3/items/creation Create and manage digital items with Handcash Items ## Suggested Prerequisites * Business wallet credentials * Item metadata and media assets * Collection management system * Item creation workflow ## Suggested Prompt ```text theme={null} Read this documentation then integrate item creation from the v3 SDK: https://docs.handcash.io/v3/items/creation ``` ## Prerequisites To create items, you need: * Business wallet credentials * Collection ID * Item metadata and media ## Create Items Order ### Basic Item Creation ```typescript theme={null} import { Items } from '@handcash/sdk'; const itemsClient = Items.fromAppCredentials({ appId: 'YOUR_APP_ID', authToken: 'YOUR_AUTH_TOKEN', // Business wallet token appSecret: 'YOUR_APP_SECRET' }); const creationOrder = await itemsClient.createItemsOrder({ collectionId: 'collection-id-here', items: [ { name: 'Legendary Sword', description: 'A powerful weapon forged in dragon fire', rarity: 'Legendary', quantity: 1, color: '#FF4500', attributes: [ { name: 'Attack', value: '100', displayType: 'number' }, { name: 'Element', value: 'Fire', displayType: 'string' }, { name: 'Durability', value: '95', displayType: 'number' } ], mediaDetails: { image: { url: 'https://example.com/sword.png', contentType: 'image/png' } } } ] }); console.log('Items order created:', creationOrder.id); ``` ### Create Multiple Items ```typescript theme={null} const creationOrder = await itemsClient.createItemsOrder({ collectionId: 'collection-id-here', items: [ { name: 'Health Potion', rarity: 'Common', quantity: 10, color: '#00FF00', attributes: [ { name: 'Healing', value: '50', displayType: 'number' }, { name: 'Type', value: 'Consumable', displayType: 'string' } ], mediaDetails: { image: { url: 'https://example.com/potion.png', contentType: 'image/png' } } }, { name: 'Magic Ring', rarity: 'Rare', quantity: 1, color: '#800080', attributes: [ { name: 'Magic Power', value: '75', displayType: 'number' }, { name: 'Enchantment', value: 'Protection', displayType: 'string' } ], mediaDetails: { image: { url: 'https://example.com/ring.png', contentType: 'image/png' } } } ] }); ``` ### Items with 3D Models ```typescript theme={null} const creationOrder = await itemsClient.createItemsOrder({ collectionId: 'collection-id-here', items: [ { name: '3D Dragon', rarity: 'Epic', quantity: 1, color: '#FF0000', attributes: [ { name: 'Size', value: 'Large', displayType: 'string' }, { name: 'Power', value: '1000', displayType: 'number' } ], mediaDetails: { image: { url: 'https://example.com/dragon-thumb.png', contentType: 'image/png' }, multimedia: { url: 'https://example.com/dragon-model.glb', contentType: 'application/glb' } } } ] }); ``` ### Items with Actions ```typescript theme={null} const creationOrder = await itemsClient.createItemsOrder({ collectionId: 'collection-id-here', items: [ { name: 'Mystery Box', rarity: 'Rare', quantity: 1, color: '#FFD700', attributes: [ { name: 'Type', value: 'Container', displayType: 'string' } ], mediaDetails: { image: { url: 'https://example.com/mystery-box.png', contentType: 'image/png' } }, actions: [ { name: 'Open', description: 'Open this box to reveal random items', url: 'https://yourgame.com/open-box', enabled: true } ] } ] }); ``` ## Item Creation Types ### CreateItemMetadata ```typescript theme={null} interface CreateItemMetadata { name: string; description?: string; rarity?: string; quantity: number; color?: string; attributes: ItemAttributeMetadata[]; mediaDetails: MediaDetails; actions?: Action[]; groupingValue?: string; externalId?: string; user?: string; // If not provided, items go to business wallet } interface ItemAttributeMetadata { name: string; value: string | number; displayType: 'string' | 'number' | 'date'; } interface MediaDetails { image: File; multimedia?: File; // For 3D models } interface File { url: string; contentType: string; } interface Action { name: string; description: string; url: string; enabled?: boolean; } ``` ## Common Use Cases ### Game Item Generator ```typescript theme={null} async function generateGameItems(collectionId: string, itemCount: number) { const items = []; for (let i = 0; i < itemCount; i++) { const rarity = ['Common', 'Rare', 'Epic', 'Legendary'][Math.floor(Math.random() * 4)]; const colors = ['#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF']; items.push({ name: `Generated Item ${i + 1}`, rarity, quantity: 1, color: colors[Math.floor(Math.random() * colors.length)], attributes: [ { name: 'Level', value: Math.floor(Math.random() * 100), displayType: 'number' }, { name: 'Type', value: 'Generated', displayType: 'string' } ], mediaDetails: { image: { url: `https://example.com/generated-${i}.png`, contentType: 'image/png' } } }); } return await itemsClient.createItemsOrder({ collectionId, items }); } ``` ### Batch Item Creation ```typescript theme={null} async function createItemBatch(collectionId: string, itemTemplates: any[]) { const items = itemTemplates.map(template => ({ name: template.name, rarity: template.rarity, quantity: template.quantity, color: template.color, attributes: template.attributes, mediaDetails: template.mediaDetails, externalId: template.externalId // For tracking in your system })); return await itemsClient.createItemsOrder({ collectionId, items }); } ``` ## Error Handling ```typescript theme={null} try { const creationOrder = await itemsClient.createItemsOrder({ collectionId, items: [/* item data */] }); } catch (error) { if (error.message.includes('Invalid collection')) { console.log('Collection ID is invalid'); } else if (error.message.includes('Insufficient permissions')) { console.log('Business wallet needs item creation permissions'); } else if (error.message.includes('Invalid media')) { console.log('Media URL or content type is invalid'); } } ``` ## Best Practices * **Use external IDs** to track items in your system * **Validate media URLs** before creating items * **Set appropriate rarity levels** for your game economy * **Use meaningful attribute names** for filtering * **Test item actions** before deploying * **Monitor creation orders** for completion status # Items Inventory Source: https://docs.handcash.io/v3/items/inventory Manage digital items and inventory with Handcash Items ## Suggested Prerequisites * User authentication completed * Items permissions granted * Item management system * Database for inventory tracking ## Suggested Prompt ```text theme={null} Read this documentation then integrate item inventory from the v3 SDK: https://docs.handcash.io/v3/items/inventory ``` ## Get Items Inventory ### Basic Inventory ```typescript theme={null} import { getInstance, Connect } from '@handcash/sdk'; const sdk = getInstance({ appId, appSecret }); const client = sdk.getAccountClient(authToken); const { data: items } = await Connect.getItemsInventory({ client, body: {} }); console.log(`User has ${items.length} items`); ``` ### Search Items ```typescript theme={null} const { data: items } = await Connect.getItemsInventory({ client, body: { searchString: 'dragon', limit: 20 } }); console.log(`Found ${items.length} items matching "dragon"`); ``` ### Filter by Collection ```typescript theme={null} const { data: items } = await Connect.getItemsInventory({ client, body: { collectionId: 'collection-id-here', limit: 50 } }); console.log(`Found ${items.length} items in collection`); ``` ### Get All Items Ungrouped By default, the inventory API groups identical items together. To get all individual items ungrouped (useful for displaying each item instance separately), set `group: false`: ```typescript theme={null} const { data: items } = await Connect.getItemsInventory({ client, body: { group: false, limit: 100 } }); console.log(`Found ${items.length} individual items (ungrouped)`); ``` This is particularly useful when you need to: * Display each item instance separately in your UI * Track individual item origins or IDs * Show the exact count of item instances rather than grouped quantities ### Advanced Filtering ```typescript theme={null} const { data: items } = await Connect.getItemsInventory({ client, body: { searchString: 'legendary', collectionId: 'collection-id', groupingValue: 'group-id', from: 0, to: 20, sort: 'name', order: 'asc' } }); ``` ## Item Data Structure ```typescript theme={null} interface Item { id: string; origin: string; name: string; description: string; imageUrl: string; multimediaUrl?: string; rarity: string; color: string; attributes: ItemAttribute[]; collection: { id: string; name: string; description: string; }; user: { alias: string; displayName: string; profilePictureUrl: string; }; app: { id: string; name: string; iconUrl: string; }; } interface ItemAttribute { name: string; value: string | number; displayType: 'string' | 'number' | 'date'; } ``` ## Common Use Cases ### User's Item Collection ```typescript theme={null} async function getUserCollection(authToken: string) { const client = sdk.getAccountClient(authToken); const { data: items } = await Connect.getItemsInventory({ client, body: { limit: 100 } }); // Group by collection const grouped = items.reduce((acc, item) => { const collectionName = item.collection.name; if (!acc[collectionName]) { acc[collectionName] = []; } acc[collectionName].push(item); return acc; }, {}); return grouped; } ``` ### Search by Rarity ```typescript theme={null} async function getRareItems(authToken: string, rarity: string) { const client = sdk.getAccountClient(authToken); const { data: items } = await Connect.getItemsInventory({ client, body: { searchString: rarity, limit: 50 } }); return items.filter(item => item.rarity.toLowerCase() === rarity.toLowerCase() ); } ``` ### Item Statistics ```typescript theme={null} async function getItemStats(authToken: string) { const client = sdk.getAccountClient(authToken); const { data: items } = await Connect.getItemsInventory({ client, body: { limit: 1000 } }); const stats = { total: items.length, byRarity: {}, byCollection: {}, byApp: {} }; items.forEach(item => { // Count by rarity stats.byRarity[item.rarity] = (stats.byRarity[item.rarity] || 0) + 1; // Count by collection const collectionName = item.collection.name; stats.byCollection[collectionName] = (stats.byCollection[collectionName] || 0) + 1; // Count by app const appName = item.app.name; stats.byApp[appName] = (stats.byApp[appName] || 0) + 1; }); return stats; } ``` ## Error Handling ```typescript theme={null} try { const { data: items } = await Connect.getItemsInventory({ client, body: { searchString: 'dragon' } }); } catch (error) { if (error.message.includes('Invalid token')) { // Redirect to re-authenticate res.redirect(sdk.getRedirectionUrl()); } else if (error.message.includes('Permission denied')) { // Handle insufficient permissions console.log('User needs to grant items permission'); } } ``` ## Display Examples ### Item Card Component ```typescript theme={null} function ItemCard({ item }: { item: Item }) { return { name: item.name, rarity: item.rarity, image: item.imageUrl, collection: item.collection.name, attributes: item.attributes.map(attr => ({ name: attr.name, value: attr.value, type: attr.displayType })) }; } ``` ### Inventory Grid ```typescript theme={null} function InventoryGrid({ items }: { items: Item[] }) { return items.map(item => ({ id: item.id, name: item.name, image: item.imageUrl, rarity: item.rarity, color: item.color })); } ``` # Configure Paymail Domain (Optional) Source: https://docs.handcash.io/wallet-api/configure-paymail Enable your users' wallets to be addressable by `alias@` from other wallets in the BSV ecosystem by configuring a custom paymail domain. ## Step 1: Add SRV Record Add an SRV record to your domain's DNS settings with the following details: * **Type:** SRV Record * **Name:** `_bsvalias._tcp` * **Value:** `10 10 443 cloud.handcash.io` ## Step 2: Enable DNSSEC Enable DNSSEC for your domain. This is typically done through your domain registrar or DNS provider's settings. ## Step 3: Update Paymail Domain 1. Go to the [HandCash developer dashboard](https://dashboard.handcash.io/) 2. Navigate to the Paymail domain settings 3. Update your Paymail domain configuration After completing these steps, your domain should be properly configured for Paymail use with HandCash Wallet as a Service. ## Example Configuration Here's an example of how to configure the SRV record using NameCheap.com: Example **Note:** The exact steps may vary depending on your domain registrar or DNS provider. Consult their documentation for specific instructions on adding SRV records and enabling DNSSEC. # Get access to an existing Wallet Source: https://docs.handcash.io/wallet-api/create-authorization-for-existing-account For users who already have an embedded wallet under your app, request email verification and **activate** their access key instead of creating a new account. Same headers as [Create a new wallet](/wallet-api/create-user-account): `app-id`, `app-secret`. ## Step 1: Request email code ```http theme={null} POST /v1/waas/account/requestEmailCode ``` ```json theme={null} { "email": "user@example.com" } ``` ## Step 2: Generate a new access key pair Generate a fresh key pair on your server (see [Create a new wallet](/wallet-api/create-user-account#step-2-generate-an-access-key-pair)). ## Step 3: Activate the access key ```http theme={null} POST /v1/waas/account/accessKey ``` ```json theme={null} { "email": "user@example.com", "accessPublicKey": "" } ``` ```javascript theme={null} await fetch('https://cloud.handcash.io/v1/waas/account/accessKey', { method: 'POST', headers: { 'Content-Type': 'application/json', 'app-id': process.env.HANDCASH_APP_ID, 'app-secret': process.env.HANDCASH_APP_SECRET, }, body: JSON.stringify({ email: 'user@example.com', accessPublicKey: publicKeyHex, }), }); ``` Store the **private** key securely, then use [request signing](/wallet-api/request-signing) for wallet endpoints. The difference from new account creation: use **`/v1/waas/account/accessKey`** instead of **`POST /v1/waas/account`**. # Create a new Wallet Source: https://docs.handcash.io/wallet-api/create-user-account Create embedded wallets with the Wallet API REST endpoints. Generate a secp256k1 **access key pair** on your server before starting. **Base URL:** `https://cloud.handcash.io`\ **Headers (account steps):** `app-id`, `app-secret`, `Content-Type: application/json` Wallet creation must be enabled for your App ID. Contact [sales@handcash.io](mailto:sales@handcash.io) if account creation fails with a feature or limit error. ## Step 1: Request email verification ```http theme={null} POST /v1/waas/account/requestEmailCode ``` ```json theme={null} { "email": "user@example.com" } ``` The user receives a verification code by email. Complete that flow before creating the account (HandCash validates the email when you call create account). ## Step 2: Generate an access key pair ```javascript theme={null} import { secp256k1 } from '@noble/curves/secp256k1'; const privateKey = secp256k1.utils.randomPrivateKey(); const privateKeyHex = Buffer.from(privateKey).toString('hex'); const publicKeyHex = Buffer.from(secp256k1.getPublicKey(privateKey, false)).toString('hex'); // Store privateKeyHex securely for this user — it signs all wallet API calls. ``` You can use `@noble/curves` only for key generation; Wallet API wallet calls do not require `@handcash/sdk`. ## Step 3: Check alias availability (optional) ```http theme={null} GET /v1/waas/account/aliasAvailability/{alias} ``` ## Step 4: Create the wallet account ```http theme={null} POST /v1/waas/account ``` ```json theme={null} { "email": "user@example.com", "alias": "myuser", "accessPublicKey": "" } ``` Response includes deposit info (paymail, address, etc.). See OpenAPI schema `wallet-depositInfoSchema`. ```javascript theme={null} const res = await fetch('https://cloud.handcash.io/v1/waas/account', { method: 'POST', headers: { 'Content-Type': 'application/json', 'app-id': process.env.HANDCASH_APP_ID, 'app-secret': process.env.HANDCASH_APP_SECRET, }, body: JSON.stringify({ email: 'user@example.com', alias: 'myuser', accessPublicKey: publicKeyHex, }), }); const depositInfo = await res.json(); ``` ## Step 5: Wallet operations Use the access **private** key to [sign requests](/wallet-api/request-signing) for `/v1/waas/wallet/*` — balances, pay, transactions, etc. See [Wallet operations](/wallet-api/manage-wallets). ## Sequence diagram ```mermaid theme={null} sequenceDiagram participant User participant App participant Handcash User->>App: Sign up request App->>Handcash: POST /v1/waas/account/requestEmailCode Handcash->>User: Verification email User->>App: Enters verification code App->>App: Generate access key pair App->>Handcash: GET aliasAvailability (optional) App->>Handcash: POST /v1/waas/account Handcash-->>App: depositInfo App-->>User: Wallet ready ``` # Configure Custom Verification Email (Optional) Source: https://docs.handcash.io/wallet-api/custom-verification-email You can enhance your user experience by customizing the verification email template. This guide explains how to implement a custom email template for the sign-up process. ## Implementation To use a custom email template, pass an options object as the second argument to the `requestSignUpEmailCode` function. This object should contain an `html` property with your custom HTML string. ### Key Points: 1. Pass `{ html: string }` as the second argument to `requestSignUpEmailCode`. 2. In your HTML string, use the literal `{code}` where you want the verification code to appear. 3. The verification server will automatically replace `{code}` with the actual verification code. ## Example Here's an example of how to implement a custom verification email: ```javascript theme={null} const customHtml = ` Welcome to My Application

Welcome to My Application!

Your verification code is: {code}

Enter this code to complete your registration. It will expire in 15 minutes.

`; export async function requestSignUpEmailCode(email: string, html: string) { return walletService.requestSignUpEmailCode(email, { html }); } ``` # Getting Started Source: https://docs.handcash.io/wallet-api/getting-started Build native wallet infrastructure into your application with email-based user onboarding The **Wallet API** (WaaS) provisions embedded HandCash wallets for your users via `/v1/waas/*`. It is separate from **[HandCash Connect](/v3/connect/authentication)**, which links users who already have HandCash. ## Setup 1. Create an application in the [Developer Dashboard](/developer-dashboard). 2. Save your **App ID** and **App Secret** for server-side calls. 3. Contact [sales@handcash.io](mailto:sales@handcash.io) to enable **wallet creation** for your App ID. 4. Use the API contract: * **OpenAPI:** [cloud.handcash.io/sdk-docs.json](https://cloud.handcash.io/sdk-docs.json) (filter **Wallet API**) * **Explorer:** [cloud.handcash.io/sdk-docs](https://cloud.handcash.io/sdk-docs/) * **Base URL:** `https://cloud.handcash.io` ## Overview | Capability | Endpoints | | - | - | | User onboarding | `POST /v1/waas/account/requestEmailCode`, `POST /v1/waas/account`, `POST /v1/waas/account/accessKey` | | Balances | `GET /v1/waas/wallet/balances` | | Payments | `POST /v1/waas/wallet/pay` | | Transaction history | `GET /v1/waas/wallet/transactions`, `GET /v1/waas/wallet/transaction` | | Deposit info | `GET /v1/waas/wallet/depositInfo` | Account setup uses **`app-id`** and **`app-secret`**. Wallet calls use **[request signing](/wallet-api/request-signing)** with the user’s access private key. ## Next steps * [Create a new wallet](/wallet-api/create-user-account) * [Access an existing wallet](/wallet-api/create-authorization-for-existing-account) * [Request signing](/wallet-api/request-signing) * [Wallet operations](/wallet-api/manage-wallets) * [Wallet API vs Connect](/wallet-api/waas-vs-connect) # Wallet operations Source: https://docs.handcash.io/wallet-api/manage-wallets Get balance, transaction history, and pay from embedded wallets Wallet endpoints live under `/v1/waas/wallet/*`. Every call must be **[signed](/wallet-api/request-signing)** with the user’s access private key, plus `app-id` and `app-secret`. ## Get deposit info ```http theme={null} GET /v1/waas/wallet/depositInfo ``` Signed `GET` with empty body. Response includes paymail, alias, and deposit address fields (see OpenAPI `wallet-depositInfoSchema`). ## Get wallet balance ```http theme={null} GET /v1/waas/wallet/balances ``` Signed `GET`. Returns spendable balances (BSV, MNEE, fiat equivalents per OpenAPI). ## Make a payment ```http theme={null} POST /v1/waas/wallet/pay ``` Signed `POST`. Request body matches Connect-style payments (`instrumentCurrencyCode`, `receivers`, `description`, etc.) — see **Wallet API** → `POST /v1/waas/wallet/pay` in [cloud.handcash.io/sdk-docs](https://cloud.handcash.io/sdk-docs/). ```javascript theme={null} const body = JSON.stringify({ instrumentCurrencyCode: 'BSV', denominationCurrencyCode: 'USD', description: 'Order #1234', receivers: [{ destination: 'merchant_handle', sendAmount: 1.0 }], }); const pathname = '/v1/waas/wallet/pay'; await fetch(`https://cloud.handcash.io${pathname}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'app-id': process.env.HANDCASH_APP_ID, 'app-secret': process.env.HANDCASH_APP_SECRET, ...signWaasRequest({ method: 'POST', pathname, body, accessPrivateKeyHex: process.env.USER_ACCESS_PRIVATE_KEY, }), }, body, }); ``` ## Transaction history ```http theme={null} GET /v1/waas/wallet/transactions GET /v1/waas/wallet/transaction ``` Signed `GET` calls. Query parameters and filters are defined in the OpenAPI spec. ## Self-custody variant If you registered a wallet with `POST /v1/waas/account/selfCustodial`, use `/v1/waas/wallet/selfCustody/*` with the **`wallet-xpub`** header instead of OAuth signing. See the OpenAPI **Wallet API** tag for those paths. # On Ramps Source: https://docs.handcash.io/wallet-api/on-ramps How to Get Money into Your Users Wallets There are several ways for users to deposit money into their new accounts: * **Other BSV Wallets** using `account.getDepositInfo()` * Paymail * Base58 Address * **Credit Card** * **Other Cryptocurrencies** While Paymail and address deposits are straightforward, this guide will cover how to add a widget for Credit Card and Cryptocurrency deposits. Both methods will use the `base58Address` provided in a user's deposit info, and the widget will conveniently pass it into the services to create a seamless user experience. A full example can be found [here](https://github.com/HandCash/external-wallet-demo/blob/master/src/components/wallet/TopUpDialog.tsx). Top Up Widget ## Cryptocurrency Deposit To support cryptocurrency deposits, you can use [LetsExchange](https://letsexchange.io). You will need an affiliate ID, or you may use the HandCash affiliate ID: `ZOTVxWxEZxW6dcfS`. Crypto Top Up ## Credit Card Top-Ups To support credit card top-ups, you can use [Guardarian](https://guardarian.com). You will need a public API token, or you can use the HandCash one: `797beecc-7f37-4f4f-9904-9bb77c38eb88`. Credit Card Here is a widget that handles both fiat and cryptocurrency top-ups using the [shadcn component library](https://ui.shadcn.com): ```typescript theme={null} 'use client'; import { AlertDialogCancel, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog'; import { IconCreditCard, IconCurrencyBitcoin, IconSwitchHorizontal, IconX, } from '@tabler/icons-react'; import { AlertDialog } from '@radix-ui/react-alert-dialog'; import React from 'react'; import InputTextField from '@/components/ui/InputTextField'; import { Button } from '@/components/ui/button'; import { cn } from "@/lib/utils"; import { useWallet } from '@/app/context/WalletContext'; type Props = { trigger: React.ReactNode; depositAddress: string; }; type LetsExchangeQueryParams = { affiliate_id: string; is_iframe: boolean; }; type GuardarianQueryParams = { partner_api_token: string; type: string; theme: string; default_from_amount: number; default_fiat_currency: string; default_crypto_currency: string; crypto_currencies_list: string; default_side: string; side_toggle_disabled: boolean; calc_background: string; body_background: string; button_background: string; create_nav_behaviour: string; without_box_shadow: boolean; }; const NEXT_PUBLIC_NEXT_EXCHANGE_AFFILIATE_ID = process.env.NEXT_PUBLIC_NEXT_EXCHANGE_AFFILIATE_ID as string; const NEXT_PUBLIC_GUARDARIAN_API_TOKEN = process.env.NEXT_PUBLIC_GUARDARIAN_API_TOKEN as string; function serializeQueryParams>(params: T): string { return Object.entries(params) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); } export default function TopUpDialog({ trigger, depositAddress }: Props) { const [selectedTopUpMethod, setSelectedTopUpMethod] = React.useState(); const { refreshBalances } = useWallet(); const guardarianQueryParams: GuardarianQueryParams = { partner_api_token: NEXT_PUBLIC_GUARDARIAN_API_TOKEN, type: 'narrow', theme: 'blue', default_from_amount: 50, default_fiat_currency: 'USD', default_crypto_currency: 'BSV', crypto_currencies_list: '%5B%7B%22ticker%22%3A%22BSV%22%2C%22network%22%3A%22BSV%22%7D%5D', default_side: 'buy_crypto', side_toggle_disabled: true, calc_background: 'hex_040404', body_background: 'hex_040404', button_background: 'hex_57ff97', create_nav_behaviour: 'new_tab', without_box_shadow: true, }; const letsExchangeQueryParams: LetsExchangeQueryParams = { affiliate_id: NEXT_PUBLIC_NEXT_EXCHANGE_AFFILIATE_ID, is_iframe: true, }; const onOpenChange = (open: boolean) => { refreshBalances(); setTimeout(() => setSelectedTopUpMethod(undefined), 500); }; return ( {trigger} Top-up your wallet {!selectedTopUpMethod && (
)} {selectedTopUpMethod === 'crypto' && (

Your BSV receiving address

)} {selectedTopUpMethod === 'fiat' && (

Your BSV receiving address

)}
); } ``` # Request signing Source: https://docs.handcash.io/wallet-api/request-signing Sign Wallet API wallet calls with the user access key Signed `/v1/waas/wallet/*` requests require: | Header | Description | | - | - | | `oauth-publickey` | Hex secp256k1 public key from the user’s access private key | | `oauth-signature` | DER signature | | `oauth-timestamp` | ISO 8601 timestamp | | `oauth-nonce` | Random string | Also send **`app-id`** and **`app-secret`** (see OpenAPI **Wallet API** security). The algorithm matches Connect’s `getAccountClient(authToken)` in `@handcash/sdk`, but the private key is the **access key** from wallet onboarding, not a Connect redirect token. ## Payload ``` {method} {pathname} {timestamp} {rawBody} {nonce} ``` SHA-256 hash the payload string (hex digest), then sign with the user’s access private key (secp256k1, DER). `pathname` is the path only (e.g. `/v1/waas/wallet/pay`). ## Node.js example ```javascript theme={null} import { createHash, randomBytes } from 'node:crypto'; import { secp256k1 } from '@noble/curves/secp256k1'; function signWaasRequest({ method, pathname, body = '', accessPrivateKeyHex }) { const timestamp = new Date().toISOString(); const nonce = randomBytes(16).toString('hex'); const publicKey = Buffer.from(secp256k1.getPublicKey(accessPrivateKeyHex, false)).toString('hex'); const signaturePayload = `${method}\n${pathname}\n${timestamp}\n${body}\n${nonce}`; const payloadHash = createHash('sha256').update(signaturePayload).digest('hex'); const signature = secp256k1.sign(payloadHash, accessPrivateKeyHex).toDERHex(); return { 'oauth-publickey': publicKey, 'oauth-signature': signature, 'oauth-timestamp': timestamp, 'oauth-nonce': nonce, }; } const pathname = '/v1/waas/wallet/balances'; const res = await fetch(`https://cloud.handcash.io${pathname}`, { method: 'GET', headers: { 'app-id': process.env.HANDCASH_APP_ID, 'app-secret': process.env.HANDCASH_APP_SECRET, ...signWaasRequest({ method: 'GET', pathname, accessPrivateKeyHex: process.env.USER_ACCESS_PRIVATE_KEY, }), }, }); ``` For `POST`, pass the **exact** JSON body string used in the request. ## OpenAPI client Generate a client from [cloud.handcash.io/sdk-docs.json](https://cloud.handcash.io/sdk-docs.json) for the **Wallet API** tag and add these headers in an interceptor. # Wallet API vs Connect Source: https://docs.handcash.io/wallet-api/waas-vs-connect Choose embedded wallets (WaaS) or Connect based on your users | | **Wallet API** | **HandCash Connect** | | - | - | - | | **User** | Your user (new or embedded) | Existing HandCash user | | **Onboarding** | Email → create account in your app | Redirect → authorize app | | **Wallet auth** | Sign with user **access private key** | `authToken` from redirect | | **Base paths** | `/v1/waas/*` | `/v3/connect/*`, `/v3/wallet/*`, … | | **npm `@handcash/sdk`** | **Not included** | `Connect`, `Items`, `Users`, … | ## Common mistake `import { WalletService } from '@handcash/sdk'` will fail — that class is not published. Connect wallet helpers are on **`Connect`** (`Connect.pay`, `Connect.getBalances`). Wallet API requires REST + signing or an OpenAPI-generated client. * Wallet API: [Getting started](/wallet-api/getting-started) * Connect: [v3 Connect authentication](/v3/connect/authentication)