Building a Vault UI
This page covers how to build a custom UI for your Vault using the flap-vault-component-template. If you don't need a bespoke UI, Flap can render a generic interaction page automatically from your vaultUISchema() / vaultDataSchema() (see the Vault & VaultFactory Specification). Use this guide when you want full control over layout, copy, and business logic instead.
This is Step 7 in the Quick start for Vault Developers: build the bespoke UI after your Vault contract is written, tested, and (per your audit plan) submitted for review.
Table of Contents
What the template is (and isn't)
flap-vault-component-template is not a free-form website container. A custom Vault UI is a controlled business component that runs inside Flap's runtime boundary:
Flap SDK for chain reads/writes, wallet, oracle, i18n, notifications, formatting, and tx-error handling.
Flap-owned host context (taxinfo/feeinfo) for token state, tax info, VaultPortal info, deployment binding, fee mode, and market phase β you consume this, you don't reimplement it.
Flap preview shell with real wallet connect, chain switching, and language preference, so you can test against real chain data locally.
Packaged Vault artifacts that contain only your Vault-specific business UI. The shell/header/frame around it is never part of your package.
A minimal manifest declaring your deployment binding (chain + factory/vault/token), i18n locales, and any unavoidable non-oracle endpoints β nothing else.
The production flow, end to end:
Your zip is never loaded by flap.sh directly β Flap builds the actual runtime artifact from it after validation.
The four-file package boundary
Every Vault UI package lives at src/vaults/{folder-name}/ and contains exactly these four files:
No helpers/, no nested components, no local asset files, no README, no extra docs. Any other file or subfolder is a blocking vault:check issue. The only exception is Mini App mode (manifest.mode: "mini-app", for 8888-suffix token-scoped artifacts only), which may also include reviewed top-level audio files (.mp3/.wav/.ogg/.m4a/.aac, β€5 MiB each, β€12 MiB total).
Folder naming is strict lowercase kebab-case: 3β64 characters, letters/numbers separated by single hyphens (e.g. flap-nft-vault). No spaces, underscores, uppercase, leading/trailing hyphens, or nested folders β the folder name is both your source directory and your local preview route (/{folder-name}).
Quick start
The template runs with no local .env file β it ships with a shared preview WalletConnect Project ID, BNB mainnet/testnet RPC fallbacks, and a host-presentation proxy target already wired up. Open the built-in examples:
If you're unsure which example to start from, use example β it carries the default compact visual baseline that vault:check expects new Vault UIs to follow (a row-heavy dashboard with stacked metric cards is blocked by default).
Scaffold your Vault package
Don't hand-create the four files β use vault:scaffold. Recommended for a factory-scoped mainnet launch:
For a single Vault with no factory:
This generates the strict four-file package, a stable artifactId (vaultui_my-vault_<ULID>), and registers my-vault in src/vaults/index.ts so /my-vault renders locally. Replace every placeholder address with a real deployed address before running the command β vault:check blocks zero addresses and reserved template placeholders.
If the four files already exist (e.g. an AI agent generated them from a manifest first), just register the local preview route:
manifest.json β declaring your binding
manifest.json is intentionally small β it's a match/review declaration, not a place to configure runtime behavior. Minimal factory-scoped example:
There are exactly three supported match modes, and you never bind by a "type" field:
Factory-scoped
chainId + non-zero factoryAddress
Vault-scoped (no factory)
chainId + exactly one vaultAddresses entry (+ optional tokenAddresses)
Token-scoped (no factory)
chainId + one or more tokenAddresses
Every binding-scoped tokenAddresses entry β including proof tokens on factory bindings β must be a real deployed ERC20 token address ending in 7777 or 8888. At least one binding in the manifest must include one of these, since it's what vault:check/E2E test against. In factory mode, tokenAddresses is package proof, not the production CA restriction β that's a Workbench/registry caRestrictionMode decision (none / reserved / verified), never a manifest field.
Do not declare in manifest.json: id, owner, version, actions, oracles, media, fallback, contracts, chainIds, or global tokenAddresses. Actions live in Component.tsx via SDK calls; oracle usage is auto-detected by vault:check, not declared.
Optional per-binding field: externalContracts β for a truly fixed contract address that isn't your runtime token/vault/factory:
See docs/manifest.md in the template repo for the full schema, including Mini App and fullscreen-layout variants.
The Flap SDK β what your component may call
Import everything from the public aliases β never introduce another SDK-like wrapper:
Common SDK methods:
Contract calls should target context.vaultAddress, context.tokenAddress, context.factoryAddress, or a binding-scoped tokenAddresses/vaultAddresses/externalContracts entry β never a hardcoded address, and never an unrelated router/bridge/aggregator contract. vault:check blocks calls to undeclared fixed addresses.
For methods with multiple return values, type the read as a tuple array β not an object β even if the ABI names the outputs:
Use the SDK's standard ERC20 ABI for normal token flows instead of copying it into VaultABI.ts:
Only add fragments to VaultABI.ts for your Vault's own non-standard methods.
Mandatory: render the current risk status
Every default Vault UI must visibly render the current contract risk status. This is a hard, checker-enforced requirement (the only exception is Mini App mode on 8888-suffix tokens).
Rules:
The risk status must appear within the first three visible Vault-specific business rows/blocks, and before any hero, banner, preview, chart, or large visual block. The safest placement is the first
CardHeaderbadge row.If
riskLevelis unavailable, render a prominent warning that risk-status integration is required β don't silently omit it.vault:checktreats a missing risk-status integration as a blocking issue.Never hardcode or unconditionally render a "Low risk" badge or reassuring copy. A low-risk label may only appear when it's selected from the host-derived
riskLevel === 1branch.
Market-phase and wrong-network gating
Every action-bearing Vault must decide whether each button is available in internal-market, dex-listed, both, or read-only stage, and gate it at runtime:
Show unavailable actions with a clear disabled state β don't hide them silently. Wrong-network handling is a separate, orthogonal concern:
Keep write buttons visible but disabled when sdk.wallet.isWrongNetwork is true, and prompt a chain switch before sending any transaction.
A minimal Component.tsx walkthrough
The built-in example package (src/vaults/example/Component.tsx) is the canonical reference: a reward vault with deposit/approve/claim actions, live oracle data, and full risk/phase gating. Its shape, condensed:
Key habits this example demonstrates:
Reads run on mount and on a polling interval; writes call
sdk.refetch()(or reload data directly) aftersdk.waitForTx(...)resolves.needsApprovalis computed by comparing liveallowanceto the parsed input amount, and the deposit button transparently routes throughapprove()first when needed.Every user-facing string comes from
i18n.t(...)β never hardcoded text β sincei18n.jsonis validated against every locale declared inmanifest.i18n.Errors from failed reads/writes are normalized through
handleTxError(...)rather than ad hoc string matching.
For layout consistency, read docs/ui-pattern-snippets.md in the template before building Component.tsx β it has sanitized card/action-panel/read-write-flow patterns. For icons, search lucide-react before hand-writing SVG.
Safety rules you must not violate
These are blocked by default and will fail vault:check:
window.ethereum.request,eval/new Function, raw<iframe>orsrcDocdynamic imports, CommonJS
require(...), runtime remote importsundeclared external URLs/endpoints/frames, or any endpoint that isn't a static absolute HTTPS string covered by
manifest.endpointsbrowser storage, navigation, workers,
postMessage, permission APIs, clipboard accessdirect browser network/media APIs (
XMLHttpRequest,WebSocket,EventSource,new Image())hidden transaction targets or contract calls to addresses outside your declared bindings
remote images (immutable Vault images must use
IpfsImage/IpfsBackgroundfrom@/src/uiwith a static CID)relative imports other than
./VaultABIbinding by a "type" field instead of
chainId+ factory/vault/token address
Prefer SDK/on-chain reads over external endpoints and frames whenever the same result is achievable that way. If a non-oracle endpoint really is unavoidable, predeclare it as a static HTTPS string in manifest.endpoints β declaring it doesn't guarantee approval, it just makes it reviewable. See docs/manifest.md for the full rule set, including the reviewed ReviewedFrame allowance for TradingView/DexScreener/GeckoTerminal charts.
Check, preview, and package
Preview your route with real runtime params before packaging:
vault:check also enforces that your local checkout is exactly up to date with origin/main and that your package.json version matches the latest published @flapsdk/vault-runtime β a stale template checkout fails with template-freshness/* errors before it even reaches your Vault-specific issues. vault:e2e requires Playwright's Chromium (yarn playwright install chromium on first run).
Handoff to Flap Artifact Workbench
The only deliverable Flap accepts is the zip produced by yarn vault:package <folder-name> β not a hand-made zip, not a prompt-only result, and not a package without a passing E2E proof. The zip embeds a package marker, npm runtime provenance, source/schema/E2E file hashes, and the E2E report; Workbench rejects anything missing that proof.
Once you have a verified zip, follow Step 6 (Audit and low-risk badge) and Step 8 (align on your launch plan) from the Quick Start guide, then hand the zip to the Flap team.
Last updated