For the complete documentation index, see llms.txt. This page is also available as Markdown.

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:

Mode
Shape

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 CardHeader badge row.

  • If riskLevel is unavailable, render a prominent warning that risk-status integration is required β€” don't silently omit it. vault:check treats 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 === 1 branch.

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) after sdk.waitForTx(...) resolves.

  • needsApproval is computed by comparing live allowance to the parsed input amount, and the deposit button transparently routes through approve() first when needed.

  • Every user-facing string comes from i18n.t(...) β€” never hardcoded text β€” since i18n.json is validated against every locale declared in manifest.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> or srcDoc

  • dynamic imports, CommonJS require(...), runtime remote imports

  • undeclared external URLs/endpoints/frames, or any endpoint that isn't a static absolute HTTPS string covered by manifest.endpoints

  • browser storage, navigation, workers, postMessage, permission APIs, clipboard access

  • direct 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/IpfsBackground from @/src/ui with a static CID)

  • relative imports other than ./VaultABI

  • binding 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