Skip to content

Latest commit

 

History

History
449 lines (323 loc) · 23.7 KB

File metadata and controls

449 lines (323 loc) · 23.7 KB

Agent Instructions

Guidelines for AI agents (Claude, Codex, Cursor, Copilot, etc.) working in this repository.

ICP Skills

ICP skills are tested, frequently-updated instruction files maintained by DFINITY (see https://skills.internetcomputer.org). Consult the relevant skills before making changes — do not rely on training-data knowledge of ICP tooling. For background on how agents consume ICP skills, see the AI coding agents guide.

How this repo is set up: it uses autosync, an ICP skill that installs the sync mechanism. A committed SessionStart hook (.claude/settings.json.claude/sync-ic-skills.sh) mirrors the latest skills into .claude/skills/ at the start of every session, so they stay current with nothing to commit when a skill changes (the skills directory itself is gitignored). The first time it runs, Claude Code asks you to trust the hook. This targets Claude Code only — other agents (Cursor, Copilot, Codex, …) don't run the hook and instead use the on-demand fallback noted below.

ICP skills auto-update each session via a SessionStart hook (.claude/sync-ic-skills.sh) and live in your agent skills directory — you don't need to run anything to refresh them. Skills are authoritative — prefer them over general knowledge for all ICP work. If they are not present (hook hasn't run, or jq is missing), fetch them on demand per the "Use skills now" steps above.

On-demand fallback (any agent, no hook needed): fetch the index once per session from https://skills.internetcomputer.org/.well-known/skills/index.json, then fetch the matching skill's SKILL.md before writing ICP code for a task. This is how non–Claude Code agents (Cursor, Copilot, Codex, …) use the skills, since the SessionStart hook is Claude Code–only.


Repository overview

This repo contains canonical ICP examples, each available in both Motoko and Rust. Every example lives under two sibling directories:

motoko/<example_name>/
rust/<example_name>/

Both implement the same Candid interface so readers can compare language implementations side by side. hosting/ contains frontend-only examples.

Reference examples to study first:

  • hello_world — canonical full-stack example (backend + Vite frontend); use as the structural template for new examples
  • who_am_i — Internet Identity integration; reference for II-authenticated examples

Adding a new example

Examples have an educational focus — general-purpose or arbitrary examples are not accepted. Before adding one, confirm that:

  • It demonstrates a distinct ICP capability or pattern not already covered by an existing example.
  • It has a clear home in the developer documentation (an existing or planned guide, tutorial, or reference page).
  • The DFINITY DX team has agreed to maintain it long-term.

Ship both language variants (Motoko and Rust) with the same Candid interface. The per-language duplication — including shared frontend code — is deliberate: it keeps each variant self-contained for readers of that language.

Toolchain: always use icp-cli for all ICP operations — never dfx, and never add dfx artifacts (dfx.json, .dfx/, dfx-generated .env) to an example. CLI docs: https://cli.internetcomputer.org · ICP developer docs: https://docs.internetcomputer.org

Versions

This document deliberately does not pin library, recipe, or toolchain versions — any vX.Y.Z in a snippet is a placeholder. When creating or updating an example, take current versions (recipe versions in icp.yaml, moc/core in mops.toml, crate versions in Cargo.toml, npm packages) from the most recently updated examples in this repo (check git log), preferring the newest version already in use. When in doubt, consult the installed ICP skills.

When asked to bump a specific version — whether a library dependency or an icp.yaml recipe (@dfinity/motoko, @dfinity/rust, @dfinity/static-site, ...) — clarify the scope first: all examples, or only the one named? The goal is that every example stays consistent and up to date with the same versions, so prefer repo-wide bumps unless told otherwise.


Canonical example structure

Follow the hello_world layout:

