Agenda

What we'll cover

From the team that built Stellar CLI before Soroban was born.

HackMeridian 2026 · The Aha Company

  1. 01

    Who we are

    The Aha Company and today's speakers

  2. 02

    Stellar Scaffold

    App bootstrap and config: from idea to working dApp, fast

  3. 03

    Stellar Registry

    Wasm and contract publishing and discovery

  4. 04

    Nido

    A passkey-based smart account wallet

  5. 05

    Your next step

    What to build next, then questions

Speakers

Who you'll hear from

Pam Selle

Principal Engineer · The Aha Company

A technologist with over a decade in software engineering and leadership. Pam maintained Terraform Core as a Senior Software Engineer at HashiCorp, and before that worked on web properties and cloud infrastructure at Comcast. She has long organized events in the JavaScript and wider developer communities.

Willem Wyndham

Chief Science Officer · The Aha Company

Willem specialized in programming languages and computer security during his PhD at the University of Maryland, then left to join NEAR Protocol as a researcher focused on Rust and WebAssembly. As its Contract Runtime Engineer he worked deep inside blockchain execution environments and the security challenges that come with them.

The big idea

Every save rebuilds, redeploys and regenerates your typed client. Spend HackMeridian on your app, not the plumbing.

The hackathon problem

  • Build Rust → Wasm
  • Deploy, copy the contract ID
  • Generate bindings, paste the ID into the frontend
  • Wire up a wallet and network config
  • …then change one function and do it all again

Three things to remember

scaffold.yml = your dependencies

Your app's contract dependencies, per network: what they are and where to get them.

Secrets and machine settings go in .env.

app-lib/clients = generated

A typed client per contract, per network, regenerated on every build and gitignored. Import by name; never paste an address.

Don't edit it: wrap clients under app/.

npm run dev = the loop

Save a contract and it rebuilds, redeploys on the local network, and regenerates its client.

App runs at localhost:5173.

Set up and start a project


cargo binstall stellar-scaffold-cli stellar-registry-cli
# no binstall? cargo install --locked (builds from source)
stellar scaffold doctor   # checks your setup
stellar scaffold init my-app   # react, svelte or none
cd my-app && npm install && npm run dev
    

my-app/
├── contracts/     Rust → Wasm
├── app/           your frontend
├── app-lib/       wallet, network
│   └── clients/   generated
└── scaffold.yml   dependencies
    
init asks which frontend you want

$ stellar scaffold init my-app
Creating new Stellar project in ./my-app
? Pick a framework ›
❯ react
  svelte
  none (no frontend)
      

Needs Rust, Node, the Stellar CLI, and Docker. Scaffold and Registry install together as stellar plugins.

Your frontend: React, Svelte or none

Pick react, svelte or none at init. The React and Svelte templates ship wallet connect, network switching, and a working GuessTheNumber component, all using the same generated clients: import { guessTheNumber } from "@stellar-scaffold/app-lib/clients".

Only need contracts? stellar scaffold init my-app --template none (or pick none) skips the frontend entirely: no app/, app-lib or generated clients.

React · GuessTheNumber.tsx

const { address, signTransaction } =
  useWallet()
const tx = await guessTheNumber.guess(
  { a_number: 5n, guesser: address },
  { publicKey: address },
)
await tx.signAndSend({ signTransaction })
      
Svelte · GuessTheNumber.svelte

import { address, signTransaction }
  from "../stores/wallet"

const tx = await guessTheNumber.guess(
  { a_number: 5n, guesser: $address },
  { publicKey: $address },
)
await tx.signAndSend({ signTransaction })
      

The loop · 1 of 3

Edit: add a method to the contract

In contracts/guess-the-number/src/lib.rs, next to guess():


/// Is the answer higher or lower than your guess?
pub fn hint(env: &Env, a_number: u64) -> Symbol {
    let n = Self::number(env);
    if a_number < n {
        symbol_short!("higher")
    } else if a_number > n {
        symbol_short!("lower")
    } else { symbol_short!("yes") }
}
  

