-
Notifications
You must be signed in to change notification settings - Fork 2
Skill improvements based on real-world session analysis (Kairos + ClearDeck) #136
Description
Context
We analyzed session transcripts from two real-world projects built on ICP with AI coding assistants (Claude Code / Opus):
- Kairos — a Motoko time capsule app (Svelte → React frontend), migrated from Caffeine, deployed to mainnet. 3 sessions analyzed.
- ClearDeck — a Rust multi-chain poker app (Svelte 5 frontend), forked and extended with ckETH + DOGE support. 2 sessions analyzed.
Both projects installed and used ICP skills (via npx skills add and/or WebFetch from skills.internetcomputer.org). This issue documents concrete problems the developers hit that better skill content could have prevented or mitigated, along with specific recommendations.
Each recommendation should be reviewed individually — not all may be appropriate or correctly scoped. The goal is to capture what happened and propose improvements for discussion.
Findings by skill
1. evm-rpc — Missing evm_rpc_types Rust crate
What happened (ClearDeck): The assistant spent 10+ iterations manually defining Candid types for the EVM RPC canister in Rust. Each attempt had subtle differences (wrong field names, missing variants, serde(rename) mismatches) that caused IC0503 "Fail to decode argument" traps at runtime. The assistant tried evm-rpc-canister-types (version conflicts with ic-cdk 0.19) before eventually finding the evm_rpc_types crate (types-only, compatible).
Current skill content: The Rust section defines all types inline (~200 lines of structs/enums). The evm_rpc_types crate is not mentioned. The Cargo.toml only lists ic-cdk, candid, serde.
Recommendation: Add evm_rpc_types as the primary Rust approach. Replace or supplement the inline type definitions with:
[dependencies]
evm_rpc_types = "1"
ic-cdk = "0.19"
candid = "0.10"Add a pitfall:
Do NOT define EVM RPC types manually in Rust. Use the `evm_rpc_types` crate
which provides all Candid types for the EVM RPC canister. Manual definitions
drift from the canister's actual interface and cause IC0503 decode traps.
Impact: This was the single most time-consuming technical issue across all sessions — roughly 2+ hours of wasted iteration.
2. icp-cli / references/dfx-migration.md — Missing @dfinity/* → @icp-sdk/* package mapping
What happened (ClearDeck): The assistant consistently used @dfinity/agent import patterns ({ agent }) when the project had migrated to @icp-sdk/core. The createActor function from @icp-sdk/bindgen expects { agentOptions: { identity, host } }, but when passed the old { agent } pattern, it silently falls back to an anonymous identity — no error thrown, just unexplained access denied or empty responses. The developer called this "the most time-consuming single issue in the migration."
Current skill content: The icp-cli skill and dfx-migration reference mention @icp-sdk/core and @icp-sdk/bindgen but don't provide an explicit mapping from the old @dfinity/* packages, and don't warn about the createActor API difference.
Recommendation: Add an explicit migration table to references/dfx-migration.md:
| @dfinity/* | @icp-sdk/* |
|----------------------|-------------------------------|
| @dfinity/agent | @icp-sdk/core |
| @dfinity/auth-client | @icp-sdk/auth |
| @dfinity/candid | @icp-sdk/core |
| @dfinity/principal | @icp-sdk/core |
| @dfinity/identity | @icp-sdk/core/identity |
| @dfinity/ledger-icp | @icp-sdk/canisters/ledger/icp |
| dfx generate | @icp-sdk/bindgen |
And add a prominent warning:
## BREAKING: createActor signature changed
@dfinity/agent: createActor(canisterId, { agent })
@icp-sdk/bindgen: createActor(canisterId, { agentOptions: { identity, host } })
Passing { agent } to the new API silently creates an anonymous identity.
No error is thrown — calls simply fail with access denied or empty data.
Why this matters for AI specifically: AI models have extensive training data for @dfinity/* packages and almost none for @icp-sdk/*. Without an explicit mapping, they default to the old patterns.
3. internet-identity — Missing icp0.io / ic0.app domain equivalence
What happened (ClearDeck): A user reported getting a different principal after login. The assistant incorrectly diagnosed this as a domain-based principal derivation issue and added derivationOrigin and ii-alternative-origins configuration — which made authentication worse. The actual cause was likely a different passkey/device, not the domain. II automatically rewrites icp0.io to ic0.app during delegation, so both domains produce the same principal.
Current skill content: The skill covers delegation, identity providers, and the 2vxsx-fae anonymous principal, but doesn't mention the icp0.io/ic0.app equivalence.
Recommendation: Add to the pitfalls section:
- icp0.io and ic0.app produce the SAME principal. Internet Identity
automatically rewrites icp0.io to ic0.app during delegation. Do NOT
add derivationOrigin or ii-alternative-origins to handle this — it
will break authentication. If a user reports a different principal,
check whether they used a different passkey/device, not the domain.
4. asset-canister — Missing version compatibility warning and correct URL format
What happened (Kairos): During the dfx → icp-cli migration, the assistant wrote a frontend/canister.yaml with a pre-built step pointing to:
- Wrong URL path:
github.com/dfinity/sdk/raw/refs/tags/0.27.0/src/distributed/assetstorage.wasm.gz(source tree, not releases) - Wrong version: 0.27.0, while dfx 0.31.0 was installed and the on-chain canister was running the 0.31.0 wasm
This caused a "Cannot parse header" stable memory panic when attempting to upgrade. The assistant concluded this was a fundamental dfx↔icp-cli incompatibility, but it was actually a version/URL mismatch. The assistant never tried the @dfinity/asset-canister recipe which would have resolved the version correctly.
Current skill content: The skill covers .ic-assets.json5 configuration, SPA routing, custom domains, and programmatic uploads. It doesn't mention wasm version compatibility or the distinction between source URLs and release URLs.
Recommendation: Add to the pitfalls section:
- Always use the recipe (@dfinity/asset-canister@v2.1.0) rather than
manually specifying pre-built URLs. The recipe handles version resolution.
- If you must use a pre-built step, use the GitHub releases URL:
https://github.com/dfinity/sdk/releases/download/<VERSION>/assetstorage.wasm.gz
NOT the source tree URL:
https://github.com/dfinity/sdk/raw/refs/tags/<VERSION>/src/distributed/assetstorage.wasm.gz
- When upgrading an existing asset canister, the new wasm version must be
compatible with the on-chain stable memory format. Downgrading or using a
mismatched version causes "Cannot parse header" panics. Check the deployed
version with: icp canister info <canister-id> -e ic
5. Proposed canister-calls skill (see #88) — Cross-cutting recommendations
Based on the session analysis, the generic canister-calls skill is the right place for issues that affect all canister interactions regardless of which specific canister is being called. These came up repeatedly:
5a. Candid ↔ JavaScript/TypeScript type mapping
What happened (ClearDeck): The ETH poker table showed "ICP" instead of "ETH" because the frontend code checked currency as a string (=== "ETH") instead of as a Candid variant object ("ETH" in value). BigInt values from nat fields broke JSON.stringify. These caused real production bugs.
Recommendation: Add a section to canister-calls:
## Candid Type Mapping in JavaScript/TypeScript
These apply to ALL canister calls from JS/TS, regardless of which canister:
- variant { ICP; BTC; ETH } → JS object: { ICP: null }, NOT string "ICP"
Check with: "ICP" in value or Object.keys(value)[0]
- nat / nat64 → BigInt — breaks JSON.stringify()
Use replacer: JSON.stringify(obj, (_, v) => typeof v === 'bigint' ? v.toString() : v)
- opt T → [value] (present) or [] (absent) — NOT null/undefined
- blob → Uint8Array, not Buffer or number[]
- record { owner; subaccount } → { owner: Principal, subaccount: [Uint8Array] | [] }
5b. Rust ic-cdk 0.19+ Call API
What happened (ClearDeck): The assistant repeatedly hallucinated old ic-cdk APIs — Call::new (doesn't exist), .change_payment128() (removed), ic_cdk::call() (deprecated). Each hallucination required a compile-error-fix cycle.
Recommendation: Add to the Rust calling section of canister-calls:
## Rust: ic-cdk 0.19+ Call API
ic_cdk::call() is deprecated. Call::new() does not exist. Use:
- Call::unbounded_wait(canister_id, method) — guaranteed response, blocks upgrades
- Call::bounded_wait(canister_id, method) — may timeout, allows upgrades
Attach cycles with .with_cycles(amount). Do NOT use .change_payment128() (removed).
Example:
let result: (String,) = Call::unbounded_wait(canister_id, "method")
.with_arg(&my_arg)
.await
.expect("call failed")
.candid_tuple()
.expect("decode failed");
5c. createActor API for @icp-sdk/bindgen
This could live in canister-calls (since it's about calling canisters from JS) or in icp-cli/dfx-migration.md (since it's a migration issue). See recommendation #2 above for the specific content. Wherever it lives, it should be discoverable from both skills via cross-reference.
6. motoko — Minor: Caffeine migration note
What happened (Kairos/Tribez): Projects migrated from the Caffeine platform used a Motoko compiler fork (0.16.3-caffeine) with --default-persistent-actors. Standard moc requires explicit persistent actor and transient var annotations. Took 4 compilation failures to figure out.
Current skill content: Already covers persistent actor (M0220), transient var, and stable keyword redundancy (M0218). This is mostly addressed.
Recommendation (minor): Consider adding one line to the pitfalls:
- Projects migrated from the Caffeine platform: the Caffeine moc fork used
--default-persistent-actors which made all variables implicitly stable.
Standard moc requires explicit `persistent actor` declaration and
`transient var` for non-persistent variables.
This is low priority since the existing M0220/M0219 coverage should guide the fix. Only relevant for Caffeine migrations.
Skill infrastructure observations (non-content)
These aren't about skill content but came up in the sessions and may be worth tracking separately:
-
npx skills addwithout-ylaunches an interactive TUI that hangs indefinitely for AI agents. The first 7 install attempts in ClearDeck timed out before the assistant discovered the-yflag. -
Skill(skill='icp-cli')returned "Unknown skill" in Claude Code despite the skill being installed and symlinked to.claude/skills/. The integration betweennpx skills addand Claude Code's skill discovery appears broken. The assistant had to fall back toWebFetchand then manual file reads. -
WebFetchof skill URLs returns AI-summarized content, not raw markdown. Critical details (exact config formats, code patterns) are lost in summarization. In the Kairos session, the assistant fetched the icp-cli skill via WebFetch, got a summary, and then wrote incorrect configs.
Session details
For reference, the sessions analyzed were:
- Kairos (Tribez) — Initial Caffeine→ICP migration, first mainnet deploy (no skills used)
- Kairos (Previous) — Feature development, UI overhaul, dfx→icp-cli migration attempt (skills fetched via WebFetch from skills.internetcomputer.org, 19 WebFetch calls)
- Kairos (Current) — Continued feature work (no skills used)
- ClearDeck (Previous) — Clone, deploy, ETH/DOGE integration, security audit (7 skills installed via
npx skills add -y, read locally for audit) - ClearDeck (Current) — dfx→icp-cli migration, testing, DX report (1 failed Skill tool call, 4 WebFetch fallbacks, local file reads)