1) iilink – One Unified Internet Identity Principal Across II-Powered Apps [Mainnet]

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:

  1. A user login with II, which generates the main principal — their single, portable ICP identity.

  2. Each app’s II-generated principal becomes a proxy — linked to the main via a time-limited delegation.

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

Right now, users need to trust me as the canister controller. That’s the honest answer.

The practical implications:

  • I can upgrade any of these canisters at any time. An upgrade could change logic — including how allowances are checked, how transfers are routed, or how key-value data is stored.

  • I can stop or delete the canisters, which would break resolution (iiname), make stored profiles unreadable (iiprofile), or break the delegation and transfer flow (iilink).

  • The canisters are not currently blackholed, not under SNS governance, and not verified open-source on-chain.

This is the standard trust model for an early-stage (MVP) ICP project under a single developer controller. I want to focus on finding PMF first before spending most of my time & energy doing decentralization. I hope people can understand this.

What partially mitigates this:

  • The ICRC-2 allowance model means iilink never custodies funds. Your tokens stay in your principal generated by Internet Identity. The canister only gets permission to transfer a capped amount per approval — and only to a destination you specified. A malicious upgrade couldn’t drain your wallet beyond the allowance you set, and allowances expire automatically.

  • For iiname and iiprofile, the risk is data loss or resolution failure rather than financial loss — no funds are held by those canisters beyond registration/storage fees already spent.

The longer-term path is:

  • Making the source code publicly verifiable on-chain

  • Moving toward a blackholed or SNS-controlled canister as the product matures and gets real usage

  • Potentially a community-governed upgrade mechanism

I’ll be transparent: none of that exists yet. You’re trusting a solo developer with a working MVP and a public reputation. If that trust model doesn’t fit your use case right now, I completely understand — and I’d rather you know upfront than discover it later.

Also straightforward: currently manual, funded by me.

Right now I’m topping them up from my own wallet. There’s no automated system, no reserve fund, and no on-chain mechanism that refills cycles from protocol revenue.

The credit system in iilink generates some TCYCLES revenue when users purchase credits — but that goes to the service provider account, not directly to canister cycles. Translating that revenue into cycles top-ups is a manual step I do periodically.

What this means practically:

  • If I’m unavailable for an extended period, the canisters could run low.

  • There’s no on-chain guarantee of uptime.

What I’m planning:

  • A cycles reserve canister that the service canisters can draw from, funded by a portion of protocol revenue automatically.

  • Longer term, exploring cycles-from-revenue patterns where a fraction of each user payment is converted to cycles and deposited directly — so the canisters are self-sustaining rather than dependent on me.

Again — this doesn’t exist yet. For now it’s “trust that the developer keeps the lights on,” which I recognize is not a satisfying answer for anything you’d build production infrastructure on.

Cool, thanks for explaining. Are the products feature complete? I’m just wondering if a quick approach to something somewhat risk-mitigated would be to blackhole them but with a mechanism where the canisters can detected that they’re no longer working, and if that occurs then automatically hand control to a lifeline canister (which you control) so that you can apply a fix (longer term the lifeline canister could be decentralised).

Also in terms of depending on you to keep the lights on, have you considered using a service like Jupiter Faucet to ensure the canisters would keep running even if you were no longer around?