Guidelines for AI agents (Claude, Codex, Cursor, Copilot, etc.) working in this repository.
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.
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 exampleswho_am_i— Internet Identity integration; reference for II-authenticated examples
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
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.
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_iusessrc/backend/andsrc/frontend/for historical reasons. New examples use the flatbackend//frontend/layout above.
- dfx artifacts (
dfx.json,.dfx/) or ICP Ninja artifacts (BUILD.md) frontend/src/bindings/— auto-generated by the bindgen Vite plugin, must be gitignoreddist/output — built byicp 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.
.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.
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 frontendThe 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.
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.didfile and embeds it as WASM metadata. - Without
candid::candid-extractorextracts the interface from the compiled WASM. For backend-only examples, omitcandid:and do not commitbackend.did.
- The standard pair is
backendandfrontend. Never use names like<example>_backend. - Multi-canister examples use short role names instead (e.g.
caller/callee,publisher/subscriber,token_a/token_b).
Examples define at most two environments, always named exactly:
local— the managed local network, used for development and CI.ic— mainnet. The nameicis 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")— neverenv!()(compile-time) orstd::env::var()(no OS environment in WASM)
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 ownconnect-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(a200rewrite, 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.).
[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.
- Top-level actor: name it after its logical role — e.g.
actor TodoList,actor CanisterFactory, notactor Backend. - Supporting module files: PascalCase matching the type they export — e.g.
Counter.moexportingactor class Counter. - Entry point file: always lowercase
backend/main.mo(or<role>/main.moin multi-canister examples) regardless of the actor name — the composition root is lowercasemain.mo(like Rust'slib.rs), matchingicp new, mops, and the Motoko skill. PascalCase is only for the exported module files above.
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.
Use the mo:ic mops package instead of ic:aaaaa-aa or inline actor("aaaaa-aa") definitions:
import { ic } "mo:ic";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 canisterlib.rs. Without it,candid-extractorcannot find theget_candid_pointerexport and the build fails.- Management canister: use the
ic-cdk-management-canistercrate (e.g.use ic_cdk_management_canister::raw_rand;) —ic_cdk::api::management_canisterwas removed in ic-cdk 0.17+.
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.yamlname (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
--queryforpublic query funcandpublic composite query funcmethods. - Use
grep -qto 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 documenticp canister top-up --amount <amount> backendin 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 oftest.shviaicp canister settings update backend --<flag> <value> -f.
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.ymlis 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 withlanguage: all. - Keep the template's
concurrencyblock and onepull_requestpath 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
scheduletrigger (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 copyingworkflow-template.yml— keep thescheduleblock.
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)
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 interfacesection — no frontend consumes the.didfile. - 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 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>
[](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 classsub-canisters,create_canister+install_code). - At most 2 canisters in the
icenvironment. Count theicenvironment's canister list, not the local one (an example may deploy extra canisters locally — mock ledgers, pre-built infra — while restrictingictobackend+frontend). If a helper canister is only needed locally (e.g. a test subject, a mock ledger), restrict it tolocalvia theenvironmentsblock soic/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-storageneeds anauthorizecall 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'slist_neuronsreturns 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.