Vitreus ChainForge
All docs

Building your chain

The day-to-day work loop.

Once you have a scaffold that compiles, the rest of the work is extension: shaping the custom pallet, composing additional pallets, tuning the runtime constants, verifying changes before you push. The pattern is the same for every shipped template; the vocabulary changes per template, but the loop and the conventions don't.

1. The shape of the work

The loop is six steps, and you'll spend most of your time in two of them:

  1. Describe what you want next. A clear English sentence — “add a claim_winnings extrinsic that lets a winning bettor claim a share of the loser pool.”
  2. Generate — your AI agent reads the CLAUDE.md, reads the existing pallet, and produces the change.
  3. Read the diff. Most days this is a few minutes. Some days it's longer.
  4. Extend or correct — accept what works, push back where the agent missed something. The corrections are usually one-sentence: “use try_mutate instead — the closure can fail.”
  5. Verify by running cargo check (and eventually cargo test when the template ships tests).
  6. Push — commit, push to GitHub, repeat.

Steps 3 and 4 are the load-bearing ones. The agent does the typing; you do the judgment. The conventions section below gives you a checklist for what to look for in the agent's output.

2. Pallets, in plain words

You've already met the term pallets on the scaffold form — they're the building blocks the generator wires up. A bit more here, now that you're extending one:

A pallet is a focused unit of chain behaviour. The token-governance template wires pallet_balances (a native token), pallet_collective (a council), and pallet_democracy (public referenda), then adds a custom pallet on top (pallet_governance in that template) that encodes the chain-specific logic. Each pallet owns its piece — its storage, its extrinsics, its events. The pieces compose because they share types and interfaces at the runtime level — the runtime is what wires every pallet together into one chain.

When you ask your agent to “add a claim_winnings extrinsic to the prediction-market pallet,” the agent is editing one file: pallets/pallet_prediction_market/src/lib.rs. The runtime that wires that pallet doesn't need to change. When you ask to “add a fee that goes to a treasury,” the agent might need to add a second pallet and edit the runtime to wire it — that's when §5 (runtime extension) becomes relevant.

3. Asking your AI agent for the right thing

