Introduction
Every ICP app that wants to show a user profile asks the same questions: What’s your name? Upload a photo. Add your bio. Social links?
Users answer the same questions on every app they use. That data lives in isolated app databases — inaccessible to other apps, owned by the platform instead of the user, and reset every time the user tries a new dApp.
iiprofile changes this. It’s a portable, decentralized key-value store built on the iilink principal layer. Write your profile once. Every iilink-integrated app reads the same data — automatically, with no re-entry, no per-app setup, and no centralized data custodian.
It’s live on ICP mainnet. I’m shipping it alongside iilink and iiname as the third layer of a portable ICP identity stack.
The Problem
The ICP ecosystem has a data portability problem. Because every app sees a different principal per user (the root problem iilink solves at the identity layer), there’s also no practical way to share user data between apps. Even if two apps wanted to share profile data, they’d have no common user identifier to key it on.
iilink solves the identity layer. iiprofile solves the data layer.
With a unified main principal from iilink, it’s now possible to attach persistent, user-owned data to that principal — and have every app in the ecosystem read from the same source of truth.
The Solution: iiprofile
iiprofile is an on-chain key-value store where:
-
->Storage is rented annually per iilink main principal
-
->Only the principal owner can write (authenticated via Internet Identity + iilink proxy auth)
-
->Anyone can read — apps make anonymous read calls with the user’s principal
-
->Keys follow an open naming convention:
iiprofile:for standard profile fields,yourapp:for app-specific data -
->The same canister stores everything — one round-trip fetches a complete profile
Key Features
For users:
-
->One profile setup covers every iilink-integrated app going forward
-
->Free public profile page at
/@yourname(if you have an iiname) or via principal -
->Store name, bio, avatar, banner, CTA button, social handles, up to 5 custom links
-
->Full control — only you can write to your store; revoke nothing, because apps only read
For developers:
-
->Fetch a complete user profile in one batched call — no auth required
-
->Write app-specific keys to the same store (
yourapp:theme,yourapp:preferences, etc.) -
->Open key convention: define your own namespace, document it, other apps can interoperate
-
->The key-value model is intentionally simple — no schema enforcement
Storage Plans
All plans are annual (365 days), payable in ICP or TCYCLES:
| Plan | TCYCLES / year | Storage | Max Keys | Writes / day |
|---|---|---|---|---|
| Starter | 0.1 | 64 KiB | 128 | 50 |
| Builder | 0.5 | 256 KiB | 512 | 300 |
| Power | 2.0 | 1 MiB | 1,024 | 500 |
Write quotas refill daily. Unused writes don’t carry over. Plans can be upgraded at any time with data preserved.
Standard Profile Keys
The iiprofile:* namespace is an open convention — not enforced by the canister. Any app can read these keys. Any app can extend the convention with new keys.
iiprofile:name iiprofile:bio
iiprofile:photo_url iiprofile:banner_photo_url
iiprofile:background_photo_url
iiprofile:twitter iiprofile:github
iiprofile:discord iiprofile:telegram
iiprofile:email iiprofile:website
iiprofile:cta_text iiprofile:cta_button_text
iiprofile:cta_url iiprofile:cta_photo_url
iiprofile:link1_text iiprofile:link1_url
iiprofile:link2_text iiprofile:link2_url
iiprofile:link3_text iiprofile:link3_url
iiprofile:link4_text iiprofile:link4_url
iiprofile:link5_text iiprofile:link5_url
Developer Integration
Read a Full Profile (Anonymous, One Call)
// main = { owner: Principal, subaccount: [] }
// This is the user's iilink main principal — you get it from the iilink auth flow
const args = [
{ main, key: 'iiprofile:name' },
{ main, key: 'iiprofile:bio' },
{ main, key: 'iiprofile:photo_url' },
{ main, key: 'iiprofile:twitter' },
{ main, key: 'iiprofile:website' },
{ main, key: 'iiprofile:cta_text' },
{ main, key: 'iiprofile:cta_url' },
// ... include all keys you need, up to ~25 in one batch
];
const res = await iiprofile.akvs_values(args);
// Build a profile object from results
const profile = {};
res.results.forEach((opt, i) => {
if (opt[0]?.Text) profile[args[i].key] = opt[0].Text;
});
// profile['iiprofile:name'] → 'Kay'
// profile['iiprofile:twitter'] → '@kayicp'
// profile['iiprofile:photo_url'] → 'https://...'
Write Your App-Specific Keys
I will implement opening iiprofile popup for your user to write your app keyvalues. Soon™.
The Open Key Convention
One design decision worth highlighting: the iiprofile:* keys are a convention, not a permission system. The canister doesn’t enforce which keys mean what. This is intentional.
Any app can:
-
Read standard
iiprofile:*keys to show a user’s profile -
Write to its own
yourapp:*namespace for preferences/settings -
Publish their key definitions so other apps can interoperate
This creates a composable data layer where each new app that defines useful keys makes the entire ecosystem more valuable. A user’s iiprofile storage gradually becomes a portable data record that follows them everywhere.
Current State & Limitations
-
->Annual renewal required — there’s no auto-renew mechanism. An on-chain reminder system is planned.
-
->Early stage — I’m the first user. The canister is live and functional but the ecosystem of apps reading from it is just getting started.
Relationship to iilink and iiname
These three projects are designed to work together:
-
->iilink provides the unified principal — the common identifier that makes cross-app data portable
-
->iiname makes that principal human-readable — giving users an @handlehandlehandlehandle across the ecosystem
-
->iiprofile attaches persistent, portable data to that principal — profile, preferences, and app-specific settings
Each is independently useful, but together they form a complete portable identity layer for ICP.
Call to Action
If you’re building any app that involves user profiles, display names, or per-user settings, I’d love for you to consider iiprofile as your data layer instead of building it from scratch.
The integration path:
-
Get the user’s iilink main principal from the iilink auth flow
-
Call
akvs_values()with the keys you need — no auth required for reads -
Display the result; fall back gracefully if the user hasn’t set up iiprofile yet
Try it: https://kvya4-zaaaa-aaaac-qgtea-cai.icp0.io/
Backend canister: lywes-wiaaa-aaaac-qgtdq-cai (dashboard)
My iiprofile:
or replace the principal with my iiname:
https://kvya4-zaaaa-aaaac-qgtea-cai.icp0.io/@kayicp
Feedback welcome.
previous topics:
byebye


