Skip to main content

Permissions

Every app declares what it needs access to in its manifest. Permissions are checked at runtime — a script that calls a wallet read without read:wallet declared in the manifest fails immediately, with an error you'll see in the script output. This makes the surface area of any app explicit: glance at the permissions list and you know exactly what it can touch.

When you ask Bankr to build an app, the agent picks the minimum set of permissions the scripts actually use. If you want fewer, ask — Bankr will rewrite the scripts to drop the dependency, or tell you the feature can't work without it.

Permission catalog​

Read permissions​

These let the app see things. None of them can change state.

PermissionWhat it grants
read:walletYour wallet identity — the EVM address, Solana address, and a stable account ID. Used by almost every app to know who's running it.
read:portfolioA full multi-chain token portfolio — balances on every supported chain, optional realized + unrealized PnL, optional NFT listings.
read:positionsYour open positions on supported venues (perps, spot trading, prediction markets).
read:tokensToken metadata, prices, and search across the chains you use.
read:chainDirect on-chain reads — call any contract's read functions, multicall, fetch logs, look up balances by address.
read:filesRead your file storage. The app can list folders, fetch file contents, and search files by name or content. Only on runs the owner starts — refused when a visitor starts the run.
read:appdataRead the app's own key-value storage (its private state and any cached snapshots).
read:secretsResolve manifest-declared secret values at runtime. Plaintext stays server-side — secrets never reach the rendered HTML.

Write permissions​

These let the app change state. They're more sensitive — Bankr doesn't grant them unless the app actually needs them.

PermissionWhat it grants
write:filesCreate and update files in your file storage. Used by apps that save reports, snapshots, or generated artifacts.
write:appdataWrite to the app's key-value storage. Required for any app that caches data, stores user preferences, or persists state between runs.
prepare:transactionBuild a transaction blob (target address, calldata, value) for the user to confirm in chat. The app can't broadcast on its own — confirmation always goes through the chat flow.
execute:swapUse Bankr's swap routing to assemble swap transactions. Still requires user confirmation in chat to broadcast.
execute:transferBuild a token transfer. Still requires user confirmation in chat to broadcast.
execute:signBuild an arbitrary signature request. Still requires user confirmation in chat to sign.

Network and AI permissions​

