Internet Computer

Candid interfaces, ICRC ledgers, ICRC-21 consent and PocketIC: the chain Meant was built on.

The Internet Computer is the chain the profile spec was written for: everything in Candid Profile v0 outside §15 describes ICP canisters. A canister publishes a Candid interface, the meant compiler turns it into a Contract graph with an identity, and a profile binds to that identity. The rest of this page is what each step looks like on ICP, with links to the pages that explain it.

bash
npm install @meantjs/profile@0.5.0
npx candid-profile resolve ledger.did --packs > ledger.resolved.json

The interface: a .did file#

  1. Candid source

    The canister's .did file, or the candid:service metadata it serves.

  2. The meant compiler

    Parses and type-checks it through the official Candid parser and projects it into a validated Contract graph (The interface layer, The Contract graph).

  3. interface_id

    A SHA-256 identity over the callable interface, candid-core:interface:v1:sha256:…. Renaming types or reordering fields does not move it; changing a method does (Content-addressed identities).

  4. Profile

    A canister profile binds to that interface_id; a pack binds to any service whose type is a subtype of its fragment (Profiles, layers and binding).

@meantjs/profile compiles a .did with @candid-core/cli 0.4.0, the same compiler built as WebAssembly, or reads a candid-core ContractEnvelope that a generator already wrote (--envelope); the resolved form is the same either way. On the ICRC ledger's own interface (release ledger-suite-icrc-2026-03-09, the copy in profile/packages/resolver/test/fixtures/ledger.did), run from profile/:

text
candid-profile: packages/resolver/test/fixtures/ledger.did  candid-core:interface:v1:sha256:22eb4787e1a755acb9aabd0ed2121d754098bcd1cedb5d0ed8b5c4effb921846  [26 methods]
  applied     icrc1            pack         candid-profile:v0:sha256:4eaa9312d60661248ff5627dba9906aa91240abe84cb1b1da8ca20a6fba2e3e6
  applied     icrc2            pack         candid-profile:v0:sha256:f9d75e8bf5ff2edb0b167988feee494eeb54c386e6f62252c4bd96ac56346567
  0 error, 0 warning, 3 info

Profiles address positions by the edge that leads to them (a method argument or result, then a path), not by type node: the compiler interns structurally equal types, so in the ICRC-1 interface one nat node serves amount, fee and the block index alike (Addresses).

Values on ICP#

TagCandid typeA person writesThe call carries
amountnat and the sized integers1.5 ICP150000000 with decimals: 8, from query:icrc1_decimals on a ledger
accountrecord { owner : principal; subaccount : opt blob }the ICRC-1 textual encoding, principal-checksum.subaccounthex, or a bare principal for the default subaccountthe record; the checksum is verified on input
subaccountblob#7, 7, or 64 hex characters32 bytes, big-endian
timestampnat64 (ledgers use unit: "ns")an RFC 3339 time, or +PT24H from the canister's clocknanoseconds since the Unix epoch

now is always the canister's clock, never the host's: a host clock ahead of the replica produces CreatedInFuture, one behind it TooOld. The rules are on Values and exact conversion.

ICRC-1 and ICRC-2 packs#

The ICRC-1 pack applies to any canister whose service is a subtype of its fragment (icrc1_symbol, icrc1_decimals, icrc1_fee, icrc1_balance_of, icrc1_transfer); the ICRC-2 pack to icrc2_approve, icrc2_transfer_from and icrc2_allowance. A ledger that implements both gets both, as above. Result variants may have more cases than the pack's (D-019): the extra cases carry no annotations and fall back to the pack's default outcome. Every annotation is listed on Standard packs.

Profile annotation
at
icrc1_transfer
tag
idempotency
args
  • mode: replay_same_payload
  • stamp: created_at_time

Retries: created_at_time and the dedup window#

fact An ICRC-1 ledger deduplicates a transfer only when created_at_time is set, and only within TX_WINDOW plus PERMITTED_DRIFT, which the ledger defines; different memos are never treated as duplicates; a duplicate returns Duplicate { duplicate_of } (spec §5.3, verified against the ICRC-1 standard on 2026-10-08). That is what the pack's idempotency tag uses: the tool layer stamps created_at_time once per intent from the ledger's clock, encodes the arguments once, and sends exactly those bytes on every retry of that intent. For the DFINITY ledger the window is 24 h 2 min: DEDUP_WINDOW_NS in @meantjs/reconcile is 86,520,000,000,000 ns, for a caller that has checked its ledger uses it.

icrc1_transfer resultClassWhat the caller does
OksuccessDone; report the block index.
Err.Duplicatealready_doneAn identical call already executed: treat it as success and report duplicate_of.
Err.TemporarilyUnavailableretry_sameRetry with the same bytes, a few times with backoff, before the window can close.
Err.TooOld, Err.CreatedInFutureretry_newRe-stamp, show the person the new intent, and retry only after they confirm. After CreatedInFuture the new stamp must not exceed the ledger_time the error reports.
Err.BadFee, Err.BadBurnfix_inputCorrect the input; the error's fields carry the hint (expected_fee).
Err.InsufficientFunds, Err.GenericError, any other casefailDo not retry.

A rejection by the replica (a trap, a refused call) means the call did not execute, and the same bytes may be sent again. An unknown outcome (a timeout, a lost connection) may be retried with the same bytes inside the window. Without an idempotency tag, an update with an unknown outcome is never retried automatically. The full rules: Outcomes and retries.

Reconciliation by ICRC-3#