<language>/<example_name>/
├── icp.yaml                  # canister definitions (icp-cli project file)
├── test.sh                   # executable bash test script
├── README.md
├── package.json              # npm workspaces root pointing to frontend/ (only if frontend exists)
├── mops.toml                 # Motoko only
├── Cargo.toml                # Rust only (workspace)
├── rust-toolchain.toml       # Rust only
├── backend/
│   ├── main.mo or lib.rs     # canister entry point
│   └── backend.did           # Candid interface (only if a frontend consumes it)
└── frontend/
    ├── index.html
    ├── package.json
    ├── vite.config.js        # includes @icp-sdk/bindgen vite plugin
    ├── src/
    │   ├── actor.js          # icp-sdk actor wiring
    │   ├── App.jsx
    │   └── main.jsx
    └── dist/                 # build output (gitignored, rebuilt by icp deploy)

who_am_i uses src/backend/ and src/frontend/ for historical reasons. New examples use the flat backend/ / frontend/ layout above.

What NOT to commit

  • dfx artifacts (dfx.json, .dfx/) or ICP Ninja artifacts (BUILD.md)
  • frontend/src/bindings/ — auto-generated by the bindgen Vite plugin, must be gitignored
  • dist/ output — built by icp deploy; never commit pre-built assets
  • Per-example .devcontainer/ — only the repo-root devcontainer exists (see below)
  • Lock files (Cargo.lock, mops.lock, package-lock.json) — gitignored on purpose. Examples resolve the latest semver-compatible dependencies on clone so a developer trying an example gets current versions, matching the repo's stay-current philosophy. The weekly CI run (below) catches any upstream breakage this exposes.

What NOT to gitignore

  • .icp/data/always commit this. It holds canister ID mappings (e.g. local.ids.json) that map canister names to on-chain principals.

The root .gitignore already has **/.icp/cache/, which correctly ignores only the ephemeral build cache. Do not add .icp/ to per-example .gitignore files — it would incorrectly hide data/ as well.


icp.yaml

Motoko

networks:                          # omit if no Internet Identity needed
  - name: local
    mode: managed
    ii: true

canisters:
  - name: backend
    recipe:
      type: "@dfinity/motoko@vX.Y.Z"

  - name: frontend
    recipe:
      type: "@dfinity/static-site@vX.Y.Z"
      configuration:
        dir: frontend/dist
        build:
          - npm install --prefix frontend
          - npm run build --prefix frontend

The Motoko recipe reads its configuration (main, candid, args) from the [canisters.<name>] section of mops.toml, so the Motoko canister needs no configuration: block here.

Rust

canisters:
  - name: backend
    recipe:
      type: "@dfinity/rust@vX.Y.Z"
      configuration:
        package: backend
        candid: backend/backend.did   # omit for backend-only examples (no frontend)

  - name: frontend
    recipe:
      type: "@dfinity/static-site@vX.Y.Z"
      configuration:
        dir: frontend/dist
        build:
          - npm install --prefix frontend
          - npm run build --prefix frontend
  • With candid:: the recipe reads the committed .did file and embeds it as WASM metadata.
  • Without candid:: candid-extractor extracts the interface from the compiled WASM. For backend-only examples, omit candid: and do not commit backend.did.

Canister naming

  • The standard pair is backend and frontend. Never use names like <example>_backend.
  • Multi-canister examples use short role names instead (e.g. caller/callee, publisher/subscriber, token_a/token_b).

Environments

Examples define at most two environments, always named exactly:

  • local — the managed local network, used for development and CI.
  • ic — mainnet. The name ic is a contract with ICP Ninja: projects deploy successfully from Ninja only if the mainnet environment carries this exact name.

Never add a staging (or any other) environment by default — developers can extend their own copies, but examples ship with only these two. Self-contained examples that behave identically everywhere need no environments block at all.

Use the block when local and mainnet deployments differ. The common pattern: a companion canister (mock or local instance) is deployed locally, while on mainnet an already-running well-known canister is used instead — the ic environment restricts the canister list and injects the principal via an environment variable:

canisters:
  - name: backend
    recipe:
      type: "@dfinity/rust@vX.Y.Z"

  - name: xrc              # mock, deployed locally only
    build:
      steps:
        - type: pre-built
          url: https://github.com/dfinity/exchange-rate-canister/releases/download/<tag>/xrc_mock.wasm.gz

