Summary
We built a production-grade vetKeys app from scratch using the ICP skill files as the primary reference — a decentralized family password manager called KeyChain (marc0olo/icp-skill-password-manager). During development, we hit several build-breaking and runtime errors that were directly caused by incorrect information in the vetkd skill. A secondary gap exists in the icp-cli binding-generation reference.
The errors below were verified by fetching the live skill files, not from memory.
1. vetkd skill — Frontend TypeScript API is wrong for @dfinity/vetkeys 0.4.0
The skill pins @dfinity/vetkeys at v0.4.0 in the prerequisites but then documents an API that does not exist in that version. All three errors below occur together when following the skill's TypeScript example.
1a. TransportSecretKey.fromSeed() does not exist
What the skill says:
const seed = crypto.getRandomValues(new Uint8Array(32));
const transportSecretKey = TransportSecretKey.fromSeed(seed);
const transportPublicKey = transportSecretKey.publicKey();
What @dfinity/vetkeys 0.4.0 actually provides:
const transportSecretKey = TransportSecretKey.random();
const transportPublicKey = transportSecretKey.publicKeyBytes();
Both fromSeed() and publicKey() are absent from the 0.4.0 package. Calling them throws a runtime TypeError: TransportSecretKey.fromSeed is not a function.
1b. vetKey.toDerivedKeyMaterial() does not exist
What the skill says:
const aesKeyMaterial = vetKey.toDerivedKeyMaterial();
const aesKey = await crypto.subtle.importKey(
"raw",
aesKeyMaterial.data.slice(0, 32), // 256-bit AES key
{ name: "AES-GCM" },
false,
["encrypt", "decrypt"],
);
What 0.4.0 actually provides:
// asDerivedKeyMaterial is async and returns DerivedKeyMaterial directly
const keyMaterial = await vetKey.asDerivedKeyMaterial();
// DerivedKeyMaterial has built-in encrypt/decrypt — no manual importKey needed
const ciphertext = keyMaterial.encryptMessage(plaintext, domainSeparator);
const plaintext = await keyMaterial.decryptMessage(ciphertext, domainSeparator);
The method was renamed from toDerivedKeyMaterial to asDerivedKeyMaterial, is now async, and the returned DerivedKeyMaterial exposes encryptMessage/decryptMessage directly. There is no .data property. Following the skill results in a runtime crash and a manual AES-GCM wiring that is both unnecessary and incorrect.
2. vetkd skill — test_key_1 cycle cost is wrong on mainnet
What the skill says (table and mistake 8):
test_key_1 | Local + Mainnet | Development & testing | 10_000_000_000
Actual runtime error when following the skill:
vetkd_derive_key request sent with 10_000_000_000 cycles,
but 26_153_846_153 cycles are required.
test_key_1 costs the same ~26B cycles as key_1 on mainnet because the cost is determined by the subnet holding the master key, not the key name. The skill itself explains this correctly in the fees paragraph ("Fees depend on the subnet where the master key resides") but then contradicts itself in the table and in mistake 8.
The skill also correctly says "send more cycles than the current cost so that future subnet size increases do not cause calls to fail; unused cycles are refunded" — but only as a note for canisters that may be blackholed. This guidance should be applied to the code example itself, regardless of key name.
Suggested fix for the Rust example:
// Send 30B cycles — actual cost for both test_key_1 and key_1 is ~26.15B.
// Fees depend on subnet size, not key name. Unused cycles are refunded.
let (response,): (VetKdDeriveKeyResponse,) = ic_cdk::api::call::call_with_payment128(
Principal::management_canister(),
"vetkd_derive_key",
(request,),
30_000_000_000u128,
)
3. icp-cli binding-generation reference — Result type representation undocumented
The binding-generation reference documents opt T → T | null but does not cover how Result<T, E> Candid types are represented in the generated bindings.
@icp-sdk/bindgen generates discriminated unions:
{ __kind__: "Ok"; Ok: T }
{ __kind__: "Err"; Err: E }
This is not obvious. The natural patterns ("Ok" in result, result.Ok !== undefined) do not work and silently fail or produce type errors. Without documentation, developers will use these patterns and get no useful error — queries simply return nothing or access is denied.
Suggested addition:
// Result<T, E> is a discriminated union — check __kind__, not property existence
const result = await actor.some_method();
if (result.__kind__ === "Err") {
console.error(result.Err);
} else {
console.log(result.Ok);
}
Similarly, Candid variants (e.g. variant { Read; Write; Manage }) become string enums:
import { AccessLevel } from "./bindings/backend";
// Use: entry.access === AccessLevel.Write
// Not: "Write" in entry.access (does not work)
4. internet-identity skill — UserInterrupt not documented
When a user closes the Internet Identity popup without authenticating, authClient.login() calls onError with a UserInterrupt error. This is expected behavior, not a real failure, but the skill does not mention it.
The skill's onError example:
onError: (error) => {
console.error("Login failed:", error);
reject(error);
}
As written, this logs/surfaces a UserInterrupt as an error on every popup dismissal.
Suggested addition:
onError: (error) => {
// UserInterrupt means the user closed the popup — not an error
if (String(error).includes("UserInterrupt")) return;
console.error("Login failed:", error);
reject(error);
},
Reference implementation
All errors above were encountered during the construction of marc0olo/icp-skill-password-manager — a working end-to-end vetKeys app with:
- Rust backend using
ic-stable-structures 0.7, ic-cdk 0.19, raw vetkd_derive_key calls
- React/TypeScript frontend using
@dfinity/vetkeys 0.4.0, @icp-sdk/auth 5.x, @icp-sdk/bindgen 0.3.x
- Client-side-only encryption: the canister never sees plaintext
- Shared vault access control (Read / Write / Manage)
The working crypto.ts (source) shows the correct 0.4.0 API in full context.
Summary
We built a production-grade vetKeys app from scratch using the ICP skill files as the primary reference — a decentralized family password manager called KeyChain (marc0olo/icp-skill-password-manager). During development, we hit several build-breaking and runtime errors that were directly caused by incorrect information in the
vetkdskill. A secondary gap exists in theicp-clibinding-generation reference.The errors below were verified by fetching the live skill files, not from memory.
1.
vetkdskill — Frontend TypeScript API is wrong for@dfinity/vetkeys0.4.0The skill pins
@dfinity/vetkeysat v0.4.0 in the prerequisites but then documents an API that does not exist in that version. All three errors below occur together when following the skill's TypeScript example.1a.
TransportSecretKey.fromSeed()does not existWhat the skill says:
What
@dfinity/vetkeys0.4.0 actually provides:Both
fromSeed()andpublicKey()are absent from the 0.4.0 package. Calling them throws a runtimeTypeError: TransportSecretKey.fromSeed is not a function.1b.
vetKey.toDerivedKeyMaterial()does not existWhat the skill says:
What 0.4.0 actually provides:
The method was renamed from
toDerivedKeyMaterialtoasDerivedKeyMaterial, is now async, and the returnedDerivedKeyMaterialexposesencryptMessage/decryptMessagedirectly. There is no.dataproperty. Following the skill results in a runtime crash and a manual AES-GCM wiring that is both unnecessary and incorrect.2.
vetkdskill —test_key_1cycle cost is wrong on mainnetWhat the skill says (table and mistake 8):
Actual runtime error when following the skill:
test_key_1costs the same ~26B cycles askey_1on mainnet because the cost is determined by the subnet holding the master key, not the key name. The skill itself explains this correctly in the fees paragraph ("Fees depend on the subnet where the master key resides") but then contradicts itself in the table and in mistake 8.The skill also correctly says "send more cycles than the current cost so that future subnet size increases do not cause calls to fail; unused cycles are refunded" — but only as a note for canisters that may be blackholed. This guidance should be applied to the code example itself, regardless of key name.
Suggested fix for the Rust example:
3.
icp-clibinding-generation reference — Result type representation undocumentedThe binding-generation reference documents
opt T → T | nullbut does not cover howResult<T, E>Candid types are represented in the generated bindings.@icp-sdk/bindgengenerates discriminated unions:This is not obvious. The natural patterns (
"Ok" in result,result.Ok !== undefined) do not work and silently fail or produce type errors. Without documentation, developers will use these patterns and get no useful error — queries simply return nothing or access is denied.Suggested addition:
Similarly, Candid variants (e.g.
variant { Read; Write; Manage }) become string enums:4.
internet-identityskill —UserInterruptnot documentedWhen a user closes the Internet Identity popup without authenticating,
authClient.login()callsonErrorwith aUserInterrupterror. This is expected behavior, not a real failure, but the skill does not mention it.The skill's
onErrorexample:As written, this logs/surfaces a
UserInterruptas an error on every popup dismissal.Suggested addition:
Reference implementation
All errors above were encountered during the construction of marc0olo/icp-skill-password-manager — a working end-to-end vetKeys app with:
ic-stable-structures0.7,ic-cdk0.19, rawvetkd_derive_keycalls@dfinity/vetkeys0.4.0,@icp-sdk/auth5.x,@icp-sdk/bindgen0.3.xThe working
crypto.ts(source) shows the correct 0.4.0 API in full context.