| name | xl1-scaffold |
| description | Bootstrap a new XL1 application — a single React dApp, a single xl1-service backend, a plain Node.js service/CLI, or a full-stack pnpm monorepo with React + xl1-service + a shared TypeScript library. Activates when the user wants to create, start, bootstrap, initialize, or scaffold a new XL1 project. Do NOT activate for work in an existing project. |
| metadata | {"version":"1.1.31"} |
XL1 Scaffold
Use this skill only when the user is starting a new XL1 project. For work in an existing repo — adding features, fixing bugs, or answering questions about XL1 concepts — use the appropriate lower-layer skill instead (xl1-knowledge for the chain, xl1-patterns for design patterns).
Skill identity. This skill's version is exposed in this file's frontmatter under metadata.version. When reporting which skills informed your work, format as <skill-name> v<version> (e.g. xl1-scaffold v1.1.19). When multiple skills from this plugin are active, each may be listed.
How to recognize the trigger
Activate when the user says anything like:
- "Build me a ___ on XL1"
- "Create a new XL1 dApp"
- "Start a React dApp that uses the wallet"
- "Scaffold an XL1 service that reads from the datalake"
- "Bootstrap a new project using @xyo-network"
Do not activate when:
- The current directory already has a
package.json depending on @xyo-network/*
- The user is describing a feature, bug, or concept within an existing project
- The user is asking how something works (that's reference, not scaffolding)
If ambiguous, ask one question: "Are you starting a new project, or adding to an existing one?"
If the user wants a new project but the request is vague or exploratory — they haven't named the archetype, the patterns, or the multi-party shape — defer to xl1-build first. That skill runs a short planning wizard and hands a refined prompt back here. Concrete prompts (the archetype is named, the patterns are implied, the network is specified) skip the wizard and come straight to this skill.
Interpreting the prompt
Before invoking the scaffold, scan the prompt and the working directory for conventions that change how you build and verify the dApp. These cues are easy to miss because they look like incidental context, but each one prescribes specific behavior.
.env file with a seed phrase already in the empty repo. The user expects the dApp to be headless-verifiable end-to-end by an agent or CI, using the same identity a browser user would have on that seed. Build the dApp normally — browser wallet wiring intact, React providers, the works — but also write a Node verification script that exercises the happy path through GatewayBuilder.buildRunner() and run it before reporting completion. The seed phrase is for verification only; it does not change the UX surface of the dApp. Read Headless dApp Verification before planning — it documents the prompt shape, signer derivation, and anti-patterns. The DoD's Headless Verification section becomes mandatory in this case.
- Prompt names which accounts hold funds (e.g. "accounts 0 and 1"). Tells you how many signers to derive and how to assign roles in multi-party flows.
- Prompt names a network (mainnet / sequence / local). Determines the
DefaultNetworks entry the scaffold's gateway points at and which INDEXER_FLOOR_BLOCK is captured.
Treat any of these cues as load-bearing. Surface them in your acknowledgement before scaffolding so the user can correct any misread.
PRD.md fallback (read)
If the prompt itself is missing the synthesized spec from xl1-build — for example, a fresh conversation where the wizard ran in a prior session — check the cwd for a PRD.md file before asking the user to repeat themselves. The PRD is written by xl1-build at the end of its Phase 4 and contains the same fields as the inline synthesized prompt (Shape, Network, Patterns, Multi-party, History/browse, Backend, Headless verification, Notes) under structured markdown headings, plus an ## Acceptance criteria section that carries Layer 3 of the completion gate. Read it the same way you would read the inline prompt.
If both the chat prompt and PRD.md are absent, fall back to asking the user — or, if the request is exploratory enough, defer to xl1-build to generate a PRD first.
Writing PRD.md from an inline prompt (advanced-user path)
When the user gives a concrete spec inline (skipping the wizard) and no PRD.md exists at the cwd, write one before scaffolding. This closes the gap so every project has a Layer 3 completion gate regardless of how it was kicked off.
The shape mirrors what xl1-build Phase 4 produces — same sections, same headings. The data sources differ:
- Build target fields (Shape, Network, Patterns, Multi-party, History/browse, Backend, Headless verification) are derived from the inline prompt the same way you derive scaffold inputs.
- Acceptance criteria are generated by you following xy-development/workflow.md § Writing Project-Specific Acceptance Criteria: 5–10 observable bullets, split into positive and negative assertions, drawn from the inline prompt + the loaded domain skills (
xl1-patterns/* for the matched patterns, dapp-checklist.md for anti-patterns to convert into negative criteria).
- Skills referenced is the list of xyo-skills plugin skills that informed this plan (typically xl1-scaffold + the relevant pattern docs).
- Plugin version uses
xl1-scaffold's metadata.version from this file's frontmatter (since the wizard didn't run, attribute to the scaffold).
- Notes captures any free-form constraint from the prompt that doesn't fit a slot.
Write the file at the cwd as PRD.md. If the user's prompt is too vague to derive acceptance criteria — for example, they said "build me something on XL1" — do not guess; defer to xl1-build to run the wizard properly. Generating weak criteria from a vague prompt is worse than no criteria at all because it makes the loop appear to terminate when the spec was never concrete.
Surface the written PRD in your acknowledgement so the user can correct any misread before scaffolding starts:
Wrote PRD.md capturing the spec and acceptance criteria. Review it briefly — anything off, tell me before I scaffold.
Invocation
The scaffold runtime ships with this plugin under scripts/scaffold/ (synced from packages/xl1-scaffold/ at build time). Invoke it directly with Node — ${CLAUDE_SKILL_DIR} resolves to this skill's directory at runtime.
Choosing template(s)
This skill scaffolds either a single project or a full-stack monorepo, based on whether the dApp needs ongoing chain tracking.
Available templates
| Template | When | Used standalone? |
|---|
react (default) | User-facing dApp that reads current chain state and submits transactions on user action. Most browser-facing apps. | ✓ also a monorepo member at packages/app |
xl1-service | Node + Express HTTP service backed by @xyo-network/xl1-sdk. The backend for any dApp needing chain-watching, indexing, or scheduled jobs. | ✓ also a monorepo member at packages/service |
node | Plain Node CLI/script — rare from this skill; usually a backend that needs HTTP routes prefers xl1-service. | ✓ standalone only |
xl1-monorepo | pnpm workspace root for a full-stack dApp. No source of its own — just pnpm-workspace.yaml, root scripts, and a README. | monorepo flow only (step 1 below) |
xl1-shared | TypeScript library for environment-neutral code (types, Zod schemas, constants) shared between app and service. Compiles to dist/ so workspace consumers import compiled JS via workspace:*. | monorepo flow only (member at packages/shared) |
Default to the monorepo flow (xl1-monorepo + xl1-shared + react + xl1-service). Drop to a single template only when the prompt is clearly read-only / browser-only (e.g. "wallet balance dashboard", "show recent transactions"). When in doubt, scaffold the monorepo — it's strictly a superset; an unused service is cheap to delete later, but retrofitting a service into a single-template scaffold is annoying.
Full-stack monorepo (xl1-monorepo + react + xl1-service + xl1-shared)
Scaffold a monorepo when the dApp needs logic that runs independently of any user's browser session. Triggers:
- Multi-actor coordination across time — e.g. commit-reveal flows where reveals happen long after commits and the browser may have closed
- Indexing or aggregating chain data — leaderboards, history pages, search across past events
- Time-based deadlines — auction settlements, period closes, reveal windows that need to be detected when blocks elapse
- Notifications or scheduled jobs — anything triggered by block arrival rather than user click
The monorepo gives the React app and the xl1-service a place to share code via the xl1-shared library — typically API request/response types, Zod schemas, and constants used on both sides.
Examples mapped to decisions:
| Prompt fragment | Decision |
|---|
| "two-player rock-paper-scissors with commit-reveal", "browse past games" | monorepo — service watches reveals, indexes history; shared holds Move/Game types |
| "prediction market with settlement deadline" | monorepo — settlement runs independently; shared holds Market/Outcome types |
| "wallet balance dashboard" | react only |
| "one-click contract interaction" | react only |
| "show recent transactions for an address" (read-on-view) | react only |
| "real-time leaderboard updated as blocks arrive" | monorepo |
The xl1-patterns skill catalogues patterns that imply a backend — commit-reveal, chain data indexing, prediction markets all do.
When ambiguous
Default to the monorepo flow. Don't ask the user — just scaffold it. The monorepo is a superset of the single-template options; unused parts (e.g. service if the app turns out to be read-only) are easy to delete later. Retrofitting a backend into a standalone React scaffold is much harder.
Only drop to a single template (react) when the prompt is unambiguously read-only — wallet balance dashboards, one-shot contract interactions, simple display dApps. If you find yourself even slightly unsure, choose the monorepo.
The clarifying question pattern ("does any logic need to run independently of a browser session?") is discouraged — it adds round-trip latency and the answer is almost always "yes, eventually." Skip it; default to monorepo and let the user prune if needed.
Target directory
- If the user named a directory ("put it in
./rps-game"), use that as the root.
- Otherwise, default to the current working directory if empty, or to a subdirectory named after the app being built (e.g.
./rps-game from "build me a rock-paper-scissors game").
- Confirm the path in a single sentence before running.
For a monorepo, scaffold sub-packages under packages/:
<root>/
├── package.json, pnpm-workspace.yaml, README.md, .gitignore ← from xl1-monorepo
└── packages/
├── app/ ← react template
├── service/ ← xl1-service template
└── shared/ ← xl1-shared template (types/schemas used by app + service)
The monorepo templates wire app ↔ service the same way every time: app on :3000, service on :3001, all service routes under /api/*, Vite dev-proxies /api/* to the service so the browser stays same-origin, and the workspace root's pnpm dev runs both concurrently. Do not add CORS middleware to the service — there is nothing to CORS for in this topology, and adding it "just in case" is the anti-pattern that the prescription exists to prevent. The full pattern (including the prod reverse-proxy story and the cross-origin escape hatch) is documented in Browser ↔ Service Wiring. When implementing the dApp, fetch with relative paths (fetch('/api/foo')) — no VITE_API_URL, no hardcoded host.
Run the scaffold
For a single template:
node "${CLAUDE_SKILL_DIR}/scripts/scaffold/scaffold-xl1-dapp.js" <root> --template=<react|node|xl1-service>
For a monorepo, four scaffold invocations + a single root install:
SKILL="${CLAUDE_SKILL_DIR}/scripts/scaffold/scaffold-xl1-dapp.js"
# 1. Workspace root (no install — verification skipped, root has no installable code yet).
# The root's package.json `name` field is the dir's basename (e.g. `rps-game`); sub-
# package scopes auto-derive from this (sub-packages become `@rps-game/<basename>`).
node "$SKILL" <root> --template=xl1-monorepo --no-install
# 2. Sub-packages — each runs in workspace-member mode (skips its own install + verification;
# drops its own packageManager + .gitignore; package name becomes @<scope>/<basename>).
# Order matters: scaffold shared FIRST. When app and service are scaffolded after,
# they auto-detect the sibling shared package and wire a "@<scope>/shared": "workspace:*"
# dep into their package.json automatically.
node "$SKILL" <root>/packages/shared --template=xl1-shared --workspace-member
node "$SKILL" <root>/packages/app --template=react --workspace-member
node "$SKILL" <root>/packages/service --template=xl1-service --workspace-member
# 3. Single install at the workspace root — links all members + installs deps for all.
cd <root> && pnpm install
# 4. Verify across the workspace.
pnpm -r run typecheck && pnpm -r run lint && pnpm -r run build
# For xl1-service specifically, also smoke-test the server boot:
pnpm --filter @<scope>/service run smoke
The scaffold (per single-template invocation, when NOT in workspace-member mode):
- Resolves the full runtime dependency graph from the npm registry (direct deps + peers).
- Writes
package.json, tsconfig.json, eslint.config.mjs, vite.config.ts (React), source files, and .gitignore.
- Runs
pnpm install, then pnpm typecheck, pnpm lint, pnpm build, and — for the Node and xl1-service templates — a smoke test that executes the compiled output.
- Prints per-step status; the final line says "Scaffold complete" on success.
In --workspace-member mode, steps 1–2 still run (with the workspace-aware tweaks); step 3 is skipped (the root install handles all linking + verification).
If any step fails, the scaffold exits non-zero. Relay the failing output to the user verbatim before attempting a fix.
Working with the shared package
xl1-shared is intentionally a thin library — its sample src/index.ts is just export {} plus a comment block. Fill it in as you discover what app and service need to agree on.
Belongs in shared/:
- API request/response types between app and service (e.g.
interface SubmitCommitRequest, interface GameStateResponse)
- Zod schemas validated on both sides
- Game/business enums and constants used in both UI and backend (e.g.
enum Move { Rock, Paper, Scissors })
- Branded ID types (
type GameId = Brand<string, 'GameId'>)
Does NOT belong in shared/:
- React components, hooks, browser globals →
packages/app/src/
- Express handlers, Node-only globals (
fs, child_process) → packages/service/src/
@xyo-network/xl1-react-client-sdk — browser-only, lives in app
@xyo-network/xl1-sdk — typically lives in service (or both if app needs read-only chain queries; either way, don't re-export it through shared)
Adding cross-package imports:
When you scaffold app and service AFTER shared (the recommended order), the scaffold auto-wires "@<scope>/shared": "workspace:*" into their package.json dependencies. No manual edit needed for the initial scaffold.
If you scaffold a workspace member out of order or add one later, edit its package.json manually to add:
{
"dependencies": {
"@<scope>/shared": "workspace:*"
}
}
Then pnpm install at the workspace root re-links. Imports work via the package's name field:
import type { SubmitCommitRequest, GameStateResponse } from '@<scope>/shared'
When you add new exports to shared/, run pnpm --filter @<scope>/shared run build (or pnpm -r run build from root) so consumers see the updated dist/.
Hand-off behavior
After the scaffold reports success:
-
Briefly summarize what was created (template(s), target dir(s), one-line "ready to develop"). For monorepos, mention the workspace root + each sub-package and remind the user that pnpm install at the root links everything.
-
Capture the indexer floor block — see the next section. Part of dApp creation, not deferred to first deploy.
-
Proactively continue implementing whatever the user originally described. Do not stop and wait. For monorepos, the natural ordering is:
- Define shared types/schemas in
packages/shared/src/ based on what the feature needs and export them for consumers.
- Implement service routes/jobs in
packages/service/src/ (chain-watching, indexing, deadline handling).
- Implement app UI in
packages/app/src/ consuming the service routes via fetch.
Consult xl1-patterns for the canonical recipe (commit-reveal, indexing, etc.).
-
Only pause if you need a clarifying decision that can't be inferred from the original request (e.g. "should players be able to rematch without re-committing stakes?").
-
Before reporting the work complete, walk the full three-layer completion gate (see xy-development/workflow.md § Applying the Definition of Done):
- Layer 1 — Generic DoD (xy-development/workflow.md): builds, lints, tests, dev server, no placeholders, no regressions
- Layer 2 — dApp Definition of Done: XL1/browser-specific gates
- Layer 3 —
PRD.md acceptance criteria (in the cwd, if present): project-specific positive and negative assertions
This is mandatory, not optional, and it is an agent-facing completion gate — it gates whether you stop and report done, independent of any later deploy or release. If any item across any layer fails, keep iterating until it passes — do not stop and report partial completion. Fix the failing item, re-walk the relevant layer, and only stop when every applicable layer is fully green. The Layer 2 checklist enumerates exactly the rules and anti-patterns the rest of the skill stack documents, and an unwalked DoD is the most common cause of completed dApps that violate them. In your completion summary, call out each relevant section across all three layers with explicit pass/fail and a one-line note on why. Sections marked "(if applicable)" can be skipped only when truly out of scope (e.g. no commit-reveal in a read-only dashboard); state that they were skipped and why. If the prompt included a seed-phrase (see "Interpreting the prompt"), the section is mandatory — you must have run the verification script and report its outcome, not just check the boxes. If the dApp exposes derived state through a service (indexer REST API, GraphQL, etc.), the verify run must have round-tripped through that service surface — never via direct synthesis — and gated on both and the indexer's watermark. Reporting "verified" when only the chain edge was exercised, or rationalizing an empty indexer as "Sequence is slow" without checking the watermark, is the failure mode the exists to prevent.
Capture the indexer floor block
If the dApp introduces its own payload schemas (the default — any custom dApp under a fresh com.<your-org>.<app>.* namespace; see Schema Naming), capture a sensible floor block as part of dApp creation and record it in .env as INDEXER_FLOOR_BLOCK. The agent does this; the user never sees the step. This is part of the dApp Definition of Done — see dApp Checklist.
The reason: the chain accepts arbitrary bytes for any schema, including before the dApp existed. An indexer that walks from block 0 will (a) waste hours on blocks that provably contain none of the dApp's data and (b) honor pre-deployment matches that cannot be the dApp's data. Capturing a floor makes correctness and performance the default. See Chain Data Indexing — Floor Block for the full framing.
Anchor the capture to development time, not first publish. Publish steps are routinely deferred, automated, or skipped, and an "after first publish" rule frequently misses. Capturing during dApp creation is unambiguous and runs every time. Precision isn't the goal — performance optimization is. A few blocks of slack on either side don't matter.
The procedure:
- Connect to the target chain using the gateway already wired into the scaffold.
- Read the current finalized head:
Number(await viewer.finalization.headNumber()).
- Write to the dApp's
.env (root for single-template scaffolds; the relevant sub-package's .env for monorepos):
INDEXER_FLOOR_BLOCK=<n>
VITE_INDEXER_FLOOR_BLOCK=<n> # only if there's a Vite-built browser package
- Reference
INDEXER_FLOOR_BLOCK from the indexer (process.env) and from the browser dApp (import.meta.env.VITE_INDEXER_FLOOR_BLOCK) so all readers share the same floor.
INDEXER_FLOOR_BLOCK is per chain. A dApp scaffolded for mainnet, sequence, and a local devnet has three different .env files with three different captured values. Don't reuse a floor across environments.
For an unbounded dApp — one whose purpose is to index pre-existing schemas (an XL1 transfer indexer, an inscription-substrate indexer, an XRC-20 ledger for an existing token) — set INDEXER_FLOOR_BLOCK=0 explicitly. The env var is required either way; there is no silent default. State the unbounded choice in the hand-off summary so it's auditable.
Mixed dApps (some bounded schemas, some pre-existing) still set INDEXER_FLOOR_BLOCK to the captured head; per-schema floors live in the indexer code per Chain Data Indexing — Mixed indexers. Mixing is the last resort — splitting into separate bounded and unbounded indexer processes is strictly faster.
Flags reference
| Flag | Values | Default | Notes |
|---|
--template, -t | react, node, xl1-service, xl1-monorepo, xl1-shared | react | xl1-service extends node with an Express HTTP server bound to process.env.PORT || 3001 (matching the app-on-3000 / service-on-3001 wiring convention) and a Hello world route. xl1-monorepo and xl1-shared are used together with the other templates in the monorepo flow — see "Choosing template(s)" above. |
--target | path | positional[0] or src | Can also pass as first positional arg |
--force | — | off | Overwrite files in a non-empty target dir |
--no-install | — | off | Write files only; skip pnpm install and verification |
--workspace-member | — | off | Sub-package mode: rename to @<scope>/<basename>, drop packageManager + .gitignore, skip the install/verification chain. The workspace root's pnpm install handles all members. |
--workspace-scope | string | auto-derived | Override the scope used in --workspace-member mode. Default: walk up to find pnpm-workspace.yaml, read the root package.json name, use it as the scope (rps-game → @rps-game; @org/foo → @org). |
Troubleshooting
Fetch errors during version resolution (ENOTFOUND, ETIMEDOUT) — transient npm registry issues. Retry once. If persistent, check the user's network.
pnpm ERR_PNPM_MISSING_TIME — the scaffold pins pnpm to the latest 10.x via packageManager + corepack specifically to avoid this; it should not appear in normal runs.
"Target dir is not empty" — pass --force to overwrite in place, or pick a different target.
When to point users elsewhere
- After the scaffold completes and the user wants to implement a feature → xl1-patterns (commit-reveal, indexing, prediction markets, datalake access).
- For questions about specific XL1 chain concepts (blocks, validators, fees, rewards) → xl1-knowledge.
- For XYO primitives (payloads, bound witnesses, modules) → xyo-knowledge.
- For toolchain-level questions (ESLint rules, TypeScript config, vitest setup) → xy-toolchain.