@powforge/identity
v0.7.2
Published
Depth-of-Identity SDK for Nostr. Measures accumulated irreversible work across four dimensions of irreversible work (social, access, vouch, economic). Try it live at powforge.dev/explorer.
Maintainers
Readme
@powforge/identity
Try it live: powforge.dev/explorer — connect your Nostr profile with NIP-07, see your depth score, rank your peers, and look up any npub. Self-lookup is free and runs client-side.
Agent-payable signed scores: POST https://identity.powforge.dev/l402/identity-score returns a schnorr-signed score JSON for 2 sats via Lightning HTTP 402.
Depth-of-Identity SDK for Nostr. Measures accumulated irreversible work across four dimensions of irreversible work (social, access, vouch, economic) to produce a single identity weight score.
Not human vs bot. Invested vs uninvested. It is about measuring intentions.
Install
npm install @powforge/identityQuick Start
const { getIdentityDepth } = require('@powforge/identity');
const report = await getIdentityDepth(pubkey, {
relays: ['wss://relay.powforge.dev'],
});
console.log(report.weight); // total identity depth score
console.log(report.dimensions); // breakdown by dimensionWhat It Measures
Four dimensions of identity depth:
| Dimension | What It Scores | Event Kinds | |-----------|---------------|-------------| | Social | Notes published, reactions given, unique peers interacted with | kind:1, kind:7 | | Access | NIP-13 proof-of-work accumulated across all events | nonce tags | | Vouch | Inbound vouches from other identities, weighted by voucher depth with sqrt dilution | kind:33335 | | Economic | Lightning zaps received and sent, scored on unique senders/recipients | kind:9735, kind:9734 |
v0.7.0 retired the cyberspace-spatial dimension. Kind:3333 movement events are grind-cheap and Sybil-trivial; the dimension was marketing, not measurement.
scoreSpatialremains exported for backwards compatibility. Passdimensions: ['spatial', 'social', 'access', 'vouch', 'economic']to opt back in; expect a one-timeconsole.warnper process.
API
getIdentityDepth(pubkey, options?)
Returns a full identity report for a hex pubkey.
Options:
| Option | Default | Description |
|--------|---------|-------------|
| relays | ['ws://localhost:3088'] | Array of relay WebSocket URLs to query |
| timeout | 5000 | Query timeout per relay in ms |
| dimensions | ['social', 'access', 'vouch', 'economic'] | Which dimensions to score. v0.7.0 retired 'spatial' from the default. Add it back explicitly to opt into the deprecated cyberspace-spatial read; a one-time console.warn will fire. |
Returns:
{
"pubkey": "93da4435...",
"totalEvents": 3,
"weight": 24,
"activeDimensions": 2,
"dimensionMultiplier": 1.2,
"dimensions": {
"social": { "notes": 0, "reactions": 0, "uniquePeers": 0, "score": 0 },
"access": { "totalEvents": 3, "totalPowBits": 0, "maxDifficulty": 0, "score": 3 },
"vouch": { "inboundVouches": 0, "uniqueVouchers": 0, "totalWeight": 0, "score": 0 },
"economic": { "inboundZaps": 0, "outboundZaps": 0, "uniqueSenders": 0, "score": 0 }
},
"firstActivity": "2026-04-12T17:25:47.000Z",
"lastActivity": "2026-04-13T21:01:16.000Z",
"relaysQueried": 1
}queryRelay(relayUrl, filters, timeout?)
Low-level function to query a single relay for events matching NIP-01 filters.
How Scoring Works
Each dimension uses log2 scaling to prevent grinding attacks. You can't just spam events to inflate your score -- each additional unit of work yields diminishing returns, just like real proof-of-work.
- Social:
log2(notes+1) * 3 + log2(reactions+1) + log2(bidirectionalPeers+1) * 15 + log2(unidirectionalPeers+1) * 5 - Access:
totalPowBits * 2 + log2(powEvents+1)(PoW bits are already exponential, so they stay linear) - Vouch:
log2(totalWeight+1) * 3 + log2(uniqueVouchers+1) * 20(sqrt dilution prevents vouch farming) - Economic:
log2(genuineSenders+1) * 12 + log2(genuineRecipients+1) * 8 + log2(satsReceived+1) + log2(satsSent+1) * 2
A dimension multiplier rewards spreading across dimensions: having depth in 2 dimensions gives 1.2x, 3 gives 1.4x, all 4 gives 1.6x. One-trick ponies get no bonus.
The total weight is the sum of all dimension scores times the multiplier. Higher weight means more accumulated, irreversible work.
Why Not Just Check Follower Count?
Follower counts are trivially faked. This SDK measures things that cost real resources to produce: computation (PoW), time (event history), Lightning payments (sats), and social commitment (vouches backed by depth). Every dimension requires irreversible expenditure to increase.
Chaintip Freshness (beta)
Starting in 0.7.0-beta.1, the SDK ships an optional Bitcoin chain-tip anchor
you can attach to a signed DoI score. The field is experimental, opt-in, and
backwards-compatible. Scores without it still verify exactly as before.
What it adds: { height, hash, observed_at } under the signer's payload, where
height and hash come from the current Bitcoin tip and observed_at is the
ISO-8601 time of the fetch. Honest claim: block height plus hash as a freshness
anchor, verifiable against any public Bitcoin node. This is not SPV, not a
Merkle proof, and not a timestamp service. It tells a consumer "this score was
measured after block N" — nothing more.
How to opt in
const { getChaintip, signWithChaintip } = require('@powforge/identity');
// Fetch the current tip directly.
const tip = await getChaintip();
// => { height: 946252, hash: '00000000...', observed_at: '2026-04-23T...' }
// Or wrap any signer to fold the tip into the signed payload.
const { payload, signature } = await signWithChaintip(
scorePayload,
(p) => mySigner(p),
{ includeChaintip: true },
);On the hosted oracle, pass ?chaintip=1 on any score endpoint:
curl -s -X POST 'https://identity.powforge.dev/l402/identity-score?chaintip=1' \
-H 'Content-Type: application/json' \
-d '{"pubkey":"<64-hex>"}'Limitations
- Data source: mempool.space public API. Subject to their rate limits and
uptime. If mempool.space is down, the score still ships, and the envelope
carries
bitcoin_tip_errorinstead ofbitcoin_tipso callers can retry. - Cache window: 60 seconds in-process. Two calls within a minute get the same tip, which is fine because Bitcoin blocks land every 10 minutes on average.
- No chain verification: we forward what mempool.space reports. A consumer who wants higher assurance should cross-check the returned hash against any Bitcoin node they already trust.
Upgrade path
When self-hosted Bitcoin RPC credentials are wired up, the mempool.space fetch
swaps for a direct bitcoind JSON-RPC getblockchaininfo call. The public SDK
surface does not change. See the TODO(bitcoind-rpc) marker in
src/chaintip.js for the exact hook point.
License
MIT
