← Signer Sidekick

How these apps were built

Why these are composable, standalone browser apps, and the skills and tiers behind them.

Signer Sidekick is a set of standalone, single-file browser apps. Each page is plain HTML, CSS, and JavaScript that talks directly to the user's wallet and to public Stacks APIs; there is no backend of our own. That is a deliberate choice, guided by the two skills included verbatim below.

The first, static-first architecture, picks the least operationally expensive option that meets the real requirements: pre-rendered static assets by default, escalating to a server only for a named, concrete need. The second, Stacks dApp architecture, adds an axis generic web tools do not have: a security tier. A dApp can move real value, and much of the user-protecting logic (post-conditions, pinned contract identity, keys that stay in the wallet) lives in the frontend, not a backend. So the safeguards that never relax are applied from the first line of code, even on a throwaway testnet toy, so a promoted demo is not born insecure.

That is why these apps separate testnet from mainnet so deliberately. Testnet is a learning tier: faucet tokens, no real value. Mainnet moves real assets and forces the full set of safeguards. The network switch on every page is that boundary, made explicit, which is how the tier is judged here.

Static-first architecture

skill: static-first-architecture

Revised after building Signer Sidekick — adds "dependencies are part of the artifact" (vendor + pin, CDN-primary with local fallback), URL-as-state, and "verify by executing the artifact, not resolving the graph."

---
name: static-first-architecture
description: Choose the least operationally expensive architecture for prototypes, dashboards, data explorers, reports, and read-heavy internal tools. Use when selecting an architecture or hosting model, building a prototype/dashboard, replacing an API or container, deciding whether a backend is needed at all, or vendoring and pinning a static app's runtime dependencies. Prefer pre-rendered static assets; escalate to browser-side compute, then WASM-backed libraries, then serverless, then an always-on server — only for named, concrete requirements. Ship a self-contained artifact: vendor and pin runtime dependencies (with a local fallback if a CDN is used), and verify by executing the built artifact, not by resolving the dependency graph.
---

# Static-First Architecture

Choose the least operationally expensive architecture that satisfies the real requirements. A static site on object storage/CDN/Pages is the default. Everything else is an escalation that must be justified by a named requirement. WASM is not an architecture: it is a compilation target used by some libraries (in-browser SQLite/DuckDB, ffmpeg, etc.). It appears in a project as a dependency pulled in at rung 3 of the ladder, never as a goal in itself. Do not instruct agents or teammates to "use WASM"; state the actual requirement (no backend, static deploy, local querying) and let wasm-backed libraries appear only where a dependency demands them.

## Apply the architecture ladder

Evaluate options in this order, and stop at the first rung that satisfies the requirements:

1. **Pre-render.** Build UI and bounded results into static HTML, JavaScript, JSON, CSV, Parquet, or other immutable assets at build time. This includes SPAs: a React/Preact/Vue app built with Vite is static output and belongs on this rung.
2. **Browser-side JavaScript.** Perform filtering, sorting, aggregation, visualization, and local persistence (IndexedDB) in plain JS over data shipped as static assets.
3. **WASM-backed local querying.** Lazily load a wasm-based library (sql.js, wa-sqlite, DuckDB-WASM) for arbitrary local queries or compute that static snapshots and plain JS cannot answer economically. Prefer a static SQLite/DuckDB-style dataset over a hosted database for read-only prototypes. Use pre-compiled library binaries; never compile application code to WASM for this purpose.
4. **Narrow serverless.** Add a small edge/serverless endpoint only for the irreducible secret, write, authorization, or freshness boundary.
5. **Always-on server.** Introduce an API/database service only after naming the requirement that makes every previous rung insufficient.

Do not ask the user to choose a hosting provider before inspecting the repository and requirements. Make the static-first choice by default and explain any escalation. Exception: if the sensitivity of the data is unclear and rungs 1–3 would ship the dataset to every visitor, ask before deploying.

## Dependencies are part of the artifact

A static site is only as robust as what it loads at runtime. A single-file page that imports its libraries from a CDN is *deploy-static* but not *self-contained* and not offline — it is hostage to that CDN's live resolution, and a broken upstream publish or an outage takes it down with nothing wrong in your code or your host.

