What you meant is what runs
Meant is the meaning layer for contract calls. It compiles a Candid interface into a canonical graph with an identity, and a profile bound to that identity says what each value means and what each call does: 1 ICP is 108 base units, this call moves money, this error means it already went through. From one profile come agent tools in human units, ICRC-21 consent messages, and display and input formatting.
From an interface to a call the owner confirms
Two layers, one job. The interface layer says what a call's values are; the profile layer says what they mean.
- 01
Compile
The
.didbecomes a validated Contract graph with aninterface_id. - 02
Describe
A profile bound to that id says what each value means and what each call does.
- 03
Generate
Agent tools, consent messages and formatters come from the resolved profile.
- 04
Confirm
The owner approves the exact call. Retries cannot pay twice.
One transfer, start to finish
An ICRC-1 transfer of 1.5 ICP, from the interface to the result. The interface, the
annotation and the consent message are real: the first two are files in the repository, and
the message is what @meantjs/consent returns for this transfer with the ICRC-1
pack. The JavaScript is compiled against the package's declarations when the site is built.
-
The interface says what the values are
candidprofile/packages/consent/test/fixtures/ledger.did
type TransferArg = record { from_subaccount : opt Subaccount; to : Account; amount : Tokens; fee : opt Tokens; memo : opt blob; created_at_time : opt Timestamp }; // … icrc1_transfer : (TransferArg) -> (TransferResult);The interface says
amount : nat. It does not say that the ledger counts in units of 10-8, thatcreated_at_timemakes a retry safe, or thatErr.Duplicatemeans the payment already went through. Themeantcompiler turns the interface into a Contract graph whose identity moves only when the callable interface does; the ICRC-1 standard's fragment has this one:interface_idcandid-core:interface:v1:sha256:7fae0dd330b20f53ae1e4e2cb1947c1c57791da0ae20299ceb499f63228d0689
-
A profile says what they mean
jsonprofile/packs/icrc1.profile.json
{ "at": "icrc1_transfer(0).amount", "tag": "amount", "args": { "decimals": { "from": "query:icrc1_decimals" }, "symbol": { "from": "query:icrc1_symbol" } } },A profile is a separate document bound to that identity. Each annotation names one position in the interface and attaches a tag to it. This one, from the ICRC-1 pack, says the transfer's
amountis a token amount whose decimals and symbol the ledger reports itself. The pack carries 29 such annotations, and applies to any ledger whose interface is a subtype of the standard's. -
The agent writes what a person would
The model sees1.5 ICP
send_tokens({ to: "ig6pt-…-ig6", amount: "1.5 ICP" })The call sends150000000icrc1_transfer(record { amount = 150_000_000; created_at_time = opt 1_793_059_200_000_000_000; … })jsimport { parseAmount } from "@meantjs/format"; parseAmount("1.5 ICP", { decimals: 8, symbol: "ICP" }); // "150000000" parseAmount("1.123456789", { decimals: 8 }); // throws E_EXCESS_PRECISION: never roundedAn agent writes amounts the way a person does. The tool layer converts them exactly, fills the defaults, stamps
created_at_timeonce per intent and keeps it out of the model's hands, and refuses rather than rounds. -
The owner reads what the call does
json
{ "FieldsDisplayMessage": { "intent": "Send tokens: Send 1.5 ICP to ig6pt-bilfj-ewrb5-gyxsa-gisbm-b7z5p-o47mn-dswdx-s225j-4ysgf-ig6", "fields": [ ["Amount", { "TokenAmount": { "decimals": 8, "amount": "150000000", "symbol": "ICP" } }], ["Recipient", { "Text": { "content": "ig6pt-bilfj-ewrb5-gyxsa-gisbm-b7z5p-o47mn-dswdx-s225j-4ysgf-ig6" } }] ] } }The same profile writes the ICRC-21 consent message for the call, for any method, not only
icrc1_transfer. Calls whose effect istransfer,approve,adminordestroywait for the owner's confirmation, unless a policy the owner set allows them: for a transfer or an approval, that can be terms the owner confirmed for the task. The model never confirms. -
Every result has a class
Result case of icrc1_transferClass What the caller does Oksuccess Report it done. Err.Duplicatealready_done Report the earlier block. Never pay again. Err.TemporarilyUnavailableretry_same Resend the same encoded bytes. Err.TooOld,Err.CreatedInFutureretry_new Stamp again, and ask the person to confirm again. Err.BadFee,Err.BadBurnfix_input Show the hint and ask for a corrected value. Err.InsufficientFunds,Err.GenericError, anything elsefail Stop. Do not retry. A timeout, then TooOldreconcile Check the ledger's history for the first attempt before anything else. Every result case has a class, so the caller knows whether to stop, fix the input, retry with the same bytes, or find out whether the first attempt already went through.
Two layers
The meant crate compiles a Candid interface through the official checker into
a canonical, validated graph with content-addressed identities. @candid-core/schema
gives TypeScript typed schemas, validation and a binary codec from the same graph.
The Candid Profile spec, its standard packs, and the @meantjs packages that
resolve profiles and generate tools, consent and formatting from them. meant-format
does the amount tag in Rust.
What you can build on it
Input in human units, a preview the person confirms before every send, and retries that cannot pay twice.
ICRC-21 messages, both forms, from a resolved profile and the encoded arguments, in a browser, in Node, or inside the canister.
Amounts, times, durations, accounts, subaccounts and bytes, parsed and shown by one rule that refuses excess precision rather than rounding it.
Compare one string. Renamed types, reordered fields and comments leave
interface_id where it was; a changed argument type moves it.
A schema-directed implementation of the wire format with no runtime dependencies, which returns issues instead of throwing.
.did source you did not write
Every entry point is bounded: over-limit input comes back as a structured diagnostic rather than exhausting memory, CPU or the stack.
What is published
One repository, B3Pay/Meant. The crates and the two package families version independently, and everything is pre-1.0.
The Rust compiler and model, and the meant binary.
The TypeScript schema runtime, and the compiler and generator as WebAssembly.
npm · 0.4.0 @meantjs/*profile, format, consent, tools and
reconcile: five packages released together.
The amount tag in Rust, with no dependencies and no_std.
Every crate and package, its version, what it depends on, and which one you need.
Chains
Meant was built on the Internet Computer. Most of the profile spec does not depend on the chain, and a binding per chain covers what does.
Candid interfaces, the ICRC-1 and ICRC-2 packs, ICRC-21 consent and PocketIC.
EVM · experimental EVM contractsProfiles for Solidity ABIs and EIP-712 messages, consent text for calls and signatures, and ERC-7730 files.
SOL · planned SolanaNot supported yet: what a binding would need, and what is decided so far.
Start where you are
Check the ICRC-1 pack against a ledger's interface and convert an amount exactly.
Install @candid-core/schema, describe an interface, validate a value and
encode it, with no Rust toolchain.
.did to a Contract
One dependency line and one call give you a validated graph with identities you can compare and cache on.
Evaluating it What is actually provenThe verification gates, the cross-implementation evidence, and what is still pending.
The TypeScript on this site is @candid-core/schema 0.5.0 and
@candid-core/cli 0.4.0, published on 2026-10-27 under the npm latest dist-tag. The previous
stable releases, @candid-core/schema 0.4.0 and @candid-core/cli 0.3.0, generate the same modules, type a decoded value as unknown, and have no didToDocAttributes; Migrating from 0.4.0 says what the upgrade means.
The stable releases before those, @candid-core/schema 0.3.0 and @candid-core/cli 0.2.0, differ more: CLI 0.2.0 declares each type as a $-prefixed local (type $Status = …) and types no method with its mode. Migrating from 0.3.0 says what that upgrade means.
The stable releases before those, @candid-core/schema 0.2.0 and @candid-core/cli 0.1.0, have the old surface, and Migrating from 0.2.0 shows what each break means, with before and
after code. The beta dist-tag still names 0.3.0-beta.1 and 0.2.0-beta.1, which lack the changes recorded under 0.3.0 and 0.2.0 and every stable release since. The repository has moved on since: it is prepared as @meantjs/schema 0.6.0 and @meantjs/cli 0.5.0, not yet published, which are this pair’s code under new names: the CLI’s command becomes meant, and the modules it generates differ from 0.4.0’s only in their first two lines, a new header and an import of @meantjs/schema. Migrating from 0.5.0 says what the upgrade means.
The crates are meant 0.1.0 and meant-format
0.2.0; the npm packages are @candid-core/schema 0.5.0,
@candid-core/cli 0.4.0 and the five @meantjs packages at
0.4.0; the profile spec is Draft 0.2. Any release before 1.0 may change the public
API, the serialized shapes, the canonical bytes and every identity computed over them, so pin
exact versions: meant = "=0.1.0", npm install --save-exact.
Versions, changes and release policy →