hint() is our example, not part of the template. It reuses the template's own Self::number(env), the same helper guess() calls.

The loop · 2 of 3

Save: npm run dev does the rest


[stellar] File changed: contracts/guess-the-number/src/lib.rs
[stellar] ℹ️  Changes detected. Rebuilding...
[stellar] ℹ️  Building for network "local" (from `stellar network use`)
[stellar] ✅ Local Stellar network is healthy and running.
[stellar]    Compiling guess-the-number v0.0.2 (contracts/guess-the-number)
[stellar] ✅ Build Complete
[stellar] 🌎 Sending transaction...
[stellar] ✅ Transaction submitted successfully!
[stellar] ✅ Client "guess-the-number" updated successfully
[stellar] ✅ Successfully generated client for: guess-the-number
[stellar] ✅ Contract "nft-enumerable" is up to date
[stellar] ℹ️  Watching for changes. Press Ctrl+C to stop.
  

One save: the Wasm rebuilds, the changed contract is updated on the local network, and its TypeScript client is regenerated with hint in it. Contracts you didn't touch stay as they are.

The loop · 3 of 3

Use it: typed, in your frontend

In GuessTheNumber.tsx, guessTheNumber. now autocompletes hint:


import { guessTheNumber } from "@stellar-scaffold/app-lib/clients"

const { result } = await guessTheNumber.hint({ a_number: 5n })
console.log(result)   // "higher" | "lower" | "yes"
  

A read-only call: no wallet signature needed. Same import in the Svelte template.

Add a contract

stellar scaffold generate writes the crate but does not add it to scaffold.yml; do that yourself.


stellar scaffold generate contract --ls   # list examples: stellar/…, oz/…
stellar scaffold generate contract --from oz/nft-royalties   # or --from-wizard
  

contracts:
  my-nft:                   # client name; exported as myNft
    type: workspace         # built from contracts/, deployed locally
    source: nft-royalties-example   # crate name in its Cargo.toml
    args:                   # constructor args, by parameter name
      uri: https://example.com/nft/
      name: My NFT
      symbol: MNFT
      admin: ${account.me}
      manager: ${account.me}
    networks:               # where your app uses this contract
      local:
  

Go to testnet and ship

By default Scaffold deploys only to your local network. To ship to testnet, publish and deploy with Stellar Registry, then point scaffold.yml at the deployed contract.


contracts:
  my-nft:
    networks:
      local:              # built from contracts/
      testnet:
        type: contract    # already deployed: fetch it
        source: C...      # ID from registry
    

stellar scaffold build --compile-only
stellar registry publish ...  # next talk
stellar registry deploy ...
stellar scaffold build --network testnet
npm run build --workspace app # → app/dist/
    

Create a .env with STELLAR_NETWORK=testnet. Contracts can differ between networks: make sure the app still typechecks.

Key takeaways

What to take away

  1. 1

    Start fast.init and npm run dev give you contracts, a frontend and a local network.

  2. 2

    Save is the loop.Every save rebuilds, redeploys locally and regenerates typed clients.

  3. 3

    Ship via Registry.By default Scaffold deploys locally; ship to testnet with the registry, then point scaffold.yml at it.

Hackathon survival kit

  • stellar scaffold doctor: anything broken, with a fix
  • stellar scaffold clean: changed constructor args? Start fresh
  • Works locally, breaks on testnet? Rebuild with --network testnet and fix type errors
  • Find contracts to depend on at rgstry.xyz
  • Teach your agent: Scaffold skill at skills.stellar.org

Your contract works locally.
How do you get it onto testnet under a name,
and use other people's without copying addresses?


stellar registry publish …   # under unverified/
  

The big idea

Give your contract a name, not an address. Ship it to testnet, and let anyone use it by that name.

Three things to remember

Wasm = published code

Reusable contract code under a name and a semantic version. Only its author can publish new versions.

unverified/guess-the-number@1.0.0

Contract = deployed instance

A live contract registered by name. The name resolves to its C… address.

No version: it's one address.

unverified/ = open to all