- **Vendor and pin what you ship.** Treat runtime libraries like any other built asset: vendor them into the deployment and pin **exact versions, including transitive ones, not just top-level.** Floating version ranges resolve to whatever the registry serves at load time, which is exactly how a broken upstream release becomes your outage. (A real case: a CDN floated a transitive dependency to a just-published, broken version and every page that imported the top-level library failed at load.)
- **CDN-primary + local fallback, if you use a CDN at all.** Prefer a pinned local copy. If you load from a CDN for convenience or freshness, fall back to the vendored copy on any failure. Note that chasing "latest" is itself the risk vector — a bad publish is *reachable*, not merely an outage — so a pinned known-good version is safer than latest, and you re-vendor deliberately when you choose to upgrade.
- **Content-address or version the bundles.** Same rule as packaged datasets: version deployed assets so a deploy can't mix stale and fresh copies, and so a shared stylesheet or script isn't served stale from cache (a `?v=` query bump on a shared asset is enough).
- **WASM libraries are just vendored dependencies here.** The same pin-and-vendor rule applies; nothing about the format is special.

## State without a backend

For a backendless app, the URL is the session substitute. Put user-selectable state — filters, the selected dataset, a network or read-endpoint override — in the URL query so a refresh preserves it and links are shareable. Never put secrets or personal data in the URL.

## Separate the data tiers

For dashboards with both hot and arbitrary reads, use three tiers:

- **Snapshot hot paths:** precompute initial pages and common reports as versioned JSON. Do not boot WASM for these views.
- **Static bounded datasets:** ship complete, reasonably sized tables and implement sort/filter/page operations locally in JS.
- **WASM long tail:** query a packaged database only for arbitrary drill-downs that cannot be enumerated safely at build time.

Avoid using WASM for answers that can be precomputed cheaply. Lazy-load it on the first genuine tail query, not at page load.

## Package WASM data for remote reads

When querying a static database over HTTP:

- Prune unused tables and columns from the deployed copy.
- Add indexes for every interactive lookup and run planner statistics generation after indexing.
- Bound high-fan-out graph expansions and result sets.
- Batch lookups instead of issuing one query per rendered item.
- Materialize compact serving projections when wide or scattered source tables cause excessive page reads.
- Match remote request chunks to the database page size and verify host range-request behavior.
- Content-address database files and snapshot generations to prevent mixed or stale deployments.
- Test with production-scale data; small fixtures validate correctness, not remote-I/O performance.

Measure actual response bodies and range `GET` requests. Do not count a bodyless `HEAD` response's advertised full-file `Content-Length` as transferred bytes.

## Escalate only for concrete constraints

Use server-side infrastructure when one or more of these requirements are real:

- Secrets or privileged credentials must be used at request time.
- Authoritative writes, transactions, server-enforced authorization, or multi-user coordination are required.
- Data must not be downloadable by every authorized static-site visitor.
- Freshness cannot be met by periodic artifact rebuilds or narrow serverless refreshes.
- The working set is too large for target devices or economical ranged reads.
- Compute is long-running, memory-heavy, unsupported in browsers, or must be trusted.
- True realtime push or collaborative state is required.

Keep the exception narrow. A write endpoint does not imply that reads, filtering, rendering, or analytics also belong on the server.

## Protect security boundaries

Treat every shipped static asset — including any WASM binary and packaged database — as downloadable by every visitor who can load the page. WASM provides no confidentiality, obfuscation, or sandboxing benefit over JavaScript; it runs in the same browser sandbox with the same origin and network access. Never embed secrets, API keys, or credentials in any client-side bundle regardless of format.

Use platform-level access control (private Pages, authenticated CDN) only when downloading the complete deployed dataset is acceptable for each authorized viewer. Verify the platform actually enforces the access control assumed — for example, private GitHub Pages publishing requires GitHub Enterprise Cloud; on other plans the site is publicly reachable. If per-viewer restriction of rows or fields is required, keep the sensitive data behind an authenticated server boundary (rung 4 or 5).

## Implement and verify

