Getting started
Install
npm install @monkeymask/react @monkeymask/wallet-standard
@monkeymask/react:MonkeyMaskProvider, hooks, wallet discovery.@monkeymask/wallet-standard: chain IDs, feature/operation types, SIWB build/verify, NFT codecs, error codes. Safe to import server-side.@monkeymask/core(optional): keys, signing, block publishing, and NFT operations as a plain library — for server-side wallets and anywhere the extension isn't available.
Quickstart
A complete component: connect the wallet, show the balance, and send BAN. This is the golden path. Everything else is a variation on it.
'use client';import {MonkeyMaskProvider,useMonkeyMask,useSend,} from '@monkeymask/react';// 1) Wrap your app once (see "Provider setup").export function App() {return (<MonkeyMaskProvider config={{ autoConnect: true }}><Wallet /></MonkeyMaskProvider>);}// 2) Connect + act.function Wallet() {const { connected, connecting, publicKey, connect, disconnect } = useMonkeyMask();const send = useSend();if (!connected) {return (<button disabled={connecting} onClick={() => connect()}>{connecting ? 'Connecting…' : 'Connect MonkeyMask'}</button>);}return (<div><div>{publicKey}</div><buttononClick={async () => {try {const { hash } = await send({ to: 'ban_1...', amount: '1.0' });console.log('sent', hash);} catch (e) {console.error(e); // user rejection => code 4001}}}>Send 1 BAN</button><button onClick={() => disconnect()}>Disconnect</button></div>);}
Provider setup
Wrap your app once. autoConnect silently reconnects previously-authorized origins.
// providers/index.tsx'use client';import { MonkeyMaskProvider } from '@monkeymask/react';export function Providers({ children }: { children: React.ReactNode }) {return (<MonkeyMaskProviderconfig={{autoConnect: true,onConnect: (publicKey) => console.log('connected', publicKey),onDisconnect: () => console.log('disconnected'),onError: (message) => console.error(message),}}>{children}</MonkeyMaskProvider>);}// app/layout.tsximport { Providers } from '@/providers';export default function RootLayout({ children }) {return (<html lang="en"><body><Providers>{children}</Providers></body></html>);}
Agent cheat sheet
Everything an AI agent needs to wire a dApp, condensed. Rules of thumb: every action hook returns a Promise. Always try/catch; a rejected request throws with code === 4001; amount is always a decimal BAN string; recipients accept a ban_… address or a .ban BNS name.
// 1. Install// npm install @monkeymask/react @monkeymask/wallet-standard// 2. Wrap once with <MonkeyMaskProvider config={{ autoConnect: true }}>// 3. State + actions all come from hooks:const { connected, connecting, publicKey, accounts, error,connect, disconnect } = useMonkeyMask();// --- Money -------------------------------------------------------------const send = useSend(); await send({ to, amount }); // { hash }await send({ sends: [{ to, amount }, ...] }); // airdrop -> { hashes, results }const sweep = useSweep(); await sweep({ to }); // send entire balanceconst receive = useReceive(); await receive(); // claim all pendingconst receivable = useReceivable(); await receivable(); // list claimableconst history = useAccountHistory(); await history(undefined, 10); // recent tx// --- Auth --------------------------------------------------------------const signIn = useSignIn(); await signIn(nonceInput); // SIWBconst signMsg = useSignMessage(); await signMsg(bytes); // raw ed25519// --- NFTs --------------------------------------------------------------const mint = useMintNFT(); await mint({ metadataCid, to, maxSupply });const edition = useMintEdition(); await edition({ metadataCid, to });const transfer = useTransferNFT(); await transfer({ assetRepresentative, to });const burn = useBurnNFT(); await burn({ assetRepresentative });const finish = useFinishSupply(); await finish({ metadataCid }); // lock collectionconst sendAll = useSendAllNfts(); await sendAll({ to }); // move every NFT// --- Names + URIs ------------------------------------------------------const { resolveBNS } = useMonkeyMask(); await resolveBNS('name.ban');buildBananoUri({ address, amount, label }); // -> "ban:ban_1...?amount=..."// Low level: signAndSendTransaction(op) accepts ANY BananoOperation (see Operations).
Core API
Hooks
useMonkeyMask() exposes the full context; the smaller hooks are ergonomic slices of it.
import {useMonkeyMask, // full context (state + all actions)useWallet, // { wallet, accounts, connected, connecting, installed }useConnect, // { connect, disconnect, connecting, connected }useAccounts, // { accounts, publicKey }useSignIn, // (input?) => BananoSignInOutputuseSignMessage, // (message: Uint8Array, account?) => outputuseSignTransaction, // (op, account?) => { signedBlock }useSignAndSendTransaction, // (op, account?) => { hash, hashes, results? }useSend, // (params, account?) => { hash, hashes, results? }: one or many recipientsuseReceive, // (params?, account?) => { hash, hashes }: claim receivablesuseReceivable, // (address?, count?) => BananoReceivable[]useAccountHistory, // (address?, count?, head?) => BananoHistoryEntry[]useReverseBNS, // (address?, tld?) => string[]: address → BNS name(s)useSweep, // ({ to, name? }, account?) => { hash }: send entire balanceuseSpendingSession, // { request, get, revoke }: per-origin auto-approve allowanceuseMintNFT, // (params, account?) => { hash, hashes }useMintEdition, // (params, account?) => { hash, hashes }: extra copy of a collectionuseTransferNFT, // (params, account?) => { hash, hashes, results? }: one or many NFTsuseBurnNFT, // (params, account?) => { hash, hashes }: destroy an NFT (send#burn)useFinishSupply, // (params, account?) => { hash }: lock a collection (#finish_supply)useSendAllNfts, // (params, account?) => { hash }: move every held NFT (send#all_nfts)// pure ban: payment-URI + QR helpersbuildBananoUri, parseBananoUri, isBananoUri, banToRaw, rawToBan,} from '@monkeymask/react';const {connected, connecting, installed, publicKey, accounts, error,connect, disconnect,signIn, signMessage, signTransaction, signAndSendTransaction,resolveBNS, reverseResolveBNS, getAccountInfo, getReceivable, getAccountHistory, clearError,} = useMonkeyMask();
Operations
Transactions are structured block intents, not opaque payloads. Pass one to signAndSendTransaction (build + sign + publish) or signTransaction (build + sign only). Most ops take a single target or an array, so one primitive covers a send and an airdrop.
type BananoOperation =// single payment...| { type: 'send'; to: string; amount: string; name?: string }// ...or a multi-send / airdrop| { type: 'send'; name?: string;sends: { to: string; amount: string; label?: string }[] }| { type: 'change'; representative: string }// claim all pending, or one specific receivable by hash| { type: 'receive'; blockHash?: string; name?: string }| { type: 'mint'; metadataCid: string; to: string;amount?: string; maxSupply?: number; name?: string;fees?: { to: string; amount: string; label?: string }[] }// mint an extra copy of a collection you issued (maxSupply > 1)| { type: 'mintEdition'; metadataCid: string; to: string;amount?: string; name?: string;fees?: { to: string; amount: string; label?: string }[] }// single NFT transfer...| { type: 'transfer'; assetRepresentative: string; to: string;amount?: string; name?: string }// ...or many at once| { type: 'transfer'; name?: string;transfers: { assetRepresentative: string; to: string; amount?: string }[] }// permanently destroy an NFT (send#burn to a black-hole account)| { type: 'burn'; assetRepresentative: string;to?: string; amount?: string; name?: string }// lock a collection you issued: no more editions can be minted| { type: 'finishSupply'; metadataCid: string; name?: string }// move every NFT the account holds to one recipient in a single block| { type: 'sendAllNfts'; to: string; amount?: string; name?: string }// send the entire spendable balance (claims pending first, no dust left)| { type: 'sweep'; to: string; name?: string };
signAndSendTransaction returns { hash, hashes, results? }: hashes lists every block published; the array forms of send/transfer also return results, one entry per recipient with its hash or error.
Accounts
Accounts update reactively. Read them from the hook. getAccountInfo proxies balance/representative/frontier info from the wallet.
const { accounts, publicKey } = useAccounts();// accounts[i].address -> ban_..., accounts[i].publicKey -> Uint8Arrayconst { getAccountInfo } = useMonkeyMask();const info = await getAccountInfo(); // current accountconst other = await getAccountInfo('ban_...'); // any account
Send & change
amount is a decimal BAN string. Both methods take an optional second argument to target a specific account.
const send = useSignAndSendTransaction();// Send BAN (recipient may be a ban_ address or a .ban BNS name)const { hash } = await send({ type: 'send', to: 'ban_1...', amount: '1.0' });// Change representative (delegates ORV voting weight — does not move funds)await send({ type: 'change', representative: 'ban_1...' });// Pick an under-delegated online rep (wallet extension: Settings → Voting representative)// Website Rep Explorer demo loads representatives + representatives_online from the node.// Sign only (returns the signed block without publishing)const signTx = useSignTransaction();const { signedBlock } = await signTx({ type: 'send', to: 'ban_1...', amount: '0.1' });
Representatives (ORV)
Banano uses Open Representative Voting. Your account balance contributes voting weight to whichever ban_ node you delegate to—representatives cannot spend your funds. MonkeyMask blocks rep changes while an NFT metaprotocol operation is in flight or when your account rep is reserved for supply/mint protocol state.
// Extension: drawer → Settings → Voting representative// Demo: home page → Representative Explorerawait signAndSendTransaction({ type: 'change', representative: 'ban_1...' });
Send & airdrop
useSend covers both a single payment and a multi-send / airdrop with one method. Pass { to, amount } or a sends array. The wallet verifies the balance covers the total up front, then publishes the airdrop as a single locally-chained block sequence: it reads account_info once, tracks the frontier and balance in memory, and pre-computes proof-of-work for the next block while the current one broadcasts (no per-block round-trip, no frontier race). It runs best-effort: a failed recipient is skipped, not fatal, and Banano can't publish an atomic batch, so results reports each recipient's hash or error. Recipients accept ban_… or .ban names.
import { useSend } from '@monkeymask/react';const send = useSend();// Single payment:const { hash } = await send({ to: 'ban_1...', amount: '1' });// Airdrop (many recipients, one approval, best-effort):const { hashes, results } = await send({name: 'Community airdrop',sends: [{ to: 'ban_1...', amount: '1', label: 'Winner #1' },{ to: 'name.ban', amount: '1', label: 'Winner #2' },{ to: 'ban_3...', amount: '5' },],});// results: { to, amount, hash? | error? }[]: one entry per recipient
Receive & history
useReceivable lists an account's pending (claimable) blocks and useReceive claims them. Pass a blockHash to claim one specific receivable, or nothing to claim them all. It publishes one receive/open block per claim and returns every hash. useAccountHistory reads recent confirmed transactions. The read hooks default to the current account; pass an address to query another.
MonkeyMask also auto-claims pending funds whenever the wallet refreshes. These primitives are for dApps that need to force a claim on demand.
import { useReceive, useReceivable, useAccountHistory } from '@monkeymask/react';const receive = useReceive();const getReceivable = useReceivable();const getHistory = useAccountHistory();// What can I claim?const pending = await getReceivable();// pending: { hash, amount, amountRaw, source? }[]// Claim everything...const { hashes } = await receive();// ...or just one:await receive({ blockHash: pending[0].hash });// Recent activityconst history = await getHistory(undefined, 10);// history: { hash, type, amount, account, timestamp }[]
Sweep (send max)
useSweep empties an account into one recipient. It claims any pending blocks first, then sends the full confirmed balance in raw so no dust is left behind. Returns { hash }.
const sweep = useSweep();await sweep({ to: 'coldwallet.ban' }); // claims pending, then sends everything
Payment URIs & QR codes
Pure helpers build and parse ban: URIs (the Banano flavour of the Nano/BIP21 scheme). They speak BAN decimals at the edges and convert to/from raw internally (no bananojs needed), so you can generate scannable payment codes anywhere.
import { buildBananoUri, parseBananoUri } from '@monkeymask/react';const uri = buildBananoUri({ address: 'cosmic.ban', amount: '1.5', label: 'Coffee' });// -> "ban:ban_1...?amount=150000000000000000000000000000&label=Coffee"const req = parseBananoUri(uri); // { address, amount: '1.5', label: 'Coffee' }// Pay it (single send):const send = useSend();await send({ to: req.address, amount: req.amount ?? '0', name: req.label });
Render the URI as a QR with any QR library (e.g. qrcode). Pasting a ban: URI into the extension's Send screen auto-fills the recipient and amount.
Spending sessions
A dApp can request a per-origin allowance so small sends are auto-approved (no popup) until the limit or expiry is reached. The user approves the allowance once; each auto-approved send is debited from the remaining balance. Only single-recipient sends within the limit qualify. Anything larger, or any other operation, still prompts.
Auto-confirmation is an advanced, opt-in feature that is off by default. The user must explicitly enable it (Settings → Advanced, or via the warning shown when a site first requests one) before any allowance can be granted, so session.request(...) will reject unless the user turns it on and approves. While the feature is off, every payment always prompts.
The user stays in control: an allowance can be revoked at any time from the wallet's Connected Sites screen (which shows the limit, spent, remaining, and expiry per site), disconnecting a site clears its allowance, and turning the feature off in Settings revokes all active allowances at once. So the wallet, not the dApp, is the ultimate kill switch; session.revoke() is just a convenience for dApps that want to offer it.
const session = useSpendingSession();// Ask the user for a 5 BAN / 30-minute allowanceawait session.request({ limit: '5', durationMs: 30 * 60_000 });// Check / revoke itconst active = await session.get(); // { address, limit, spent, remaining, expiresAt } | nullawait session.revoke();// While active, tiny sends go through without a prompt:const send = useSend();await send({ to: 'game.ban', amount: '0.01' }); // auto-approved
BNS names
Resolve Banano Name System (.ban) names to addresses. Recipients in send/mint/transfer operations also accept BNS names directly.
const { resolveBNS } = useMonkeyMask();const address = await resolveBNS('mycoolname.ban'); // -> ban_1...
Reverse resolution goes the other way: address → name(s). It's best-effort (an address can have several names, or none) and crawls the ledger, so it's slower than a forward lookup. Pass no address to look up the connected account.
const reverse = useReverseBNS();const names = await reverse(); // your account -> ['mycoolname.ban']const other = await reverse('ban_1...', 'ban'); // optional TLD filter
Authentication
Sign In With Banano
SIWB proves account ownership without a transaction. The server issues a nonce, the wallet signs an ABNF message, and the server verifies it with verifySignIn. Bind the message to the request host, and treat the nonce as single-use.
// Clientimport { useSignIn } from '@monkeymask/react';const signIn = useSignIn();const input = await fetch('/api/auth/nonce').then((r) => r.json());const output = await signIn(input);const res = await fetch('/api/auth/verify', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ input, output }),}).then((r) => r.json());// res.valid === true, res.address, res.sessionToken (also set as httpOnly cookie)
// Server (app/api/auth/verify/route.ts)import { deserializeSignInOutput, verifySignIn } from '@monkeymask/wallet-standard';const bananojs = require('@bananocoin/bananojs');const output = deserializeSignInOutput(rawOutput);const valid = verifySignIn(input, output, bananojs.BananoUtil, { expectedDomain });
Sign message
Raw message signing returns the message plus its ed25519 signature.
const signMessage = useSignMessage();const bytes = new TextEncoder().encode('gm banano');const { signedMessage, signature } = await signMessage(bytes);
NFTs
MonkeyMask mints Banano NFTs using the 73-meta-tokens metaprotocol: the wallet publishes a change#supply block followed by a send#mint block, and the mint block hash becomes the asset representative. Art + metadata are pinned to IPFS by your app (v0 Qm… or v1 b… sha2-256 CIDs).
The pinned metadata follows the standard ERC-721 / ERC-1155 JSON shape (name, description, image, optional attributes). The metaprotocol itself only reads the CID, so traits are purely for display. The artwork's MIME type is auto-detected and stored under properties.content_type.
Mint
import { useMintNFT } from '@monkeymask/react';const mint = useMintNFT();// 1) Pin art + metadata to IPFS (server route; needs PINATA_JWT).const form = new FormData();form.append('file', file);form.append('name', 'My NFT');form.append('description', 'Minted with MonkeyMask');// Optional ERC-721 traits (shown by wallets/marketplaces):form.append('attributes', JSON.stringify([{ trait_type: 'Background', value: 'Volcano' },]));const { metadataCid, imageCid } = await fetch('/api/ipfs', {method: 'POST', body: form,}).then((r) => r.json());// 2) Mint + send. hash = the asset representative;// hashes = [mint, ...feeSends].const { hash } = await mint({metadataCid,to: publicKey, // mint to yourself, or any ban_/.ban recipientmaxSupply: 1,name: 'My NFT',});
Mint fees & pricing
Building a mint platform? Attach a fees array to the mint. Each entry is a plain send published after a successful mint, and the wallet verifies the balance covers the mint plus every fee before publishing anything, so a failed mint never costs the user a fee. Every leg is itemized in the approval UI. Fees are opt-in and set by the calling app (honor system): charge your own mint price and, if you like, a MonkeyMask protocol fee.
const { hash } = await mint({metadataCid,to: buyer,name: 'My NFT',fees: [{ to: PLATFORM_TREASURY, amount: '10', label: 'Mint price' },{ to: MONKEYMASK_TREASURY, amount: '19', label: 'MonkeyMask fee' },],});
Editions
Mint a collection with maxSupply > 1 (0 = unlimited) to allow multiple copies. Later, mint additional editions of a collection you issued with useMintEdition. The wallet reuses the collection's metadata and rejects the mint if the edition limit is reached or the collection has been finished. Each edition is its own self-delimiting change#supply → send#mint pair, so an ordinary send can never be miscounted as an edition, and each copy gets its own asset representative.
const mint = useMintNFT();const mintEdition = useMintEdition();// Create a 10-copy edition collection (first copy minted now)const { hash } = await mint({ metadataCid, to, maxSupply: 10 });// Mint another copy laterawait mintEdition({ metadataCid, to: recipient });
Transfer
Send an owned NFT to another account. The wallet pockets any pending balance for the asset, then publishes a send#asset block: a normal send whose representative is the asset representative, which the indexer follows to move ownership. Pass a transfers array to move several NFTs in one approval: the wallet pockets pending once, publishes each block as a locally-chained sequence (frontier tracked in memory, work pre-computed for the next block while the current one broadcasts), then restores your representative. hashes lists each block and results reports per-NFT success/failure.
import { useTransferNFT } from '@monkeymask/react';const transfer = useTransferNFT();// One NFT (assetRepresentative is the NFT's id = its mint block hash):const { hash } = await transfer({assetRepresentative: nft.assetRepresentative,to: 'ban_1...', // or a .ban namename: nft.name, // shown in the approval UI});// Or many at once (best-effort; results itemizes each NFT):const { hashes, results } = await transfer({transfers: [{ assetRepresentative: a.assetRepresentative, to: 'ban_1...' },{ assetRepresentative: b.assetRepresentative, to: 'name.ban' },],});// results: { assetRepresentative, to, amount, hash? | error? }[]
Burn
Permanently destroy an owned NFT. This is a send#asset to a canonical burn account (the 73-meta-tokens send#burn convention): a black-hole address with no recoverable key, so the asset can never be moved again. The wallet surfaces a distinct red, destructive confirmation. Irreversible. The default target is the canonical burn account; pass to only to pick a different recognized burn address.
import { useBurnNFT } from '@monkeymask/react';const burn = useBurnNFT();const { hash } = await burn({assetRepresentative: nft.assetRepresentative,name: nft.name, // shown in the approval UI});
Finish (lock a collection)
Lock a collection you issued so no further editions can ever be minted (73-meta-tokens #finish_supply). The wallet publishes a change block whose representative encodes the collection's supply-block height; afterwards useMintEdition for this collection is refused. Existing copies are unaffected.
import { useFinishSupply } from '@monkeymask/react';const finish = useFinishSupply();const { hash } = await finish({ metadataCid: nft.metadataCid, name: nft.name });
Send all NFTs
Move every NFT the account holds to one recipient in a single block (73-meta-tokens send#all_nfts). The wallet pockets pending assets first, publishes one send whose representative is the "send all NFTs" marker, then restores a clean representative so later ordinary sends aren't treated as send-all.
import { useSendAllNfts } from '@monkeymask/react';const sendAll = useSendAllNfts();const { hash } = await sendAll({ to: 'ban_1...' }); // or a .ban name
Read / query
Fetch normalized NFTs for an address from /api/nfts. Ownership is read directly from the account's own ledger chain (a bounded set of batched account_history + blocks_info calls), so NFTs minted on any site appear for the minter and every recipient with no crawler, index, or backend required. IPFS metadata is resolved server-side.
const { nfts, error } = await fetch(`/api/nfts?address=${address}`).then((r) => r.json());// nfts: {// id, name, description?, image?, collection?, assetRepresentative?, metadataCid?,// supplyType?: 'unique' | 'limited' | 'unlimited',// maxSupply?, mintedCount?, heldCount?,// }[]
Gating
Token-gate content by combining Sign In With Banano (proves the visitor controls the address) with the crawler-free ownership read. The rule that keeps it secure: resolve the address from the SIWB session, never from the client, then scan that address server-side. Only confirmed, non-pending holdings should grant access.
Gating on metadata CID alone is weak: anyone can mint a new edition that points at the same IPFS document. For a strong gate, also require an issuer address and verify on-chain that each held NFT's mint block was published on that issuer's account as a valid change#supply → send#mint pair (the mint block's block_account must match). Do not trust properties.issuer in metadata JSON alone.
// Server: unlock only for authentic collection holdersimport { accountHoldsCollection, getSessionAddress } from '@/lib/gating';export async function GET(request: Request) {const address = await getSessionAddress(request); // from SIWB session cookieif (!address) return Response.json({ unlocked: false }, { status: 401 });const params = new URL(request.url).searchParams;const { holds } = await accountHoldsCollection(address, {collection: params.get('collection') ?? undefined, // metadata CIDissuer: params.get('issuer') ?? undefined, // on-chain minter account});if (!holds) return Response.json({ unlocked: false }, { status: 403 });return Response.json({ unlocked: true, content: /* members-only payload */ {} });}
Reference
Operation reference
Every operation, its ergonomic hook, and what it resolves to. All action hooks accept an optional trailing account argument and go through the wallet approval UI (unless a spending session auto-approves a small send).
| Operation | Hook | Returns |
|---|---|---|
send | useSend | { hash, hashes, results? } |
change | useSignAndSendTransaction | { hash } |
receive | useReceive | { hashes } |
mint | useMintNFT | { hash, hashes } |
mintEdition | useMintEdition | { hash, hashes } |
transfer | useTransferNFT | { hash, hashes, results? } |
burn | useBurnNFT | { hash, hashes } |
finishSupply | useFinishSupply | { hash } |
sendAllNfts | useSendAllNfts | { hash } |
sweep | useSweep | { hash } |
Read-only helpers (no approval): useReceivable, useAccountHistory, resolveBNS, useReverseBNS, getAccountInfo.
Error handling
Actions throw on failure; rejected requests surface a code from PROVIDER_ERRORS (EIP-1193 style). Always handle user rejection gracefully.
import { PROVIDER_ERRORS } from '@monkeymask/wallet-standard';try {await signAndSendTransaction({ type: 'send', to, amount: '1.0' });} catch (e) {if ((e as { code?: number }).code === PROVIDER_ERRORS.USER_REJECTED.code) {// user clicked "Reject"}}
| Code | Meaning |
|---|---|
4001 | User rejected the request |
4100 | Unauthorized: not connected |
4200 | Unsupported method |
4900 | Provider is disconnected |
-32602 | Invalid method parameters |
-32603 | Internal error |
Events
Prefer the reactive hooks: accounts/publicKey/connected update automatically when the user switches or disconnects accounts (via the Wallet Standard standard:events change event). Lifecycle callbacks are available on the provider config:
<MonkeyMaskProvider config={{onConnect: (publicKey) => {},onDisconnect: () => {},onError: (message) => {},}} />// Or react to account changes directly:const { publicKey } = useAccounts();useEffect(() => { /* refetch data for the active account */ }, [publicKey]);
Legacy provider
For simple integrations MonkeyMask also injects window.banano. Prefer the Wallet Standard packages for new apps.
if (window.banano?.isMonkeyMask) {const { publicKey } = await window.banano.request({ method: 'connect' });window.banano.on('accountChanged', (pk) => {});}
Backend (Convex)
The dApp template ships an optional Convex backend (monkeymask-website/convex/) that makes SIWB nonces/sessions durable and serves the Explore directory. It's fully optional: NFT ownership is always read crawler-free from the chain (no backend involved), and without Convex the app just uses an in-memory SIWB store. Enable it with npx convex dev and set NEXT_PUBLIC_CONVEX_URL / NEXT_PUBLIC_CONVEX_SITE_URL. See the repository README for details.
Server wallet (@monkeymask/core)
The wallet adapter pattern covers users signing in the browser — but a server can hold its own account too, exactly like loading a Solana Keypair from an env secret. @monkeymask/core is the @solana/web3.js-style layer of the stack: everything the extension can do (send, claim pending, sweep, sign messages / SIWB, mint / transfer / burn NFTs, airdrops) as a plain library that runs in Node.
// Server only — API route, server action, cron, bot…import { Wallet } from '@monkeymask/core';const wallet = await Wallet.fromSeed(process.env.BANANO_SEED!); // hex seed or mnemonicawait wallet.receiveAll(); // claim pendingconst hash = await wallet.send('user.ban', '1'); // BNS names resolve// Sign / verify without the extensionconst sig = await wallet.signMessage('hello');// Full NFT surface: mint, editions, transfer, burn, finish, send-allconst mint = await wallet.mintNFT({ metadataCid: 'Qm…', to: 'ban_1recipient…' });await wallet.transferNFT({ assetRepresentative: mint.assetRepresentative, to: 'ban_1buyer…' });// Same structured envelope dApps send through the providerawait wallet.sendOperation({ type: 'sweep', to: 'ban_1vault…' });
This template ships a ready helper — src/lib/server-wallet.ts exposes getServerWallet(), cached per process and driven by the BANANO_SEED env var. The same metaprotocol safety rules as the extension apply (self-delimiting supply→mint pairs, clean-representative restore after mints/transfers), and setBananoRpcEndpoints() points it at your own node.
Security: a seed in an env var is a hot wallet. Use a dedicated account (never a treasury), keep only working balances on it, never expose the seed with a NEXT_PUBLIC_ prefix, and gate any HTTP endpoint that triggers signing behind a verified SIWB session. Full API in packages/core/README.md.
Chain ID
banano:mainnet