meant 0.1.0 · @meantjs 0.4.0 · pre-1.0

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.

  1. 01

    Compile

    The .did becomes a validated Contract graph with an interface_id.

  2. 02

    Describe

    A profile bound to that id says what each value means and what each call does.

  3. 03

    Generate

    Agent tools, consent messages and formatters come from the resolved profile.

  4. 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.

  1. 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, that created_at_time makes a retry safe, or that Err.Duplicate means the payment already went through. The meant compiler 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

  2. 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 amount is 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.

  3. The agent writes what a person would
    The model sees1.5 ICP
    send_tokens({
      to: "ig6pt-…-ig6",
      amount: "1.5 ICP"
    })
    The call sends150000000
    icrc1_transfer(record {
      amount = 150_000_000;
      created_at_time = opt 1_793_059_200_000_000_000;
      …
    })
    js
    import { parseAmount } from "@meantjs/format";
    
    parseAmount("1.5 ICP", { decimals: 8, symbol: "ICP" }); // "150000000"
    parseAmount("1.123456789", { decimals: 8 }); // throws E_EXCESS_PRECISION: never rounded

    An agent writes amounts the way a person does. The tool layer converts them exactly, fills the defaults, stamps created_at_time once per intent and keeps it out of the model's hands, and refuses rather than rounds.

  4. 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 is transfer, approve, admin or destroy wait 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.

  5. Every result has a class
    Result case of icrc1_transferClassWhat the caller does
    OksuccessReport it done.
    Err.Duplicatealready_doneReport the earlier block. Never pay again.
    Err.TemporarilyUnavailableretry_sameResend the same encoded bytes.
    Err.TooOld, Err.CreatedInFutureretry_newStamp again, and ask the person to confirm again.
    Err.BadFee, Err.BadBurnfix_inputShow the hint and ask for a corrected value.
    Err.InsufficientFunds, Err.GenericError, anything elsefailStop. Do not retry.
    A timeout, then TooOldreconcileCheck 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

What you can build on it

What is published

One repository, B3Pay/Meant. The crates and the two package families version independently, and everything is pre-1.0.

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.

Start where you are

Upgrading from an earlier release

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.

Pre-1.0, and it says so

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 →