1. Identify reads, writes, secrets, authorization, freshness, data size, and target devices.
2. State the selected rung of the architecture ladder and the evidence for any escalation.
3. Build a reproducible static artifact with its dependencies vendored and pinned, and keep deployment host-agnostic where practical.
4. **Confirm the built artifact actually runs — do not confirm only that the dependency tree resolves.** A green `install` and a resolvable graph prove nothing about runtime: load the page (or import the bundle) and assert the exact exports, symbols, and behavior you depend on. Runtime failures (a missing named export, a version-skewed API) are invisible to dependency resolution.
5. Verify correctness against the source implementation or database with parity tests.
6. Gate representative scenarios on transferred bytes, request count, WASM boot, and usable-content latency.
7. Test the exact production-sized artifact on a range-capable local server before claiming performance.

Prefer a deployable artifact over infrastructure scaffolding. Do not add Docker, a hosted database, queues, or an API process merely because they are conventional.

Stacks dApp architecture

skill: stacks-dapp-architecture

Revised after building Signer Sidekick — adds wallet SIP-030 capability, source verification, read-resilience, and network/chain-id handling.

---
name: stacks-dapp-architecture
description: "Choose the architecture, deployment, security posture, and wallet support for a Stacks (or similar wallet-and-chain) decentralized application: playgrounds, single-feature demos, testnet experiments, and production value-moving dApps such as swaps, DEXs, lending, and token sales. Use when building or deploying a dApp frontend, deciding whether it needs a backend, wiring wallet connection and contract calls, pinning contract identity, choosing which wallets to support, or moving something from a demo to real value. Governs the security tier separately from the deployment cost, treats wallet SIP-030 capability as part of the security model, and enforces a hard boundary between throwaway demos and anything that moves real assets."
---

## Scope and relationship to static-first

This skill governs dApps. It sits alongside `static-first-architecture`, which governs generic prototype deployment. Read both. A dApp frontend is still a static build in the ordinary case (see below), so the deployment ladder from that skill applies — including its rule that a page which fetches its libraries from a CDN at load is *deployable-static but not self-contained*, so pin and vendor dependencies with a local fallback. What this skill adds is a **second, independent axis: security tier**, and a **third input: wallet capability.** Cheap deployment, relaxed security, and "whatever wallet the user has" are three separate choices; the dangerous mistake is carrying a playground's relaxed security — or an unverified wallet — into something that moves real value.

Do not treat a dApp as "just a quick thing you show colleagues." A dApp can be that — a playground or a single-feature testnet demo — but the same code shape also underlies swaps and token sales, and the frontend, not a backend, is where much of the user-protecting security lives. Establish the tier explicitly before writing code.

## Why a dApp usually needs no backend of its own

A Stacks dApp is close to the ideal static-first case, because the concerns that normally force a server are handled by infrastructure that is neither yours nor a server you run:

- **Keys and signing: the wallet.** Leather, Xverse, and other SIP-030 wallets hold the seed and sign. The frontend talks to them over the injected provider / `@stacks/connect` (8.x is SIP-030 JSON-RPC; 7.x was JWT-based). Keys never touch your frontend or any server. See "Wallet capability" below — support is not uniform across wallets.
- **Writes: the wallet.** `stx_callContract` / `stx_transferStx` build, the wallet signs, the wallet broadcasts. There is no write endpoint of yours to host.
- **Auth: wallet connect + signature.** Connection plus SIP-018 structured-message signing replaces credential servers. No session backend.
- **Reads: the public node / Hiro API.** Balances, contract state, and tx status come from public chain infrastructure over HTTPS. That is a public read, not your secret backend — but it is not a *reliable* boundary; see "Reads."

Consequence for the escalation logic in `static-first-architecture`: the triggers "secrets used at request time," "authoritative writes," and "server-enforced authorization" are, for a dApp, **satisfied by the wallet and the chain and therefore do NOT justify a backend of your own.** Do not stand up a server just because a dApp signs, writes, or authenticates.

A dApp does have its own genuine backend triggers, distinct from the generic list:

- Data the public API cannot serve at the needed shape or freshness (a custom indexer + database).
- A transaction relayer or sponsored-transaction service.
- Off-chain order matching or an order book.
- Private per-user data that must not be world-readable.

Escalate only for those, and keep the exception narrow.

