Introduction
If you’ve built or used multiple Internet Identity-powered apps on ICP, you’ve run into this problem: every app generates a different principal for the same user. Your ICP balance sits in one principal, your ckBTC in another, your app data somewhere else entirely. There’s no continuity of identity, no way to consolidate funds, and no shared context between apps.
I’ve been working on a fix. It’s called iilink, and it’s live on ICP mainnet.
The Problem in Detail
When a user authenticates with Internet Identity across multiple apps:
-
->App A sees principal
a1b4d-...-qae -
->App B sees principal
c9fdj-...-xae -
->App C sees principal
d0m5n-...-xqe
These are completely isolated. Funds sent to App A’s principal can’t be accessed from App B. Data stored in App A is invisible to App C. Every app starts from zero — no shared history, no unified balance, no persistent identity.
The typical workaround is a browser extension wallet (static principal) — but that requires an extension download, introduces constant approval popups for every call, and creates yet another layer of complexity for users unfamiliar with crypto tooling.
The Solution: iilink
iilink introduces a proxy-to-main delegation model on top of Internet Identity:
-
A user login with II, which generates the main principal — their single, portable ICP identity.
-
Each app’s II-generated principal becomes a proxy — linked to the main via a time-limited delegation.
-
Apps charge the main via ICRC-2 delegated transfers, not the proxy.
The result: one wallet, one balance, one identity — accessible from every iilink-integrated app, without the user needing to install anything or manually approve every transaction.
Key Features
-
->Unified principal — one iilink account recognized across all integrated apps
-
->No browser extension — works entirely through Internet Identity + canister logic
-
->ICRC-2 delegated transfers — apps charge the main principal via
icrc2_transfer_from, scoped to user-set spending limits -
->Allowance system — users set per-app, per-token spending limits with automatic expiry via
als_icrc1_approve -
->Identity delegation — proxy principals are linked to the main via
als_delegate -
->Credit system — lightweight on-chain credit mechanism (priced in TCYCLES for stability, payable in ICP or TCYCLES) via
als_add_credits -
->ICRC-1 whitelisted tokens — ICP, TCYCLES, ckBTC, ckETH, ckUSDT, ckUSDC.
How It Works (Technical)
Frontend Integration
The entire user-facing flow is a single popup:
javascript
function openIILink(amount) {
const params = new URLSearchParams({
spender: YOUR_CANISTER_ID, // your app's backend canister
proxy: ii.principal.toText(), // the user's II-generated principal
token: icp_token.id.toText(), // ICP ledger canister
amount: amount + icp_token.fee, // eg: '1.0001', not in e8s
expiry_unit: 'minutes',
expiry_amount: 5,
});
window.open(
`https://loxja-3yaaa-aaaan-qz3ha-cai.icp0.io/setup?${params}`,
'iilink',
'popup,width=480,height=660',
);
}
// Listen for completion
window.addEventListener('message', (e) => {
if (e.data?.type === 'iilink-done') {
// user has approved — proceed with your backend call
}
});
Backend Integration (Motoko)
Once the user approves, charge their iilink main principal from your backend:
public shared ({ caller }) func your_action(arg: YourArg) : async Result {
let proxy_a = { owner = caller; subaccount = arg.proxy_subaccount };
// Check sufficient allowance
let iilink = actor ("lhuc4-nqaaa-aaaan-qz3gq-cai") : Linker.Canister;
let pay_arg = {
main = main_account_opt; // if null: iilink will autoselect
spender_subaccount = null;
token = arg.token;
proxy = proxy_a;
amount = arg.amount;
to = recipient_account;
memo = arg.memo;
created_at = arg.created_at; // deduplication is mandatory
};
// iilink handles auth, allowance check, and ICRC-2 transfer
let pay_res = try await iilink.als_icrc1_transfer_from(pay_arg)
catch (e) return #Err(Error.convert(e));
// pay_res.Ok contains: block_index + resolved main account
// Store the main principal to unify this user across future calls
};
Querying User State
To check which mains a proxy is linked to:
// Get all main principals for a given proxy
const mains_res = await iilink.als_mains({
proxy: { owner: ii.principal, subaccount: [] },
previous: [],
take: [],
});
// Check allowance for a specific main + token
const allowance_res = await iilink.als_icrc1_allowances([{
main: { owner: main_p, subaccount: [] },
spender: { owner: your_canister_p, subaccount: [] },
token: icp_token_p,
}]);
Canister Details
| Property | Value |
|---|---|
| Canister ID | `lhuc4-nqaaa-aaaan-qz3gq-cai` (dashboard) |
| Network | ICP Mainnet |
| Token support | ICP, TCYCLES, ckBTC, ckETH, ckUSDT, ckUSDC |
| Pricing | 110 credits / 0.01 TCYCLES (starter) |
Current State & Limitations
This is an early-stage MVP. I’m the first user. A few honest notes:
-
->No frontend SDK yet — integration requires following the pattern above manually. A JS/TS SDK is on the roadmap.
-
->Allowance expiry is required — by design, all allowances must have an expiry. Permanent allowances are not supported.
-
->Audit — the canister has not been formally audited. Use with amounts you’re comfortable with while it matures.
Call to Action
If you’re building an II-powered app on ICP, I’d genuinely love for you to try integrating iilink. The full setup flow is one popup URL + one Motoko function. I’m happy to help with any integration questions directly in this thread.
Try it: https://loxja-3yaaa-aaaan-qz3ha-cai.icp0.io
Feedback welcome — especially from devs who’ve tried implementing ICRC-2 flows before and hit friction.