When an earlier attempt had an unknown outcome and a later one returns TooOld, the window has passed and the first attempt may have executed: the caller must reconcile before anything is sent or re-stamped. That means reading the ledger's ICRC-3 blocks since the stamp and matching the original on its operation, stamp, amount, accounts, memo and caller; balances and allowances cannot settle it. If the history cannot settle it, the caller asks the person.

jsprofile/packages/reconcile/README.md
import { expectedTransaction, matchBlocks } from "@meantjs/reconcile";

const expected = expectedTransaction("icrc1_transfer", arg, callerText, { minting });  // minting: icrc1_minting_account
// Read the blocks yourself (certified, archives followed): every index from `floor` to `logLength - 1`.
const r = matchBlocks(blocks, expected, { logLength, floor, inFlight: true, ledgerTime, windowNs });  // windowNs: the ledger's TX_WINDOW + PERMITTED_DRIFT
r.status; // "found" (r.block) | "absent" (r.closedBy) | "not_yet" (r.openUntil) | "unreadable" (r.reason, r.lookalike?)

matchBlocks never answers absent while a send may still land or a block in the range could not be read. See Settle an unknown outcome.

ICRC-21 is the Internet Computer's consent message standard: a signer asks the canister to describe a call before the person confirms it. @meantjs/consent answers it for any method from the resolved form and the argument bytes. For the ICRC-1 pack, a transfer of 0.29 TKA on a local ledger with 8 decimals gives this generic message:

textprofile/packages/consent/test/golden/REVIEW.md
# Send tokens

Send `0.29 TKA` to `k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae`

- **Amount:** `0.29 TKA`
- **Recipient:** `k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae`
ProfileFieldsDisplayMessage
intent.title and the rendered templateintent
each placeholderone (label, Value) row; the label from describe.label, else the field name
amountTokenAmount { decimals; amount; symbol } when it fits nat64, else Text
timestampTimestampSeconds { amount } after unit conversion
durationDurationSeconds { amount } after unit conversion
everything elseText { content }
jsprofile/packages/consent/README.md
const response = icrc21ConsentMessage(
  {
    method: "icrc1_transfer",
    arg: argBytes, // Uint8Array: the Candid-encoded argument tuple of the call
    user_preferences: { metadata: { language: "en", utc_offset_minutes: 120 }, device_spec: { GenericDisplay: null } },
  },
  {
    resolved, // fixtures/*.resolved.json shape, or the output of `candid-profile resolve`
    refs: { "query:icrc1_decimals": 8, "query:icrc1_symbol": "TKA" }, // dynamic refs, resolved by the caller
  },
);
Decimals come from one replica

A plain query is answered by a single replica, so a malicious one could report the wrong decimals and every amount shown would be wrong by a power of ten. Resolve the refs used to render a transfer or approve through an update call or a certified path, or cross-check with icrc1_metadata. A consent message generated inside the canister avoids this: the canister knows its own decimals.

TokenAmount.amount is a nat64, so an 18-decimal token above about 18.4 whole units falls back to a Text row. open OQ-12 (owner W4/W6): propose a nat variant upstream, or always fall back to Text? See Generate consent messages.

Serving a profile in a custom section#

A canister can serve its own profile as the public Wasm custom section icp:public candid:profile, beside the icp:public candid:service that holds its interface. A consumer reads both paths in one read_state request and verifies the certificate, so the pair comes from one version of the module.

  • fact The content of a custom section icp:public <name> is at /canister/<canister_id>/metadata/<name> in the state tree (IC interface specification).
  • fact Anyone may request that path for a public section, also when the canister has no such section, and the reply is a certificate of the subnet's state, unlike a query's reply, which one replica signs (HTTPS interface).
  • fact A canister has at most 16 exported custom sections named icp:… and 1 MiB of exported custom sections, so a served profile is at most 1 MiB (resource limits).
  • What may be served: a profile of kind: "canister" and origin: "author". Anything else is EE_SERVED, and the profile is not applied.
  • It is certified, not vouched for: the certificate proves the canister serves these bytes, but the words are still the author's claim.
bashprofile/docs/profile-v0.md
ic-wasm canister.wasm -o canister.wasm metadata candid:profile -f my.profile.json -v public

readServedProfile in @meantjs/profile checks the bytes a consumer read. The walk-through is Serve a profile from a canister.

Local tests on PocketIC#

Everything runs on a local network: no mainnet, no cycles bought, no keys. The SNS validator test in profile/packages/consent/pocketic/ registers the example validator canister with the real SNS governance canister on PocketIC. It downloads its artifacts into .cache/ and refuses any whose sha256 does not match artifacts.json.

PinnedVersion
PocketIC server16.0.0 (linux-x64 and darwin-arm64 binaries, each with its sha256)
@dfinity/pic0.25.0
@icp-sdk/core6.1.0
SNS governance wasm and .didbuilt by DFINITY's CI at dfinity/ic 649156c2bca1315da5699e2125b34be6397c9987
bashprofile/packages/consent/README.md
npm ci                               # at the repository root (the test imports this package and @dfinity/didc)
cd packages/consent/pocketic
npm ci                               # @dfinity/pic 0.25.0, @icp-sdk/core 6.1.0; install scripts off (.npmrc)
npm test                             # fetch + sha256-check the artifacts, build both example canisters, run

fact Observed on 2026-10-12: 10 of 10 tests pass (the package README, "On SNS governance"). open Deployed SNSes run the governance version blessed through SNS-W, which may differ from that commit; the test is re-run on a version bump. The reconciler's tests use ICRC-3 blocks in the shapes recorded from the pinned DFINITY ledger wasm on PocketIC.