## Reads: what can and cannot be snapshotted, and the public API is not a solved boundary

Live chain state — current balances, mempool, latest block, current pool reserves — is the "constantly changing" case and cannot be served from a static snapshot. A static SQLite/JSON snapshot is stale the moment a block lands. Read live state from the node/Hiro API.

Immutable historical or indexed data (past events, closed epochs) can be shipped as a static dataset and queried client-side per the `static-first-architecture` data ladder. That is an optimization to avoid running your own indexer; it is never the source of truth for anything a user acts on. Never snapshot state that a user will make a value decision against.

The public API is a dependency, not a given. Design for its failure modes:

- **Rate limits and CORS.** Unauthenticated public endpoints rate-limit; a rate-limited response can arrive header-less and surface in the browser as a *CORS error* rather than a 429, which misleads debugging. Throttle your own calls, back off, and degrade gracefully rather than hammering.
- **Fallback providers.** Have a second read path (an alternate public API, or the app degrading to "couldn't load" without breaking the page) rather than a single hard dependency.
- **Let advanced users point reads at their own node.** A read-node override (e.g. a URL parameter) lets signers/operators verify on-chain data against infrastructure they trust; scope it to reads only, never to what the wallet signs.
- **Know when you actually need an indexer.** Endpoints that enumerate large event sets time out at scale; that is a real, narrow backend trigger (a custom indexer), not a reason to serverize the whole app.

## WASM in a dApp

Same rule as `static-first-architecture`: do not compile the project to WASM. Use precompiled WASM libraries only where they earn it — secp256k1 signing/hashing helpers, a local Clarity VM, or a `clarinet format` build used for source verification (see "Verify the source"). Note that `clar2wasm` is primarily the node-runtime compiler; the browser-usable Clarity path is the Clarinet / Clarity-VM WASM simnet used by web IDEs and the JS test SDK. That simnet is a **playground and testing** tool (run and inspect contract calls with no network), not a per-dApp production runtime. Verify maturity before depending on it.

## Wallet capability is part of the security model

Wallet SIP-030 support is **not uniform**, and the gaps are exactly where the security lives. Before you support a wallet, verify — against the wallet's actual behavior, not the spec — that it implements the methods your safety model depends on. In practice this means:

- **Post-conditions on contract calls and deploys.** A wallet that ignores or rejects `postConditions` / `postConditionMode` on `stx_callContract` / `stx_deployContract` cannot enforce the primary user-fund protection. Real example: some popular wallets accept the call but drop post-conditions entirely. **A wallet that cannot enforce post-conditions is unusable for asset-moving calls — detect it and refuse, with a clear explanation, rather than silently signing without protection.** Signing unprotected to "support more wallets" is the wrong trade.
- **The connect / address path.** Wallets differ on `stx_getAddresses` vs `getAddresses` vs `wallet_connect`; a wallet may register only a Bitcoin provider for what your library treats as the Stacks connect call, causing connect to hang with no error. Confirm connect actually returns a usable Stacks address for each supported wallet.
- **Post-condition and payload types the wallet/library will serialize.** Connect libraries often whitelist only `stx`/`ft`/`nft` post-conditions; newer condition types (e.g. Stacks `staking-postcondition`, `pox-postcondition`) must be pre-serialized to wire hex or the library rejects them. Hardware wallets add another gate: a given Ledger app version may not sign certain post-condition types or certain contract-payload versions (e.g. a newer Clarity deploy). Design the flow around the real capability matrix — including a fallback path (deploy from a software wallet, then rotate admin to the hardware key) when hardware can't sign a payload yet.

Practical consequence: if the wallet-selection UI you get from a library can't represent "this wallet is not supported," build your own small selector so unsupported wallets are shown as blocked (linking to an explanation) instead of silently failing. Treat "which wallets we support, and why the others are blocked" as a documented, deliberate decision.

## Security tiers (the second axis)

Set the tier before building. The tier fixes both the financial exposure and — critically — which safeguards are allowed to be absent.

**Tier 0 — Playground / learning.** Local Clarity simnet (Clarinet WASM) or a scratch testnet contract. No real value, often no network. Purpose: learn the tech, try an idea. Deployment can be a private static build (org-only Pages is fine). Financial safeguards: not applicable, nothing is at stake.

