Skip to content

Skill improvements based on real-world session analysis (Kairos + ClearDeck) #136

@marc0olo

Description

@marc0olo

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:

  1. npx skills add without -y launches an interactive TUI that hangs indefinitely for AI agents. The first 7 install attempts in ClearDeck timed out before the assistant discovered the -y flag.

  2. Skill(skill='icp-cli') returned "Unknown skill" in Claude Code despite the skill being installed and symlinked to .claude/skills/. The integration between npx skills add and Claude Code's skill discovery appears broken. The assistant had to fall back to WebFetch and then manual file reads.

  3. WebFetch of 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    hallucinationSkill produces incorrect or fabricated outputskill-improvementImprovement to an existing skill

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions