IC-POS is a simple Point of Sale (POS) app that accepts ICRC-1 token payments on the Internet Computer. A merchant logs in with Internet Identity, configures a store, and charges customers by showing a QR code; incoming payments show up in the store with a live balance and transaction history.
To let you try the full flow without spending real funds, this example uses the TICRC1 test token (3jkp5-oyaaa-aaaaj-azwqa-cai) on mainnet, and a local ICRC-1 ledger + index when developing. Because the app talks to any ICRC-1 ledger, the same code works with real tokens (e.g. ckBTC) by pointing it at a different ledger.
- Create store: Log in with Internet Identity and configure the store with a name and notification settings.
- Charge customer: Enter an amount to generate a payment QR code (following the ICRC-22 payment-request format) for the customer to scan and pay.
- Send tokens: Send tokens to other principals from within the app.
- Transaction history: View recent transactions and a live balance for the store, queried from the ICRC-1 index canister.
Heads up: the payment QR code cannot be scanned by any wallet yet. ICRC-22 is still a draft (issue #22, PR #101) and, as far as we know, no wallet (OISY included) has adopted it. So the "Charge customer" QR code is illustrative — you can't currently pay it by scanning from a wallet. To actually complete a payment, send the tokens directly to the store's principal (copy it from the store page) with a wallet or
icp-cli, as shown in Try a payment below.
The backend is a single Motoko canister (backend). It stores per-merchant configuration (getMerchant / updateMerchant) and runs a timer that monitors the ICRC-1 ledger for incoming transfers. When a payment is detected for a merchant that has notifications enabled, it emits a canister log entry (observable with icp canister logs backend) noting where a notification would be sent.
The backend reaches the ledger through the typed import import Ledger "canister:icrc1_ledger" — no ledger actor type is declared in the code. The import is typed against the ledger's committed Candid interface (candid/icrc1_ledger.did), and the --actor-env-alias flag in mops.toml binds it to the PUBLIC_CANISTER_ID:icrc1_ledger environment variable that icp-cli injects (the local ledger when developing, TICRC1 on mainnet). The principal is resolved at canister install/upgrade; no principal is compiled into the Wasm, so the same artifact runs in every environment.
Notifications. The original app sent email/SMS via an HTTPS outcall to a third-party service. This version instead logs where a notification would be sent. To implement real notifications, use HTTPS outcalls.
Note. The monitor scans the ledger's global transaction log sequentially, which is illustrative rather than production-grade — it does not scale to a busy shared ledger. A production app would query the index canister per merchant account (as this app's frontend already does).
A TypeScript + React + Vite + Tailwind app. It authenticates with Internet Identity, calls the backend canister for store configuration, and uses the ICRC-1 ledger (balance, transfers) and index (transaction history) canisters. Canister IDs are read at runtime from the environment injected by icp-cli — there are no hardcoded IDs.
- Node.js
- icp-cli:
npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm - ic-mops:
npm install -g ic-mops
git clone https://github.com/dfinity/examples
cd examples/motoko/ic-pos
icp network start -d
bash deploy.shInternet Identity sign-in runs against the Internet Identity canister deployed on the local network (
ii: trueinicp.yaml). If signing in fails, update the local network launcher withicp network updateand restart the network.
Use
bash deploy.sh, noticp deploy, locally. The ICRC-1 ledger and index require init args (minting account, initial balances, and the ledger's canister ID) that are only known after identities and canisters are created, sodeploy.shinstalls them with the right arguments. A plainicp deploytraps because those canisters receive no init args.
deploy.sh installs a local ICRC-1 ledger + index (a throwaway token named LICRC1, distinct from the mainnet TICRC1), the backend, and the frontend. Internet Identity is provided by the local network (ii: true in icp.yaml) at http://id.ai.localhost:8000 — no separate deployment. Open the frontend URL printed by the script.
The script also creates a pre-funded ic-pos-dev identity that holds the local test tokens. It does not change your selected identity — your default identity has a zero balance, so pass --identity ic-pos-dev to spend the test tokens (no need to switch your default). Check it with:
icp token $(icp canister status icrc1_ledger -i) balance --identity ic-pos-dev-
Open the frontend, log in with Internet Identity, and give your store a name.
-
On the store page, click the principal pill to copy your store's principal.
-
Send tokens to the store from the pre-funded
ic-pos-devidentity using icp-cli. Amounts are in base units; with 8 decimals,100_000_000is 1 LICRC1. The ledger charges a small fee (0.0001 LICRC1) to the sender on top of the amount — the store receives the full amount.icp canister call icrc1_ledger icrc1_transfer \ '(record { to = record { owner = principal "<STORE_PRINCIPAL>" }; amount = 100_000_000 : nat })' \ --identity ic-pos-dev
The payment appears in the store. If the merchant has notifications enabled, the backend also emits a would-be-notification entry to its canister logs (icp canister logs backend).
For frontend hot-reload development after deploying: npm run dev --prefix frontend.
bash test.shtest.sh configures a merchant and performs a real token payment via icp-cli, asserting the merchant's balance increases.
Unlike local (which needs deploy.sh), mainnet uses a plain icp deploy:
icp deploy -e icOn mainnet the app uses the shared TICRC1 test token (ledger 3jkp5-oyaaa-aaaaj-azwqa-cai, index qzre3-3iaaa-aaaai-aqmsa-cai) and the production Internet Identity at https://id.ai — the local ledger and index are not deployed. Because those two canisters already exist on mainnet, there are no init-args canisters to install, so a plain icp deploy -e ic works (no deploy.sh needed).
To get TICRC1 tokens to test with:
- Obtain your principal — for example from OISY Wallet or with
icp identity principal. - Request TICRC1 tokens from the faucet using that principal.
- Send tokens to a store principal from your wallet (e.g. OISY) or with
icp canister call ... icrc1_transferagainst the TICRC1 ledger.
The backend/backend.did file defines the backend's public interface; the frontend bindings are generated from it during the build. If you change the backend's public API, regenerate it:
mops generate candid backendcandid/icrc1_ledger.did is the ledger's own interface, not the backend's — the canister:icrc1_ledger import is typed against it. The candid/ directory holds the interfaces of external canisters this project calls (as opposed to backend/backend.did, which is this project's own interface). These are not produced by mops generate candid — each is the Candid interface of an external canister. To refresh one (e.g. after bumping the ledger release), get it straight from the ledger with either of:
From mainnet — the live shared ledger (TICRC1). One command, no files to handle:
icp canister metadata 3jkp5-oyaaa-aaaaj-azwqa-cai candid:service -e ic > candid/icrc1_ledger.didFrom the pinned Wasm — matches exactly what deploys locally. The Wasm is the pre-built artifact pinned in this project's icp.yaml (the icrc1_ledger canister's build.steps[].url); download and unpack it, then extract its interface:
curl -sSL https://github.com/dfinity/ic/releases/download/ledger-suite-icrc-2026-03-09/ic-icrc1-ledger.wasm.gz | gunzip > ic-icrc1-ledger.wasm
ic-wasm ic-icrc1-ledger.wasm metadata candid:service > candid/icrc1_ledger.didBoth give the same interface as long as icp.yaml pins the release that is live on mainnet.
- A transaction detail page and pagination (currently only the latest transactions are shown).
- A confirmation dialog before sending.
Contributions are welcome! Please open an issue or submit a pull request.
Based on the original IC-POS by Kristofer Lund — kristofer@fmckl.se.