**Tier 1 — Single-feature testnet demo.** One flow, on testnet, faucet tokens, shown to colleagues. No real value. Deployment: private static build. Financial safeguards: still not applicable.

**Tier 2 — Single-feature mainnet demo.** Real network, real (if small) value, limited scope. The moment real value is involved, full user-protecting safeguards apply regardless of how small the feature looks. There is no "it's only a demo" discount once mainnet assets can move.

**Tier 3 — Production value-moving dApp (swap, DEX, lending, sale, treasury).** Real value, adversarial environment, multiple users. Maximum posture, plus contract-level concerns (audit, oracle trust, front-running/MEV, governance) that are out of scope for a frontend but must be named and owned by someone.

### The invariants that never relax

These are structural, not financial. They cost nothing on testnet and they are the habits that get carried forward. Apply them at **every** tier, including playgrounds, so that a promoted demo is not born insecure:

- **Post-conditions that mirror the real asset flow, in Deny mode.** Always Deny mode. The *conditions* must match what the call actually does: an **explicit-amount** condition for a transfer out; the **correct protocol-specific condition** for a call that performs a protocol action but moves nothing to another principal (on Stacks pox-5, a `pox-postcondition` with `will-perform` for actions like unstake / stake-update); and **zero conditions** under Deny for a call that moves no assets to another principal at all (a deploy, a config/admin call, or a lock-in-place like `stake` — a spurious "amount sent" post-condition there fails with `SentEq 0`). Wrong here in either direction: a missing condition leaves funds unprotected; a mismatched condition aborts a correct call. Allow mode and missing-where-needed post-conditions are the default failure and must not appear even in a testnet toy. Verify the wallet and the library actually support and serialize the condition type you use (see "Wallet capability").
- **Never trust client-side validation for correctness.** Frontend checks are UX, not security. The contract and the post-conditions are the enforcement. Likewise, any user-facing claim about what a call does (what locks, when funds unlock, fee units, irreversibility) must be derived from the *verified contract source*, not assumed — wrong copy misleads a value decision as surely as a missing guard.
- **Pin and verify contract identity — the source, not just the address.** Hardcode the exact contract principal, and verify the deployed *source* matches the reviewed reference. Address-pinning alone is insufficient. See "Verify the source."
- **Scope token approvals/allowances tightly.** Prefer exact amounts over unlimited approvals.
- **Keys stay in the wallet.** Never handle a seed or private key in frontend code or config, not even a throwaway one, because the pattern propagates.

### Verify the source, robustly

Verifying a contract means comparing its source to a reviewed reference. Naive hashing misleads:

- A **raw byte hash** changes with any whitespace, newline, or line-ending difference.
- A **light canonical hash** (strip comments, collapse whitespace runs) still changes when a formatter (e.g. `clarinet format`) adjusts spacing *around* delimiters — so a formatted-but-identical contract reads as "unverified."
- A **structure/token hash** (tokenize the source; drop all whitespace, comments, and separators; hash the token stream) is formatting-independent and is the reliable "same code" check. It ignores comment text by design.
- Clarity's own `contract-hash?` is **`SHA-512/256` over the deployed source bytes** — a *different algorithm and normalization* than an app's `SHA-256`, so it will never equal your SHA-256 values; don't compare across them. The emerging ecosystem convention (draft SIP-043) pins the canonical form as `clarinet format` output hashed with `SHA-512/256`, which is what matches `contract-hash?`.

Offer a formatting-robust comparison (structure hash, or the SIP-043 convention if you can run `clarinet format`), and treat the pinned reference as an immutable, commit-addressed source, not a moving branch.

### What scales with the tier

- **Financial safeguards** (slippage bounds tuned to pool depth, sanity checks on displayed prices, confirmation friction, rate limits): irrelevant at Tier 0–1, mandatory from Tier 2 up.
- **Slippage tolerance** for any swap is a frontend-set parameter and a top cause of real-world losses when misconfigured — too loose invites sandwich/front-running, too tight wastes fees on failed txs. Owned by the frontend at Tier 2+.
- **Front-running / MEV awareness:** mempool visibility means a value-moving swap's intent is public before it lands. This is largely a contract/UX concern; name it and assign it at Tier 3.
- **Frontend integrity and anti-phishing:** for a public Tier 3 dApp the frontend must be publicly reachable, so "org-only access control" is not a security control here — interface phishing (fake URLs mimicking the real dApp) is a leading incident category. Integrity and authenticity of the served frontend matter more than restricting who can load it.