environments:
  # Local: deploys backend and the mock; icp-cli auto-injects
  # PUBLIC_CANISTER_ID:xrc into the backend after deploying xrc.
  - name: local
    network: local

  # Mainnet: deploys only the backend; the production XRC canister already exists.
  - name: ic
    network: ic
    canisters: [backend]
    settings:
      backend:
        environment_variables:
          "PUBLIC_CANISTER_ID:xrc": "uf6dk-hyaaa-aaaaq-qaaaq-cai"

Per-environment init_args follow the same shape (see basic_bitcoin: regtest locally, testnet on ic).

icp-cli applies environment_variables as canister settings at deploy time. Read them at runtime:

  • Motoko: Runtime.envVar<system>("PUBLIC_CANISTER_ID:xrc")
  • Rust: ic_cdk::api::env_var_value("PUBLIC_CANISTER_ID:xrc") — never env!() (compile-time) or std::env::var() (no OS environment in WASM)

Frontend headers and routing

Frontends use the @dfinity/static-site recipe (the certified-assets canister). Configure it with two files at the root of the build's publish dir — put them in frontend/public/ (Vite's public/ is copied to dist/), never in a subfolder that only gets bundled:

  • _headers — security headers and caching, Netlify-style. Certified-assets ships no default headers, so declare them explicitly. Baseline (tighten the CSP per app; keep each example's own connect-src/script-src):
    /*
      X-Frame-Options: DENY
      X-Content-Type-Options: nosniff
      Referrer-Policy: strict-origin-when-cross-origin
      Content-Security-Policy: default-src 'self'; ...
    /assets/*
      Cache-Control: public, max-age=31536000, immutable
    /*.html
      Cache-Control: public, max-age=0, must-revalidate
    
  • _redirects — SPA fallback so deep-link reloads don't 404: /* /index.html 200 (a 200 rewrite, not a redirect).

Do not use .ic-assets.json5 — that is the legacy asset-canister config; certified-assets ignores it (it gets served as a dead asset). No HSTS in the baseline: _headers apply in local dev too, and a canister-served Strict-Transport-Security would force HTTPS on http://…localhost. Consult the static-site skill before changing these (reserved-header list, _headers matches the asset key not the URL, etc.).


Motoko conventions

mops.toml

[toolchain]
moc = "X.Y.Z"

[dependencies]
core = "X.Y.Z"

[moc]
# M0236: use context dot notation
# M0237: redundant explicit implicit arguments
# M0223: redundant type instantiation
args = ["--default-persistent-actors", "-W=M0236,M0237,M0223"]

[canisters.backend]
main = "backend/main.mo"
candid = "backend/backend.did"   # omit for backend-only examples (no frontend)

The [canisters.<name>] name must match the canister name in icp.yaml.

After writing or editing Motoko source files, always run — both must pass before committing:

mops check          # type-check all canister entry points
mops check --fix    # auto-fix style warnings (M0236, M0237, M0223)

--default-persistent-actors makes the main actor persistent by default, so the persistent keyword is omitted on the top-level actor declaration. persistent actor class declarations holding mutable state must still carry the keyword explicitly — the flag does not propagate into actor class sub-WASMs.

Naming

  • Top-level actor: name it after its logical role — e.g. actor TodoList, actor CanisterFactory, not actor Backend.
  • Supporting module files: PascalCase matching the type they export — e.g. Counter.mo exporting actor class Counter.
  • Entry point file: always lowercase backend/main.mo (or <role>/main.mo in multi-canister examples) regardless of the actor name — the composition root is lowercase main.mo (like Rust's lib.rs), matching icp new, mops, and the Motoko skill. PascalCase is only for the exported module files above.

Inter-canister calls

For calls between canisters in the same project (and to well-known external canisters), use a canister: import wired via --actor-env-alias — do not hand-write actor types or use actor(...) casts:

import Callee "canister:callee";
[canisters.caller]
main = "caller/main.mo"
# Per-canister args REPLACE the global [moc].args — repeat the shared flags.
args = [
    "--default-persistent-actors",
    "-W=M0236,M0237,M0223",
    "--actor-env-alias", "callee", "PUBLIC_CANISTER_ID:callee", "callee/callee.did",
]

icp deploy injects the PUBLIC_CANISTER_ID:<name> environment variable; the import is typed against the committed .did file of the target canister. See parallel_calls and pub-sub for working setups.

Management canister

Use the mo:ic mops package instead of ic:aaaaa-aa or inline actor("aaaaa-aa") definitions:

import { ic } "mo:ic";

Rust conventions

Root workspace Cargo.toml:

[workspace]
members = ["backend"]
resolver = "2"

backend/Cargo.toml:

[package]
name = "backend"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]
  • ic_cdk::export_candid!() is required at the end of every canister lib.rs. Without it, candid-extractor cannot find the get_candid_pointer export and the build fails.
  • Management canister: use the ic-cdk-management-canister crate (e.g. use ic_cdk_management_canister::raw_rand;) — ic_cdk::api::management_canister was removed in ic-cdk 0.17+.

test.sh

Every example has an executable test.sh that exercises the deployed canister via icp canister call. Write a numbered test for every public function. For mutating operations, also assert state: call the mutating function, then a read function, and check the stored value changed.

#!/usr/bin/env bash
set -e

echo "=== Test 1: <description of what is tested> ==="
result=$(icp canister call backend <method> '<args>')
echo "$result"
echo "$result" | grep -q '<expected>' && echo "PASS" || (echo "FAIL" && exit 1)
  • Call canisters by their icp.yaml name (backend).
  • Always pass explicit Candid args, including '()' for zero-argument calls — omitting args triggers an interactive prompt that blocks CI. This applies especially when calling dynamically created canisters by principal.
  • Always pass --query for public query func and public composite query func methods.
  • Use grep -q to assert on output and number each test (=== Test N: ... ===) so CI logs are easy to scan.
  • For examples that create child canisters, capture the returned principal and call the child directly by ID. Deploy with icp deploy --cycles <amount> (in CI and README) so the parent can fund children, and document icp canister top-up --amount <amount> backend in the README.
  • Balance checks: use delta-based assertions (record before, act, assert the delta) rather than absolute values — keeps tests idempotent across re-runs.
  • Async/time-dependent behavior (timers, heartbeats): poll in a loop with a timeout instead of a fixed sleep.
  • Non-default canister settings (e.g. wasm_memory_limit): apply at the top of test.sh via icp canister settings update backend --<flag> <value> -f.

CI workflow

Copy .github/workflow-template.yml to .github/workflows/<example_name>.yml and fill in the placeholders. One workflow file covers all language variants of an example; each job calls the reusable _run-example.yml workflow:

jobs:
  motoko:
    uses: ./.github/workflows/_run-example.yml
    with:
      language: motoko
      working-directory: motoko/<example_name>
      run: |
        icp network start -d
        icp deploy
        bash test.sh
  • _run-example.yml is the single source of truth for the dev-env container image version — never pin container images in per-example workflows.
  • One job per language variant the example ships; frontend-only hosting/ examples use a single job with language: all.
  • Keep the template's concurrency block and one pull_request path entry per shipped language directory.
  • Examples with PocketIC integration tests set install-pocketic: true.
  • Linux only; the toolchain comes from the container image — no provision scripts.
  • Weekly run: every example workflow also carries a schedule trigger (cron: "30 6 * * 1") so all examples build/deploy/test weekly, not only when changed. Since lock files are gitignored (deps resolve fresh), this is the safety net that catches upstream dependency breakage. New examples get it automatically by copying workflow-template.yml — keep the schedule block.

Dev-env container images

Images live at ghcr.io/dfinity/icp-dev-env-{motoko,rust,all} (source: https://github.com/dfinity/icp-dev-env), pinned with a v-prefixed tag in exactly two places, guarded by dev-env-version-check.yml:

  • .github/workflows/_run-example.yml (CI)
  • .devcontainer/devcontainer.json (local VS Code; single root devcontainer for all examples — do not add per-example configs)

README structure

Each example's README follows this structure:

# <Example Title>

<ICP Ninja badge + callout — eligible examples only; see "ICP Ninja badge" below>

<2-3 sentences describing what the example demonstrates>

## Build and deploy from the command line

### Prerequisites

- Node.js
- icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm`
- ic-mops: `npm install -g ic-mops`

### Install
<git clone + cd>

### Deploy and test
<icp network start -d && icp deploy && bash test.sh && icp network stop>
If the example has a frontend: `npm run dev` (Vite dev server with hot reload)

## Updating the Candid interface
Motoko: `mops generate candid backend`
Rust: `icp build backend && candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > backend/backend.did`

## Security considerations and best practices
<standard disclaimer linking to https://docs.internetcomputer.org/guides/security/overview>
  • Say "canister", not "smart contract" — in READMEs and code comments alike.
  • No back-references: do not add a "View this sample's code on GitHub" link or a cross-language "also available in <other language>" link. Readers arrive from GitHub and pick a language folder upfront, so both are redundant. (Language-description links — e.g. to the Motoko or Rust docs — are fine; those explain the language, they don't jump to the sibling example.)
  • Backend-only examples: omit the ## Updating the Candid interface section — no frontend consumes the .did file.
  • Child-canister examples: add a note that an out-of-cycles error is fixed with icp canister top-up --amount <amount> backend.
  • Links: only add links you have verified resolve on the current docs site; prefer top-level pages over deep anchors when unsure.
  • Docs over product pages: when the context is learning or integrating a feature, link the developer docs (e.g. https://docs.internetcomputer.org/guides/authentication/internet-identity for Internet Identity); link the product itself (e.g. https://id.ai) only when referring to the live instance an end user interacts with.

ICP Ninja badge

ICP Ninja is a browser-based IDE that deploys a project to the mainnet for free. An example carries an "Open in ICP Ninja" badge only once it is both eligible (rules below) and known to ICP Ninja; the badge is the first thing under the H1, followed by a short callout:

# <Example Title>

[![Open in ICP Ninja](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/dfinity/examples/tree/master/<language>/<example_name>)

> 🥷 **Try it live — no local setup.** [ICP Ninja](https://icp.ninja) is a web-based IDE that builds and deploys this project to the mainnet for free, right in your browser. Click the badge above, or hit **Deploy** if you're already in Ninja. To build and run it locally instead, follow the steps below.
  • Badge asset: always https://icp.ninja/assets/open.svg (the official badge). Import URL: https://icp.ninja/i?g=<full GitHub tree URL on master>.
  • The callout is dual-context. ICP Ninja renders the README as its default preview, so the block is read both on GitHub (where the badge is the call-to-action) and inside Ninja (where "hit Deploy" is the actionable step). Keep both cues.

Eligibility — only add the badge when all of these hold:

  • No canister factory. The example must not create canisters at runtime (e.g. actor class sub-canisters, create_canister + install_code).
  • At most 2 canisters in the ic environment. Count the ic environment's canister list, not the local one (an example may deploy extra canisters locally — mock ledgers, pre-built infra — while restricting ic to backend+frontend). If a helper canister is only needed locally (e.g. a test subject, a mock ledger), restrict it to local via the environments block so ic/Ninja deploys only the real thing.
  • Deployable in Ninja's no-terminal environment. No custom build scripts beyond the standard recipe build; no controller-only setup step a Ninja user can't perform (e.g. photo-storage needs an authorize call from a controller, so it is excluded despite being a single canister).
  • Demonstrable, not just deployable. A user must be able to do something meaningful with a fresh deploy — the core method returns a real result (compute, an HTTPS/inter-canister call, a CRUD round-trip, canister logs, a derived address/key), or the README gives a clear path to make it meaningful using resources the user can obtain themselves (II login, faucet TESTICP/TESTICRC1 tokens, passing a canister principal). Exclude examples whose headline function returns nothing without state the user can't supply — e.g. candid_type_generation's list_neurons returns only neurons the caller controls, which a fresh canister has none of, so it always comes back empty.

For token examples, the ic environment should use the TESTICP/TESTICRC1 test ledgers (not the real ICP ledger) so users can exercise transfers with free faucet tokens — and the README should point at the faucet and the steps to use it.

Skip the badge for ineligible examples and for language-native/CLI examples that aren't a deployable canister project. Eligibility is necessary but not sufficient: an example that satisfies these rules but is not yet known to ICP Ninja should be proposed to the Ninja team first, and the badge added only once Ninja supports it.