The CLAUDE.md tunes the agent for your project. Prompts that work well share three properties: they reference an existing thing, they name what should change, and they leave the mechanism to the agent (it knows the conventions, you don't need to repeat them).

Concrete prompts for each shipped template:

  • prediction-market: “Add a claim_winnings(market_id) extrinsic that lets a winning bettor claim a proportional share of the loser pool. Use the bettor's stake as numerator and the sum of losers' stakes as the bonus pool. Bound the per-call payout by the bettor's own claim — no looping over winners — so resolution compute stays bounded per call.”
  • marketplace: “Add a cancel_offer(listing_id, offer_id) extrinsic that lets a buyer withdraw their own offer before the seller accepts. Mirror cancel_listing — same drain shape, but the origin check is offerer == caller instead of seller == caller.”
  • token-governance: “Add a batch_endorse extrinsic that lets an account endorse up to 8 hashes in one transaction. Bound by MaxProposalsPerAccount; return early if any one fails to maintain atomicity.”
  • generic: “Add a remove_item extrinsic that takes an index and removes the entry from the BoundedVec storage.”

What works less well: prompts that name files (“edit lib.rs on line 187”) or mechanisms (“use Vec::push”). The agent is doing better when you let it pick the mechanism — the CLAUDE.md tells it which patterns this project follows.

4. The conventions your agent should follow

This section is for the developer reviewing the agent's output. If you're reading along as a non-developer, the gist is: there are seven conventions a Vitreus-shaped pallet follows. Your AI agent handles them; the section below is what you'd spot-check if the agent missed something.

These are the patterns the rendered CLAUDE.md's footguns section nails for the agent. Same content, framed here for the human reviewer. Each subsection has a stable anchor — CLAUDE.md fragments deep-link to these for the “why” when the “how” alone isn't enough.

4.1 — Bounded storage, always

BoundedVec<T, MaxN> over Vec<T> in storage, with MaxN declared as a Config constant. An unbounded Vec is a DoS vector — an attacker fills it until reads exceed the runtime weight budget and the chain stalls. The compiler refuses to let you forget the bound when the type carries it.

Lookup detail in reference #unbounded-vec.

4.2 — try_mutate for fallible operations

When a storage closure can fail, use try_mutate (which propagates the closure's Err) over plain mutate (which swallows it). With a BoundedVec, try_push is fallible, so try_mutate is the only correct shape.

Lookup detail in reference #mutate-instead-of-try-mutate.

4.3 — Modern fungible / fungibles surfaces

Use the fungible::Mutate family (native token) and fungibles::Mutate family (multi-asset) for transfers. The legacy Currency::transfer and ReservableCurrency traits are deprecated. For held funds, the modern pattern is a composite HoldReason enum on the pallet plus fungible::MutateHold against a RuntimeHoldReason associated type — see the prediction-market template's place_bet extrinsic for the canonical example.

Lookup detail in reference #legacy-currency-reserve.

4.4 — DecodeWithMemTracking on every SCALE-decodable custom type

polkadot-sdk stable2603 requires every custom enum or struct used in storage / events / extrinsic args to derive DecodeWithMemTracking alongside Decode. The derive isn't auto-implied and isn't in the prelude. Every shipped template applies it; new types your agent adds need it too.

Lookup detail in reference #missing-decode-with-mem-tracking.

4.5 — Real weight annotations on every dispatchable

Every extrinsic carries a #[pallet::weight(T::WeightInfo::<name>())] annotation referencing a real WeightInfo trait impl. The shipped templates use a placeholder () impl with non-zero constants — fine for developer iteration, but replace with benchmarked weights before any chain that charges fees. #[pallet::weight(0)] silently compiles to a free DoS vector.

Lookup detail in reference #weight-zero.

4.6 — Explicit origin checks on every state-mutating extrinsic

Every dispatchable that writes calls ensure_signed(origin)? (the standard signed case), ensure_root(origin)? (sudo-only), or a Config-trait associated EnsureOrigin (e.g. ResolveOrigin in prediction-market). Never let the agent ship an anonymous write — every shipped pallet's extrinsics begin with an origin check.

Lookup detail in reference #anonymous-extrinsic.

4.7 — Stable #[pallet::call_index]

Every dispatchable carries an explicit #[pallet::call_index(N)] attribute. Call indices are part of the chain's API — reordering or removing dispatchables without keeping indices stable breaks every signed extrinsic in flight. Treat them like storage keys.

Lookup detail in reference #missing-call-index.

5. Extending the runtime — parameter_types! and Config impl

This section is for the developer wiring a new pallet or tuning the constants. If you're reading along, the gist is: the runtime is the file that wires the pallets together; the constants are the numbers each pallet uses, tunable from a single block.

The runtime lives at runtime/src/lib.rs. Three load-bearing blocks:

  1. parameter_types! — declares the constants every pallet's Config uses. Tuning a cap (e.g. MaxOffersPerListing in marketplace) means editing one ConstU32<N> in this block.
  2. impl <Pallet>::Config for Runtime blocks — one per wired pallet. Each names the concrete types that satisfy the pallet's Config trait.
  3. construct_runtime! — the macro that composes every impl block into one runtime type, assigning each pallet a stable index.

When your agent adds a new pallet, all three blocks get an entry. Reordering existing entries in construct_runtime! breaks storage migrations — add new pallets at the end.

The full per-template constant table lives in reference §6.

6. Storage, weight, and the not-a-mainnet-yet gotchas

This section is for the developer preparing for testnet or mainnet. If you're reading along: the shipped templates are tuned for dev. There are three classes of change to make before a chain processes real volume.

The shipped templates ship developer-iteration values. Before any real deploy:

  • Replace placeholder weights. Every extrinsic uses WeightInfo = () with constants like Weight::from_parts(15_000, 0). These are guesses. Real benchmarks produce a concrete WeightInfo impl in a separate weights.rs file.
  • Tune the caps. Constants like MaxBetsPerMarket, MaxOffersPerListing, MaxProposalsPerAccount ship at developer-friendly values (32–64). Production tuning depends on your chain's expected throughput; the reference config table lists the defaults so you have something to depart from.
  • Bump spec_version on every breaking change. The runtime declares a RuntimeVersion; spec_version must increment when storage layouts or extrinsic signatures change. Substrate refuses to apply a runtime upgrade that drops or rolls back this number.
For now: A future validator will lint for these before deploy. Today the conventions in §4 plus the “footguns” section of your generated CLAUDE.md cover the same ground; your AI agent applies them when prompted, and the manual checklist here is the human-side equivalent.

7. Verifying before you push

The minimum bar: run cargo check at the workspace root and confirm it returns clean. That covers everything the type system can catch — missing derives, unbounded storage that the type system can't prove bounded, the FRAME macros refusing to expand against an invalid pallet shape.

Beyond compile-time: read the diff. The conventions in §4 are the checklist. Look for ensure_signed at the top of every state-mutating extrinsic. Look for BoundedVec on any new storage that holds a collection. Look for try_mutate on storage closures that can fail. Look for a real WeightInfo reference on every new dispatchable.

For now: A future Vitreus-specific lint pass will run these checks mechanically. For now, cargo check + the §4 conventions list + spot-reading the agent's diff is the working substitute. Most days that's a five-minute loop.

8. Composing two pallets — the worked example

This section is for the developer adding a second pallet (e.g., bolting an oracle onto prediction-market, or a royalty pallet onto marketplace). If you're reading along: this is what cross-pallet integration looks like at the code level.

Every shipped template uses the same composition pattern: the custom pallet's Config trait declares associated types, and the runtime's impl Config for Runtime block wires concrete pallet instances to those types. Three shipped examples:

  • prediction-market declares ResolveOrigin: EnsureOrigin. The runtime wires it to EnsureRoot for the hello-world; production templates swap to an oracle pallet, a multisig, or a council track.
  • marketplace declares AssetTransfer: fungibles::Mutate. The runtime wires it to Assets (pallet_assets) for the hello-world; an NFT marketplace would swap to pallet_nfts via the nonfungibles surface.
  • prediction-market + marketplace both declare NativeBalance: fungible::Mutate. The runtime wires it to Balances in both cases.

When you add a second custom pallet, the pattern repeats: declare what it depends on as an associated type, wire it in the runtime, and (if both pallets share a hold reason or a balance type) the runtime ties them together via the same RuntimeHoldReason or balance type. The shipped templates' Config blocks are the working catalog.

For now: A “Compose” surface that walks cross-pallet integration interactively isn't shipped yet. For now, copy a pallet from one template into another and re-wire the Config trait — the four shipped templates' Config shapes are the working examples of how the pattern plays out.