Individuality¶
Introduction¶
@parity/product-sdk-individuality reads a person's standing on the Individuality chain and lets your Product act as that person on it. It is the typed way to answer "is this a verified human, and how far along are they?" without learning who they are.
The package has two halves. The read half works in both directions: given a .dot username or an account, what is that person's Proof of Personhood state; and given an account, which usernames does it hold. The write half is a single function, withAsPerson, which wraps a signer so a transaction dispatches under a person origin instead of an account origin.
Reads return a typed Result, so you check .ok before reading .value. A username nobody owns is not a failure: it arrives on the success channel as a UsernameUnowned result.
Not an authorization oracle
This is a client-side read in a client-side library. A backend that trusts "the SDK said Member" is trivially spoofed. Use it to shape your interface — show progress, gate a button, pick a label — but anything that gates real value must verify on chain itself.
When to Use It¶
- To read a person's personhood state and progress metrics for display, from either a username or an account (
readPersonhoodState). - To resolve which usernames an account holds, and which one to show (
lookupUsername,displayUsername). - To dispatch a call under a person origin rather than an account origin (
withAsPerson), for extrinsics the Individuality chain gates on personhood. - To read the periodic game and its prize draws, and to build the sign-up and claim calls around them.
- Not to authorize anything server-side, and not to gate access to funds. See the warning above.
Core Concepts¶
- Everything is pinned to one block: A read batches several storage lookups and reports the
FinalizedSnapshot(blockHash,blockNumber) they all came from. The personhood threshold and the absence-grace ratio update on a session cadence, so an unpinned read could mix eras and derive a state that never existed. PersonhoodResultversusPersonhoodState: The outer result isUsernameUnownedorResolved. OnlyResolvedcarries an account, an optional contextualalias, thestate, andmetrics.- Seven states, discriminated by
tag:NotEnrolled,Lite,Candidate(accruing score, carriesscoreandpersonhoodThreshold),MembershipReady,Member(carriesactiveWeeks),Caution(the next absence would breach the grace policy), andSuspended. metricsis always present on a resolved read: The same numbers the state was derived from, in every state, so a progress interface renders without branching on the tag first.Caution.missesis a projection: It is what the absence window would hold after one more absence, not a count of past absences. Awindowof0means no grace at all and lands inCautionregardless.- Lite and full usernames: An account always has a lite username (
example.07); a full one appears only once the person claims a bare name.displayUsernamepicks the right one,usernameBaseextracts the letters a claim would offer, andcanClaimFullUsernameis the chain's own precondition, not an approximation. - The derivation is exported separately:
derivePersonhoodStateis pure. Feed it a snapshot you already hold and it needs no chain client and no Host.
Read a Person's Standing¶
Pass either a username or an account. Branch on .ok, then on the result tag:
import { getChainAPI } from '@parity/product-sdk-chain-client';
import { readPersonhoodState } from '@parity/product-sdk-individuality';
const chain = await getChainAPI('paseo');
const result = await readPersonhoodState(chain, { username: 'alice.dot' });
if (!result.ok) {
console.error(result.error.message); // ProductIndividualityError
} else if (result.value.tag === 'UsernameUnowned') {
// Nobody owns this name — a success value, not an error.
} else {
const { state, metrics, at } = result.value;
console.log(state.tag, 'as of block', at.blockNumber);
if (state.tag === 'Candidate') {
console.log(`${state.score} of ${state.personhoodThreshold}`);
} else if (state.tag === 'Member') {
console.log(`${state.activeWeeks} consecutive games`);
}
}
Resolve an Account's Username¶
The other direction, from an account to the names it holds:
import { getChainAPI } from '@parity/product-sdk-chain-client';
import {
canClaimFullUsername,
displayUsername,
lookupUsername,
usernameBase,
} from '@parity/product-sdk-individuality';
const chain = await getChainAPI('paseo');
const usernames = await lookupUsername(chain, { account: rootAddress });
if (usernames.ok && usernames.value !== null) {
const record = usernames.value;
console.log(displayUsername(record)); // full name if claimed, else the lite one
if (canClaimFullUsername(record)) {
console.log('could claim:', usernameBase(record.liteUsername));
}
}
A null value means the account has no record at all, which is an answer rather than a failure.
Act Under a Person Origin¶
withAsPerson wraps a PolkadotSigner so the call dispatches as a person. It returns a signer, so submission stays with Transactions:
import { submitAndWatch } from '@parity/product-sdk-tx';
import { withAsPerson } from '@parity/product-sdk-individuality';
const personSigner = withAsPerson(accounts.getProductAccountSigner(account), {
tag: 'AliasWithAccount',
});
const result = await submitAndWatch(someGatedCall, personSigner);
The AsPersonInfo variants are AliasWithAccount (the signing account is already bound to the alias, no proof needed), AliasWithProof (authorized by a ring-VRF proof alone), and AliasWithAccountRevised (signs and moves the stored alias to the current ring revision, which is the fix when the chain answers BadSigner).
AsPerson errors are thrown, not returned
Unlike the rest of the package, withAsPerson raises AsPersonError rather than returning a Result. It has to: the failure happens inside PolkadotSigner.signTx, where there is no error channel to return on. Wrap the submission in try/catch as well as checking the Result.
The Game and Prize Draws¶
The Individuality chain runs a periodic game, and the package covers it end to end: readCurrentGame for the current game and its phase, readGameAirdropEventIds and readAirdropDraw for its prize draws, readPrizeStatus for one identity's outcome across every draw at a single pinned block, signUpWithAccountTx to enter, and readClaimEligibility plus claimPrizeTx and confirmClaim to collect a prize.
Two details shape how you use it: claim_airdrop has six gates and only two concern personhood, so eligibility is exported as a predicate (deriveClaimEligibility) separately from the read that feeds it; and confirmClaim re-reads whether a claim landed, which is how a claim flow survives a page reload, since a successful claim removes the Winners row.
Only the account sign-up path is buildable today
Of the two sign-up variants, only Account can be constructed. The Alias variant needs a ring-VRF proof at a context the chain chooses, and every context a Host will sign under is derived from the product id. The package's signup-types.ts records the current blockers.
Limitations¶
- Client-side only, and not a source of authorization. Verify on chain for anything that gates value.
- Reads return a
ResultcarryingProductIndividualityError;withAsPersonthrowsAsPersonErrorinstead. readPersonhoodStatepins one finalized block. Treat the state as a snapshot with anat, not a live value, and re-read rather than caching across sessions.- The
AliasWithProofvariant is rejected on the Individuality runtime Paseo runs today, however correct the bytes are. It becomes reachable after the network upgrades, with no change needed here. - A personhood tier is obtained in the Polkadot App; nothing in this package grants or raises one.
Where to Go Next¶
-
Learn Identity
How the
.dotname, the per-app account, and Proof of Personhood stay separate, and why. -
Learn Proof of Personhood
The Ring-VRF mechanism, the tiers this package reads, and per-app aliases in depth.
-
External Package Source
The complete
individualitysurface: the state machine, the game and airdrop reads, andwithAsPerson.
| Created: September 2, 2026