PermissionWhat it grants
fetch:httpMake outbound HTTP requests from the server-side runtime. Locked to public hostnames — private IPs are blocked.
pay:x402Lets the iframe call bankr.x402.fetch(url, options) to pay an x402 endpoint from the visitor's wallet (not the owner's). Each call pops a confirmation modal showing URL + max USD before any payment. Requires a companion x402 manifest block declaring allowedHosts and a per-call USD cap — without it the runtime rejects every fetch. Redirects aren't followed, so the URL must be the endpoint's final URL on an allowed host. Per-(visitor, app) rate limit: 10 calls/minute.
fetch:x402Reserved for server-side, owner-paid x402 fetches (cron-driven premium feeds where the owner's wallet covers the bill once and visitors read the cached snapshot for free). The permission is declared today; the runtime client is not yet wired — scheduled scripts that need paid data should stick to fetch:http plus an upstream API key in secrets for now.
invoke:agentRun a curated, read-only Bankr agent turn from inside a script (bankr.askAgent) — useful for LLM-powered curation, scoring, or summaries. The agent runs as the app owner, and only on runs the owner starts — refused when a visitor starts the run. Two tiers: Free (default model, no credit deduction, capped at 2 runs/day per owner — 5/day with Bankr Club, 0/day once the owner has had no signed-in Bankr activity for 14 days — with a 5/min burst) and Max (set a per-app agent model — runs that model and deducts the owner's LLM credits, exempt from the free daily cap). See Agent model tiers.

Defaults Bankr picks​

When the agent builds an app, it picks the smallest set that satisfies your request:

  • Read-only dashboard of your data → read:wallet, read:portfolio and/or read:positions, read:appdata, write:appdata.
  • Public snapshot dashboard with scheduled-refresh content → adds fetch:http, often invoke:agent.
  • Trade builder / order helper → adds one of prepare:transaction, execute:swap, execute:transfer.
  • Apps that store generated artifacts (charts, reports) → adds read:files, write:files.

You can audit the list anytime — every app's manifest is in your filesystem under /apps/{slug}/manifest.json.

Wallet identity​

Apps run scripts under a single wallet identity. Which wallet depends on what triggered the script:

TriggerWhose wallet drives the script
Frontend — a button or bankr.scripts.run call from the app's HTMLSet by the manifest's frontendIdentity field — "owner" (default) or "viewer" (owner-only for now, see below)
Scheduled run — fires automatically on the app's scheduleAlways the owner (the wallet that installed the app)
Owner manually clicks Run in the Scripts drawerThe owner

A run that someone other than the owner starts keeps the app's other permissions but never gets invoke:agent or read:files, whatever the manifest declares, so a visitor's input can't reach the owner's agent or files. Put agent calls and file reads in a scheduled script and have the page read its snapshot.

The default frontendIdentity: "owner" is the right choice for shared dashboards where you want every visitor to see the same data — your trades, your portfolio, your curated picks. Bankr leans on this default unless you specifically ask for visitor-scoped behavior.

frontendIdentity: "viewer" is meant for per-visitor tools — a calculator, a "score MY positions" widget, a personal trade plan — where each visitor's wallet drives the script. It is closed to visitors for now: a viewer-mode app runs only for its owner and can't be made public or unlisted, and a shared app has to be made private before it can switch to viewer mode.

Forking someone else's app is also turned off for now. A visitor who wants the same dashboard pointed at their own data asks Bankr to build them their own app.

Anonymous viewers​

Anyone visiting a public or unlisted app without signing in:

  • Can read any data the app explicitly publishes via publicDataKeys (snapshots populated by scheduled runs, curated content).
  • Can run only the scripts the owner lists in publicScripts, within that script's publicScriptLimits. A public script runs with no caller wallet and can't use the wallet, files, transactions, the agent or the app's private storage. Any other button that runs server-side code shows a sign-in prompt.
  • Cannot read or write the app's private storage.

This is why public dashboards typically have a scheduled script that pre-computes the data into a public snapshot — anonymous viewers see the rendered snapshot instantly without authenticating.

Before publishing public or unlisted​

The most common breakage when an app moves from private to public is reading an appKV key the manifest never declared. Anonymous viewers get a 403 with the exact error Public data key "<k>" is not exposed by this app, and the iframe shows an empty panel where data should be.

Walk this checklist before flipping make this app public or sharing the unlisted link:

  1. List every key the iframe reads. Open index.html and find every bankr.appKV.get(...) and bankr.appKV.list(...) call — including the ones nested inside Promise.all, fallback branches, and event handlers that run before sign-in.
  2. Each key must be in manifest.publicDataKeys. If the iframe reads portfolio_snapshot, meta.balancesUpdatedAt, and last_curated, all three names must be in the array. Meta and timestamp keys are separate keys, not sub-paths of the data key — the runtime treats meta.balancesUpdatedAt as its own entry.
  3. Public-facing keys must be file-backed. Keys that start with record: route to the per-viewer queryable record store and can never be exposed publicly. If you need a public snapshot, the writer must use a plain key (no prefix) so it lands under /apps/{slug}/data/{key}.json.
  4. The writer must be the owner. Schedule the script that populates the snapshot, or run it manually as the owner — viewers can't write file-backed appKV. Cron-triggered runs always execute as the owner, which is the right pattern for refreshable public data.
  5. Gate authenticated-only reads. If a key is intentionally private (per-viewer state, owner-only secrets), don't read it unconditionally. Wrap the read in if (bankr.ctx.isAuthenticated) { ... } and render a signed-out fallback so anonymous viewers see an empty state instead of an error.
  6. Wrap interactive buttons in bankr.auth.requireSignIn(). Calls like bankr.appKV.set, bankr.askChat, bankr.confirmTransaction, and bankr.invokeScript for any script not in publicScripts require sign-in. Render those buttons disabled (or hidden) when bankr.auth.isAuthenticated is false, and call bankr.auth.requireSignIn() from the click handler so the visitor sees a sign-in modal instead of a thrown error.
  7. Re-audit on every edit. Whenever you (or Bankr) add a new appKV.get call or rename an existing one, recheck step 1 against the manifest. The cleanest way to ask Bankr is "audit publicDataKeys against every appKV.get in index.html and add any missing keys".

A working public-dashboard pattern looks like this:

// manifest.json
{
"permissions": ["read:portfolio", "read:appdata", "write:appdata"],
"publicDataKeys": [
"portfolio_snapshot",
"portfolio_history",
"meta.refreshedAt"
],
"schedule": [
{ "script": "refreshSnapshot", "cron": "*/15 * * * *", "enabled": true }
]
}
// scripts/refreshSnapshot.ts — runs as owner on the schedule
const portfolio = await bankr.wallet.balances({ include: { pnl: true } });
await appKV.set("portfolio_snapshot", { tokens: portfolio.tokens });
await appKV.set("portfolio_history", { points: portfolio.history });
await appKV.set("meta.refreshedAt", Date.now());
return { ok: true };
<!-- index.html — runs in the public iframe -->
<script>
bankr.on("ready", async () => {
const [snapshot, history, meta] = await Promise.all([
bankr.appKV.get("portfolio_snapshot"),
bankr.appKV.get("portfolio_history"),
bankr.appKV.get("meta.refreshedAt"),
]);
render(snapshot, history, meta);

// Owner-only "edit notes" — gated, never thrown for visitors.
if (bankr.ctx.isAuthenticated) {
const notes = await bankr.appKV.get("private_notes");
renderNotes(notes);
}
});
</script>

Every key the iframe reads (portfolio_snapshot, portfolio_history, meta.refreshedAt) is in publicDataKeys. The owner-only private_notes is gated on bankr.ctx.isAuthenticated, so anonymous viewers never trigger the read. The cron writer runs as the owner, and the keys are plain (file-backed), not record:-prefixed.

Asking Bankr to change permissions​

Like everything else about an app, permissions are editable through chat:

this app doesn't need to read my files anymore — drop that permission
add fetch:http so you can pull market data from the upstream API

Bankr updates the manifest, redeploys, and confirms what changed. The new permission set takes effect on the next script run — no reinstall, no fork.

Where to next​