Skip to main content

SDK Reference

An app is two things glued together: an index.html rendered inside a sandboxed iframe, and zero-or-more server-side scripts that the iframe can call. Both surfaces ship with a bankr global that exposes all the runtime capabilities. The two bankr objects share a brand but have different APIs — pick the right one for the surface you're writing.

This page is the reference. You don't have to memorize it — Bankr does the writing — but knowing what's available helps you ask for things in the chat.

The two surfaces

Frontend (the app's index.html)

Runs inside a sandboxed iframe at null origin. This is where the UI lives. The host injects the bankr global before any of your code runs.

The frontend can:

  • Read shared snapshot data (bankr.appKV.get).
  • Trigger backend scripts (bankr.scripts.run).
  • Hand off to chat (bankr.prefillChat, bankr.askChat).
  • Ask the user to confirm a transaction (bankr.confirmTransaction).
  • Detect whether the visitor is signed in (bankr.auth.isAuthenticated).

The frontend cannot make raw fetch() calls to external URLs — the sandbox is opaque-origin and most APIs CORS-block it. Always route external HTTP through a backend script.

Backend (server-side scripts)

Each script lives at /apps/{slug}/scripts/{name}.ts in your filesystem. They're written as top-level statements ending in return — not modules. The runtime is a hardened sandbox: no DOM, no require, no import, no access to other apps' state.

The backend can:

  • Read your wallet identity, balances, and on-chain data.
  • Read and write your file storage and the app's persistent storage.
  • Call public HTTP APIs (paid x402 endpoints from the server side aren't yet wired — visitor-paid x402 lives in the iframe SDK).
  • Build (but not broadcast) transactions for the user to confirm.
  • Invoke the Bankr agent itself for LLM-driven curation.

Backend scripts run when:

  • The frontend calls bankr.scripts.run('name').
  • A scheduled run fires automatically on the app's schedule.
  • The app owner clicks Run in the Scripts drawer.

Frontend SDK

type BankrAppSdk = {
ctx: {
slug: string;
walletAddress?: string;
theme: "light" | "dark";
isPublic: boolean;
isCompact: boolean;
isAuthenticated: boolean;
};

// Lifecycle — wrap first-paint logic so wallet info is populated.
on(event: "ready" | "refresh" | "theme", cb: (payload: unknown) => void): () => void;

// Wallet info — property only, NOT a function.
wallet: {
address?: string;
evmAddress?: string;
};

// Auth state.
auth: {
isAuthenticated: boolean;
requireSignIn(): Promise<void>;
};

// Trigger a backend script.
invokeScript(name: string, args?: Record<string, unknown>): Promise<unknown>;
// Aliases (all identical):
runScript(name: string, args?: Record<string, unknown>): Promise<unknown>;
run(name: string, args?: Record<string, unknown>): Promise<unknown>;
scripts: {
run(name: string, args?: Record<string, unknown>): Promise<unknown>;
invoke(name: string, args?: Record<string, unknown>): Promise<unknown>;
};

// Persistent key-value store, server-mediated.
appKV: {
get(key: string): Promise<unknown | null>;
set(key: string, value: unknown): Promise<void>;
list(prefix?: string): Promise<string[]>;
delete(key: string): Promise<void>;
};

// Hand off to chat.
prefillChat(message: string): Promise<void>;
askChat(message: string): Promise<void>;
chat: {
prefill(message: string): Promise<void>;
send(message: string): Promise<void>;
ask(message: string): Promise<void>;
};

// Transaction confirmation. The blob comes from a backend script.
confirmTransaction(blob: unknown): Promise<void>;

// Visitor-paid x402 fetch — settles USDC on Base from the SIGNED-IN
// viewer's wallet (not the owner's). Pops a confirmation modal first.
// Requires `pay:x402` permission + `x402.allowedHosts` manifest block.
x402: {
fetch(
url: string,
options?: {
method?: "GET" | "POST" | "PUT" | "DELETE";
body?: string;
headers?: Record<string, string>;
maxPaymentUsd?: number;
},
): Promise<{
status: number;
ok: boolean;
body: unknown;
payment: { amountUsd: number; network: string; payTo: string };
}>;
};

// Skill management.
installSkill(slug: string): Promise<void>;

// UI helpers.
openExternal(url: string): Promise<void>;
copy(text: string): Promise<void>;
navigate(page: string): Promise<void>;

// Secrets — status only. The plaintext NEVER reaches the iframe.
secrets: {
status(): Promise<Array<{
key: string;
scope: "author" | "viewer";
required: boolean;
set: boolean;
preview?: string;
}>>;
requestSetup(key: string): Promise<void>;
};

// Pass context back into the chat thread (e.g. "user is viewing X").
setContext(body: string, label?: string): Promise<void>;
};

Wallet info is populated after the host posts context — wrap your initial render in bankr.on('ready', …) so the address is defined when you read it:

bankr.on('ready', async () => {
const wallet = bankr.ctx.walletAddress;
if (!wallet) {
renderSignedOutState();
return;
}
const snapshot = await bankr.appKV.get('snapshot');
render(snapshot);
});

Gate per-user actions on bankr.auth.isAuthenticated:

async function loadMyData() {
if (!bankr.auth.isAuthenticated) {
showSignInPrompt(() => bankr.auth.requireSignIn());
return;
}
const data = await bankr.scripts.run('fetchMyData');
render(data);
}

Visitor-paid x402 buttons

When the app needs the visitor to pay for a single upstream call from THEIR wallet (paid AI image generation, premium data lookup, gated x402 endpoint), use bankr.x402.fetch. The host pops a confirmation modal showing the URL and max USDC; on Approve it settles via the visitor's wallet on Base and returns the upstream response.

async function generateImage(prompt) {
if (!bankr.auth.isAuthenticated) {
return bankr.auth.requireSignIn();
}
try {
const result = await bankr.x402.fetch(
'https://x402.bankr.bot/0xowner/image-gen',
{
method: 'POST',
body: JSON.stringify({ prompt }),
headers: { 'content-type': 'application/json' },
maxPaymentUsd: 0.10, // per-call cap; defaults to manifest cap
},
);
// result.body is the parsed upstream response
// result.payment is { amountUsd, network, payTo }
showImage(result.body.url);
showReceipt(`Paid $${result.payment.amountUsd} USDC`);
} catch (err) {
if (err.message.includes('rejected')) {
// user clicked Reject — silent
return;
}
showError(err.message);
}
}

The manifest must declare pay:x402 and an x402 block with the hostname in allowedHosts:

{
"permissions": ["pay:x402", ...],
"x402": {
"maxPaymentUsdPerCall": 0.50,
"allowedHosts": ["x402.bankr.bot", "api.example.com"]
}
}

Without those, the call is rejected before any modal pops. Distinct from fetch:x402 — that permission is reserved for the server-side, owner-paid surface (planned; not yet wired); for now only the iframe path settles real x402 payments. See Permissions.

Per-(visitor, app) rate limits apply: 10 calls/min, $10/day cap.

Backend SDK

Scripts have these globals available without any import: bankr, appKV, http, secrets, args, ctx, log. Standard JS built-ins (Buffer, URL, URLSearchParams, crypto, setTimeout, console) work normally.

// Wallet identity — async!
bankr.wallet.me(): Promise<{
address: string; // alias for evmAddress
evmAddress: string;
walletId: string;
}>;

// Multi-chain portfolio — tokens, balances, optional PnL/NFTs.
bankr.wallet.balances(args?: {
chains?: string; // CSV like "base,polygon"; default = all enabled
showLowValueTokens?: boolean;
include?: { pnl?: boolean; nfts?: boolean };
}): Promise<WalletPortfolioResult>;

// File storage. Permission: read:files / write:files.
bankr.files.read({ path }: { path: string }): Promise<{ content: string; ... }>;
bankr.files.write({ path, content, mimeType? }): Promise<{ fileId; ... }>;
bankr.files.list({ folder?, limit? }): Promise<UserFileSummary[]>;
bankr.files.search({ query, folder?, mimeType? }): Promise<UserFileSummary[]>;

// On-chain reads. Permission: read:chain.
bankr.chain.readContract({ chain, address, abi, functionName, args }): Promise<unknown>;
bankr.chain.multicall({ chain, contracts: [...], allowFailure? }): Promise<unknown[]>;
bankr.chain.getBalance({ chain, address }): Promise<bigint>;
bankr.chain.getLogs({ chain, address?, topics?, fromBlock, toBlock }): Promise<Log[]>;
bankr.chain.encodeFunctionData({ abi, functionName, args }): Promise<`0x${string}`>;

// Build a transaction blob. Permission: prepare:transaction.
bankr.tx.prepare({ chain, to, data?, value?, label? }): Promise<TxButton>;

// Curated agent invocation. Permission: invoke:agent.
bankr.askAgent(prompt: string): Promise<string>;

// HTTP fetch from the server-side runtime. Permission: fetch:http.
http.fetch(url: string, init?: RequestInit): Promise<unknown>;
// Returns the parsed body directly: object/array for application/json,
// raw text otherwise. There is no .body, .json(), or .headers — use the
// alias `bankr.fetchHttp` if you need a Response-shaped wrapper:
bankr.fetchHttp(url, init?): Promise<{ status: 200; ok: true; body: unknown }>;

// Secrets resolution. Permission: read:secrets.
secrets.get(key: string): Promise<string>;
secrets.status(): Promise<Array<{ key; required; set; allowedHosts }>>;

// Persistent key-value store, scoped to this app.
appKV.get(key: string): Promise<unknown | null>;
appKV.set(key: string, value: unknown): Promise<void>;
appKV.delete(key: string): Promise<boolean>;
appKV.list(prefix?: string): Promise<Array<{ key; value }>>;

// Args from the iframe (the second arg to bankr.scripts.run).
const args: Record<string, unknown>;

// Context — read-only.
const ctx: {
caller: { walletId: string; walletAddress: string };
author: { walletId: string; walletAddress: string };
appId: string;
permissions: string[];
};

// Diagnostic logger — output appears alongside the script return value.
log("anything");
log("with values:", { foo: 1, bar: 2 });

Scripts are top-level — no function wrapping the body, no export, no import. End with return:

const me = await bankr.wallet.me();
const portfolio = await bankr.wallet.balances({ include: { pnl: true } });

await appKV.set('snapshot', {
address: me.evmAddress,
totalUsd: portfolio.totalUsd,
topHoldings: portfolio.tokens.slice(0, 10),
refreshedAt: new Date().toISOString(),
});

return { ok: true, holdings: portfolio.tokens.length };

The return value is what the iframe receives back from bankr.scripts.run. Keep it small — anything large should land in appKV with the iframe reading it via bankr.appKV.get.

Manifest

Every app has a manifest.json at /apps/{slug}/manifest.json. You don't write it by hand — Bankr generates and updates it as you ask for changes — but it's editable and the runtime reads it on every script run, so changes pick up immediately.

{
"slug": "polymarket-alpha",
"title": "Polymarket Alpha — curated picks + your positions",
"description": "Live alpha picks scored by Bankr, plus a panel for your open positions.",
"tags": ["polymarket", "trading", "alpha"],

"permissions": [
"read:wallet",
"read:positions",
"read:appdata",
"write:appdata",
"fetch:http",
"invoke:agent"
],

"scripts": ["refreshAlpha", "fetchMyPositions"],

"schedule": [
{ "script": "refreshAlpha", "cron": "*/15 * * * *", "enabled": true }
],

"secrets": [],

"publicDataKeys": ["alpha_snapshot", "trending_snapshot"],

"frontendIdentity": "viewer",

"pricing": { "type": "free" }
}
FieldPurpose
slugURL-safe identifier, unique within your wallet.
title, description, tagsDisplay metadata for the app directory.
permissionsRuntime permissions — see Permissions.
scriptsNames of the script files (without the .ts extension).
scheduleScheduled-run entries. Each references a script by name plus a cron expression — the standard 5-field schedule syntax (minute hour day-of-month month day-of-week). For example, "*/15 * * * *" means "every 15 minutes," "0 14 * * *" means "every day at 14:00 UTC." Tools like crontab.guru translate plain English into the syntax.
secretsManifest-declared secrets. The actual values are stored separately and never appear in the manifest itself.
publicDataKeysappKV keys that anonymous viewers can read. Use for public snapshots populated by scheduled runs.
frontendIdentity"owner" (default) or "viewer" — see Wallet identity.
x402Required when permissions includes pay:x402. Shape: { maxPaymentUsdPerCall: number, allowedHosts: string[] }. The hostname allowlist is mandatory and non-empty — without it the runtime rejects every bankr.x402.fetch.
pricingFree, one-time, per-use, or subscription. Most apps are free.

File storage layout

Apps live in your regular file storage under /apps/{slug}/. You can browse, edit, and even copy files between apps using the file explorer or by asking Bankr.

/apps/{slug}/
├── manifest.json ← the app config
├── index.html ← the rendered HTML
├── scripts/
│ ├── refreshAlpha.ts ← server-side script
│ └── fetchMyPositions.ts
├── data/ ← appKV file-backed values
│ ├── alpha_snapshot.json
│ └── trending_snapshot.json
└── designs/ ← optional reference images, mockups
└── v1.png

Editing a script file in the file explorer is the same as asking Bankr to update it — the next script run picks up your edit. Same for index.html. The manifest.json is also editable, but if you're going to change permissions or schedules, asking the agent to do it is usually faster (and avoids syntax mistakes).

The data/ folder is where appKV.set('snapshot', …) lands when the value is small JSON. Each key becomes a {key}.json file that's browsable and exportable like any other file. Larger values automatically use a queryable backend instead — write keys with a mongo: prefix to opt into that explicitly:

// File-backed: lands at /apps/{slug}/data/snapshot.json
await appKV.set('snapshot', { ... });

// Queryable backend, scoped to caller wallet:
await appKV.set('mongo:user_prefs', { ... });

The two paths feel the same from the app's perspective — same get, set, list, delete methods. The difference matters for sharing: only file-backed keys can be exposed to anonymous viewers via publicDataKeys.

Where to next

  • Apps Overview — how to build and refine.
  • Permissions — the full catalog plus the wallet identity model for shared dashboards.