Anyone can publish here, no approval. Bare names on the main registry need a manager's OK.

Use this!

Ship to testnet by name


stellar keys fund me --network testnet
stellar keys use me

stellar registry publish \
  --wasm target/stellar/testnet/guess_the_number.wasm \
  --wasm-name unverified/guess-the-number --binver 1.0.0

stellar registry deploy \
  --contract-name unverified/guess-the-number \
  --wasm-name unverified/guess-the-number --version 1.0.0 \
  -- --admin me                        # constructor args after --

stellar registry fetch-contract-id unverified/guess-the-number   # C… for scaffold.yml
  

The Wasm is what stellar scaffold build --compile-only wrote with STELLAR_NETWORK=testnet. Add --dry-run to preview any write.

How names are organised

One root registry, many sub-registries


registry          root: bare names, manager-approved
├── unverified/   open to anyone: use this!
├── oz/           OpenZeppelin Wasms, ready to deploy
├── circle/       circle/usdc
├── soroswap/     soroswap/router, soroswap/factory
├── blend/        blend/pool-factory-v2, …
└── defindex/     defindex/xlm-vault, …
    

A sub-registry is its own registry contract with its own rules, registered by name in the root. Names read <sub-registry>/<name>.

Sub-registries group related contracts under one prefix, like soroswap/router and soroswap/factory.

Browse them all at rgstry.xyz under Contracts.

Start from OpenZeppelin

Deploy OpenZeppelin contracts straight from oz/


stellar registry deploy \
  --contract-name unverified/my-token \
  --wasm-name oz/ft-standard \
  -- --name "My Token" --symbol MTK --decimals 7 --owner me --initial_supply 1000000
  

30+ pre-built combinations of OpenZeppelin Stellar Contracts, on testnet and mainnet: ft-standard, ft-capped, ft-pausable, ft-allowlist, ft-votes, ft-vault, nft-standard, nft-royalties, nft-enumerable, …

Your name, their Wasm: deploy into unverified/ from any sub-registry. Want to edit the source instead? stellar scaffold generate contract --from oz/…

Point scaffold.yml at it

Today works now

contracts:
  guess-the-number:
    networks:
      local:              # built from contracts/
      testnet:
        type: contract    # already deployed: fetch it
        source: C...      # from fetch-contract-id
      
By name coming soon

contracts:
  guess-the-number:
    networks:
      local:              # built from contracts/
      testnet:
        type: registry    # resolve the name
        source: unverified/guess-the-number
      

Tracked in stellar-scaffold/cli#617.

Then stellar scaffold build --network testnet regenerates the typed guessTheNumber client against testnet. Same for anyone else's contract.

Case study

guess-the-number, local to testnet

  • 01. Context: it works locally with Scaffold; putting it on testnet under a name is the registry's job.
  • 02. Decision: publish the Wasm and deploy it as unverified/guess-the-number.
  • 03. Result: testnet has a named contract, scaffold.yml points at it, and the app gets a typed client.
Named contracts on testnet.rgstry.xyz

Call any named contract from Rust

You already have: guess-the-number pays out with stellar_registry::import_asset!("xlm").


let game = stellar_registry::import_contract!(
    env, "unverified/guess-the-number");   // typed client, no address in source

let won = game.guess(&5, &player);
  

STELLAR_NETWORK=testnet stellar scaffold build   # names resolve at build time
  

Defaults to local, and switching networks rebuilds automatically. Assets like xlm resolve at runtime, so one Wasm works on any network. Contract names resolve at build time: after a redeploy, cargo clean and rebuild.

Key takeaways

What to take away

  1. 1

    Name it.Publish under unverified/. Anyone can, with no approval.

  2. 2

    Ship it.Publish and deploy with the registry: that's how you reach testnet.

  3. 3

    Use it.Point scaffold.yml or import_contract! at a name, never a copied address.