## Network identity: pin it, and prefer the primary testnet

Pin, per network, the **network name** the wallet expects, the **chain-id**, the **boot/system-contract principals**, and the **read API**. By default target two networks: the **primary public testnet** and **mainnet**. Custom/experimental testnets exist — usually spun up around consensus-breaking upgrades — and purely local networks (regtest/mocknet) exist for solo development, but neither invites sharing code with others, so prefer the primary testnet: it resets the least often.

**Testnets are volatile — expect resets, but understand what a reset changes.** A reset of the primary testnet keeps the **same chain-id and network identity**; what it wipes is chainstate — contracts you deployed, grants/signatures you produced, balances. So never rely on anything you built there before a reset: redeploy and re-derive, but you do not need to re-key the network.

**A *different* network is what changes the chain-id** — mainnet vs testnet, or a custom/experimental testnet vs the primary one. That is where chain-id mismatches bite. Anything that signs **offline** — a SIP-018 structured message such as a signer/authorization grant — bakes the domain **chain-id** into the signature, so using the wrong one (for example a retired custom network's) makes signatures silently fail to verify on the target network. Do not default the network silently: pass it explicitly on connect (some wallets otherwise fall back to mainnet when the active account differs).

When you preflight the live network, distinguish a genuine *wrong network* (different boot principal, e.g. SP- vs ST-prefixed) from a protocol *not yet activated* (same boot principal, node still reports the older contract) — they need different messages and different user actions.

## The playground-to-production guard

The core risk: someone builds a Tier 0/1 demo, it works, and it silently becomes a Tier 3 dApp while keeping the demo's relaxed security and cheap deployment. Prevent it structurally.

- **Mainnet + real value forces Tier 2+ automatically.** There is no path where a mainnet asset can move under playground rules. If the target is mainnet, apply Tier 2 safeguards from the first line of code.
- **The invariants above are enforced even at Tier 0**, so a promoted demo starts from correct patterns rather than teaching post-condition-free, allow-mode, unpinned-contract, or unverified-wallet habits that then ship to mainnet.
- **Crossing from Tier 1 to Tier 2+ is an explicit promotion, not a config flip.** Require, and surface to the user, a checklist: switch network to mainnet deliberately (and update chain-id everywhere, including offline signers); confirm post-conditions are Deny mode with conditions that match each call's real asset flow; verify the pinned contract *source* on mainnet with a formatting-robust hash; confirm every supported wallet actually enforces those post-conditions; add slippage bounds and price sanity checks; vendor and pin the exact dependency bundles served; remove every testnet shortcut (faucet assumptions, disabled checks, hardcoded values, allow-mode fallbacks); and for Tier 3, confirm the contract has been audited and that oracle, front-running, and governance risks have an owner. Do not let a demo reach real users by quietly repointing an RPC URL.
- **State the tier in the output** — to the builder and the promotion checklist. Do not necessarily stamp the internal tier taxonomy onto the end-user UI; end users need "testnet vs mainnet" and clear safety cues, not your internal tier labels.

Be conservative about the tier. When unsure whether something is a demo or the seed of a production dApp, treat it as the higher tier. The cost of over-securing a playground is a few unnecessary post-conditions. The cost of under-securing a swap is user funds.

Design and tone of voice

skill: reserved

Reserved for a design and tone-of-voice skill, to be added here later.

For now these apps share one stylesheet, stacks.css, vendored from the Stacks Labs design tokens: dark-only, self-hosted OFL fonts, no external CSS or CDN, and an accent that flips per network (violet on testnet, Stacks orange on mainnet). The rules that keep each page standalone: one self-contained file per app, no build step, no framework, and only esm.sh module imports for the Stacks libraries.

Signer Sidekick is interim tooling. The skills above are included verbatim.