Registry survival kit

  • Auth failure on publish? You used a bare name. Add unverified/
  • Error(Contract, #5): that contract name is taken. #8: publish a higher --binver
  • Macro can't find a contract? STELLAR_NETWORK wasn't set at build time
  • deploy … -- --help prints the constructor's arguments
  • Teach your agent: Registry skill at skills.stellar.org

New to Stellar?

Stellar has two kinds of accounts

G-address · starts with G
GBO4H3WFMTNWZU5SJJSXXOW2ML26TEIQOY66MF2SYANVWAI4JY4YEIJP
C-address · starts with C
CCH2FDKHAMKNT33JCF6NOOUDKZNFIKVOM2P5PMDI7D456LLI6VZSO732
What it isAn account with keysA contract
Who can signOne key, or up to 20 with weights and thresholdsWhatever its code allows: passkeys, a G-address, an app’s session key
What it can limitHow many signatures an action needsWhich contracts, how much, until when
Typical walletExtension and seed phraseNido: nothing to install

Nido gives each of your users a C-address.

The old way vs with Nido

Getting in

Freighter screen: illustrative mock from the template walkthrough

The old way: a wallet extension
Freighter asking the user to write down a 12-word recovery phrase
  • Install an extension
  • Create a password
  • Write down 12 words
  • Switch the network
  • Fund the account
With Nido: one passkey
Nido's create-with-a-passkey screen
  • Face ID or a fingerprint
  • Nothing to write down
  • Nothing to install
  • Or any key type, e.g. post-quantum
  • Recovery: add a trusted friend to get back in

Never locked out

Recovery

Nido's recovery setup: what to protect against, who can help you back in, how many must approve, and timing

Off by default. Turn it on and choose:

Protect againstA lost passkey, or a stolen one
Who helpsTrusted friends, your recovery kit, or both
How manyFriends who must approve
TimingStandard: wait 7 days, then 14 days to finish

The old way vs with Nido

Approve once, then no redirects

Freighter screen: illustrative mock from the template walkthrough

The old way: every signature goes to the wallet
Freighter asking the user to confirm a guess transaction

1

trip to the wallet per guess, to read and confirm

With Nido: approve the app once

0

redirects per guess after that: the app’s session key signs

Approve in Nido · 1 passkey tapGuessGuessGuess …

The old way vs with Nido

Limits

The old way: no limit

The wallet signs whatever the user confirms. The only protection is reading every popup.

With Nido: a scoped key with a cap

The user approves a key for your app, and sets how much it can spend.

Spending capAn amount per day, week or month
ExpiryThe key stops working after it
Enforced byThe account, on chain

A call over the cap is refused before anything is sent.

Guess the Number refusing a guess at the 2 XLM a day cap: it cost nothing

Summary

What Nido gives you

Built on OpenZeppelin’s Smart Account and Relayer foundations.

For your users
  1. 1

    No extension, no seed phrase

  2. 2

    Approve an app once

  3. 3

    Every approval has a cap and an expiry

For your app
  1. 1

    Users signed in within a minute

  2. 2

    Calls without having to use the main wallet

  3. 3

    Limits enforced by the account, not your code

Part 2

How to Nido

The same Scaffold React app and Guess the Number sample. Three diffs.


stellar scaffold init my-app \
    --template nidohq/scaffold-template
npm run tools:scaffold && npm ci && npm run doctor
npm run clients:testnet && npm run dev
  

1 · Imports    2 · Ask for access    3 · Call for the user

Diff 1 of 3 · vs the React template

Imports

app/src/components/GuessTheNumber.tsx

+import {
+  describe, isOverCap, submitCall,
+  xlmTokenId, type Grant,
+} from "@stellar-scaffold/app-lib"
 import { guessTheNumber } from
   "@stellar-scaffold/app-lib/clients"
 import { useState } from "react"
 import { useWallet } from "../hooks/useWallet"
+import { SessionGate } from "./SessionGate"
      

submitCall
Sends a call for any wallet

Grant, SessionGate
What the app asks the user for

xlmTokenId
The token to put a cap on

describe, isOverCap
Refusals as sentences

Nido’s wallet module is already in app-lib/wallet.ts. You don’t touch it.

Diff 2 of 3 · vs the React template

Connect, then ask for access

Connect · unchanged
The Connect Wallet picker with Nido listed first

Same connectWallet(). Nido is first in the picker.

GuessTheNumber.tsx

+const grants: Grant[] = [
+  { target: guessTheNumber.options.contractId,
+    functions: ["guess"],
+    duration: "24h", label: "guess" },
+  { target: xlmTokenId,
+    functions: ["transfer"], duration: "24h",
+    limit: { amount: "2", period: "day" },
+    label: "guess-xlm" },
+]
 …
+<SessionGate grants={grants} explain={…}>
   <form action={submitGuess}>…</form>
+</SessionGate>
      

One grant per contract the call touches. The user approves them once, in Nido.

Diff 2 of 3 · what the user sees

The grants, in Nido

Nido's approval page for a scoped session key with two rules
In your codeOn Nido’s page
target: guessTheNumberRule 1 · guess: only guess() on the game
target: xlmTokenIdRule 2 · guess-xlm: only transfer(), and only to the game
limit: 2 / dayThe cap. The user can change it before approving.
duration: "24h"Expires at a ledger about 24 hours away
your app’s keyThe session key: the app holds it, and it signs every guess

One passkey confirmation, and the SessionGate opens.

Diff 3 of 3 · vs the React template

Call for the user

GuessTheNumber.tsx · submitGuess

-const tx = await guessTheNumber.guess(
-  { a_number: BigInt(guess), guesser: address },
-  { publicKey: address },
-)
-const { result } = await tx.signAndSend({ signTransaction })
+const { result, hash } = await submitCall(
+  address, (publicKey) =>
+    guessTheNumber.guess(
+      { a_number: BigInt(guess), guesser: address },
+      { publicKey })
+)
      

No new round-trip through Nido.

The app’s session key signs each call itself, and the account checks it against the grants.

A classic G-address wallet still works: submitCall signs with it as before.

1Password's Sign in with a passkey prompt for Nidohq

After approval · what your account enforces

Your policies, in plain words

Home: every policy change is in Recent activity
Nido's home screen; Recent activity shows Updated policies, 2 rules added
Account policy: the document, verified
Nido's Account policy page: 3 rules installed, policy document verified against the on-chain hash, two signers and one admin key

Same policy, two views

What the user sees, what the account stores

Amounts are in stroops, 10,000,000 to the XLM. The account stores this document’s hash on chain.

In Nido: the rule cards
Nido's rule cards for guess and guess-xlm: session can call transfer on the XLM contract, up to 2 XLM per day
The raw Perch policy document

{ "network": "Test SDF Network ; September 2015",
  "rules": [
    { "name": "admin", …, "scope": { "type": "self-admin" } },
    { "functions": ["guess"], "name": "guess", … },
    { "args": [{ "index": 1, "pred": {
        "address": "CBY4S4…ECYGUW", "type": "address-eq" } }],
      "cap": { "limit": "20000000", "period-ledgers": 17280 },
      "functions": ["transfer"],
      "name": "guess-xlm",
      "not-after-ledger": 5382692,
      "principals": { "signers": ["session"], "type": "all" },
      "scope": { "address": "CDLZFC…HGCYSC", "type": "contract" } }
  ],
  "signers": [{ "id": "admin", … }, { "id": "session", … }],
  "version": 1 }
      

Keep building

Nido resources

The template

github.com/nidohq/scaffold-template

The guide

NIDO.md, in the template

The sample app

github.com/nidohq/guess-the-number

Try it live

nidohq.github.io/guess-the-number

Nido

github.com/nidohq/nido

Under the hood

github.com/stellar-registry/perch
OpenZeppelin stellar-contracts

Keep building

Your next step

Publish your hackathon contract under unverified/, and give your users passkey accounts with Nido.

  • stellarscaffold.org: Scaffold docs, tutorial and Registry guide
  • rgstry.xyz: browse contracts to depend on
  • nido.fyi: passkey smart accounts for your app
  • skills.stellar.org: Scaffold and Registry skills for your agent
QR code for these slides

These slides