Mask.ID Protect your identity. Prove your reputation.

The Mask.ID Attestation API

Let your users prove they are established, reputation-backed people — no email, no phone number, no bots. Your site receives the member’s Trust Index and, with their permission, the reputation evidence behind it. You never learn who they are on Mask.ID.

Two ways to integrate

Aspect Log in with Mask.ID Verify with Mask.ID
What it does A passwordless sign-in. Accounts keyed by a pairwise ID, each with a pairwise shielded address you can pay, plus a one-word standing (established / new) to hold newcomers in probation. Trust Index + consented reputation data, re-readable while the member allows it.
Your site receives subject_id, shield_address, kind and standing — no Trust Index, no data subject_id + Trust Index + the packs you ask for
The member sees One confirm click A consent screen with every value they would disclose
Standing API access None — the subject endpoints 404 Re-reads and write-back until revoked
Price Free for every site, forever. No key. Free during the pilot; paid tiers when keys ship (below)

The same member gets the same subject_id on your site from both flows, so a site can start with free Login and add Verify later without touching its accounts.

Log in with Mask.ID — the whole integration

  1. Mint a single-use nonce, bind it to the visitor’s browser session, and send them to https://app.mask.id/login?c=<base64url(request)>:
    {
      "v": 1,
      "type": "login",
      "nonce": "d41d8cd98f00b204e9800998",
      "redirect": "https://your-site.example/maskid/return",
      "expires": 1765588800
    }
    No rp_user_id, no scopes — a login request that asks for data is refused. So is a redirect with a #fragment or a port.
  2. Recommended: publish the paths you return to at https://your-site/.well-known/maskid-redirects as {"v": 1, "redirects": ["/maskid/return"]}. Mask.ID then refuses any request for your host (Login or a Verify redirect) that returns anywhere else, so an open redirector on your site can’t be used to carry a member’s sign-in off to someone else. No file means no restriction.
  3. The member confirms on Mask.ID and returns to your redirect with ?maskid=<payload>.<signature>. The payload:
    {
      "v": 1,
      "type": "maskid.login",
      "subject_id": "msub_k5r2…",
      "shield_address": "ps1…",
      "kind": "individual",
      "standing": "established",
      "rp_host": "your-site.example",
      "nonce": "d41d8cd98f00b204e9800998",
      "issued_at": "2026-10-25T14:02:09Z",
      "expires_at": "2026-10-25T14:07:09Z"
    }
  4. Verify the Ed25519 signature over the decoded bytes (the same key and routine as attestations), then check type, rp_host, your nonce (unused, and from this browser), and expires_at (two minutes). Find or create the account by subject_id and start your own session. You never learn a name — assign one or ask. Keep shield_address with the account: it is a shielded PIVX address made for your site alone, the same one on every sign-in, so you can refund, pay out, or let the member withdraw to it without ever asking for an address. standing is established once the member has cleared the real-person hurdle and new until then — hold new accounts in probation and let the next sign-in lift it. kind says individual or organization.

Test your verifier: add &test=1 to your deep link and the confirm screen offers deliberately broken answers — bad signature, wrong type, wrong site, expired. Your site should accept the valid one once and refuse everything else, including the same answer a second time or in another browser.

Quick start: maskid-example-login.js (Node 18+, zero dependencies) is the whole thing in one file: button, nonce binding, verification, accounts, log out. Run it, open localhost:8080, sign in.

The button (copy-paste HTML)
<a href="https://app.mask.id/login?c=BASE64URL_REQUEST"
   style="display:inline-flex;align-items:center;gap:.55rem;background:#4530b3;color:#fff;
          padding:.6rem 1rem;border-radius:9px;text-decoration:none;font:600 15px system-ui">
  <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor"
       stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
    <path d="M3 7c0-1 1-2 2-2h14c1 0 2 1 2 2v5c0 5-4 8-9 9-5-1-9-4-9-9V7z"/>
    <path d="M8 11.5c.8 1 1.7 1.5 2.5 1.5M13.5 13c.8 0 1.7-.5 2.5-1.5"/>
  </svg>
  Log in with Mask.ID
</a>

Verify with Mask.ID — the Attestation API

Quick start: download the single-file example relying party — maskid-example-rp.js (Node 18+, zero dependencies). Run node maskid-example-rp.js, open localhost:8080, and receive your first verified attestation in redirect mode with nothing publicly hosted. It implements the entire verify procedure documented below.

The privacy model, in three rules

  1. Pairwise identity. You receive a subject_id minted for your site alone. Two sites can’t correlate their users; nothing about the member is recoverable from it.
  2. Ranges, not records. Sensitive quantities arrive as buckets — "1k–10k PIV", "active within 30 days", "25+" — never exact amounts, dates, or ranks that could single a member out.
  3. Member-granted, member-revoked. Every data point beyond the Trust Index requires the member’s explicit consent, and any grant can be revoked at any time — your access ends that moment.

Scope packs

Your request's scopes list names the packs below by id; the member approves or unchecks each one at verification time. The Trust Index itself rides every grant — no scope needed.

Scope id Name Availability What the member is asked to share
profile Profile basics Live Broad account facts: profile type, that you cleared the real-person hurdle, account age and activity as ranges, Founding Member yes/no, invited yes/no.
referrals Referrals Live Aggregate referral strength: counts as ranges, your referrers' average Trust Index and verification rate. Never who they are.
reputation Trust Scores Live What your referrers said, averaged across all of them: real-person confidence, how well they know you, whether they'd recommend you. No individual answers.
presence Online Presence Live How verified your social footprint is: coverage score, verified platform count, crypto-key linkage. Not which accounts.
commitment Commitment Live Economic commitment as ranges: Trust Capital score, holdings and coin-age buckets, tip and username-market activity. Never exact amounts or addresses.
humanity Humanity Live Two "is this a person?" confidences and a roll-up: what your referrers witnessed (real-person and one-operator confidence, weighted up by video, audio or in-person contact) and how costly your profile would be to fake (real-person hurdle, oldest verified identity, key linkage, backing). Never who vouched or what any one of them said.
mno Masternode owner Live · Opt-in Reveal that you run a PIVX masternode.
location Location context Live · Opt-in Your region (continent only), language, and UTC offset — never your country or city.
platforms Verified platforms Live · Opt-in WHICH platforms you've verified (e.g. GitHub, X) — not your usernames on them. The combination is more identifying than the count alone.
competence Confirmed fields Live · Opt-in The fields of work you listed on your profile that at least 3 of your referrers confirmed, with the share who vouched. Never who vouched.
track_record Track record Live · Opt-in Counts of dealings that settled as agreed on other sites you've verified with — how many sites, how many people, and whether any were disputed. Never amounts, never which sites, never who.
pay_link Payment link Live · Opt-in Creates a private payment page for this site — a fresh shielded address just for them, behind a random link. People there can pay you directly; the site and its visitors never see your username, balance, or other addresses.

Data points

Each row names the scope that carries it. Live ships today. Live · Opt-in starts unchecked — the member must deliberately turn it on (the payment link is the one exception: it starts on, and the member may turn it off). Never available is a design guarantee, not a default: those rows can’t be requested, bought, or opted into.

Profile

The identity surface. Names, images, and free text never cross the API; dates and ranks travel only as ranges.

Data point Scope Availability Example
Trust Index (0–100) always Live "trust_index": 87
Trust Index tier — unproven · low · building · trusted (the platform's own bands) always Live "trust_index_tier": "trusted"
Profile type profile Live "profile_type": "individual"
Verified website (organizations only; the domain that vouches for the profile) profile Live "website": "acme.org"
Real-person hurdle cleared (anti-sybil gate) profile Live "earned_username": true
Account age profile Live "account_age": "1–2 years"
Recently active profile Live "active_within": "30 days"
Founding Member profile Live "founding_member": true
Invited by an existing member profile Live "invited": true
Masternode owner mno Live · Opt-in "mno_verified": true
Region / language / timezone location Live · Opt-in "region": "North America"
Username, Mask ID, display name, bio, avatar — Never available —
Member number, exact join date, exact last-seen time — Never available —

Referrals

Aggregates over the member's referral graph. The graph itself — who vouched for whom — is never exposed, in any direction.

Data point Scope Availability Example
Referrals pillar score (0–100) referrals Live "referrals_score": 72
Accepted referrals received referrals Live "referrals_received": "25+"
Accepted referrals given referrals Live "referrals_given": "10+"
Average Trust Index of referrers referrals Live "referrer_avg_ti": 64
Share of referrers with verified platforms referrals Live "referrers_verified_pct": 80
New referral accepted in the last 90 days referrals Live "referral_recent": true
Who referred whom — names, dates, notes, answers, connection paths — Never available —

Trust Scores

Averages of what referrers actually answered — computed only once ten or more referrers exist, so no individual answer is ever recoverable.

Data point Scope Availability Example
Overall Trust Score + tier reputation Live "trust_score": 78, "trust_tier": "Established"
Section scores — Confidence, Relationship, Identity, Reputation, Trust-with, Longevity reputation Live "identity_score": 84
“Real person” confidence (referrer average) reputation Live "real_person": 92
“Identity has stayed consistent” (referrer average) reputation Live "identity_consistent": 90
“The Mask ID belongs to them” confidence (referrer average) reputation Live "owns_user_id": 85
How well referrers know them (referrer average) reputation Live "familiarity": 80
In what capacity referrers know them (share per bucket; all-that-apply) reputation Live "capacity_mix": {"colleague": 40, "client": 60, "community": 30}
Still in contact this year (share of referrers) reputation Live "active_relationship_pct": 80
Known 5+ years and still in contact (share of referrers) reputation Live "long_standing_current_pct": 30
Would trust them with money (share of referrers who answered) reputation Live "trust_money_pct": 80
Would trust them with their home, vehicle or belongings (share) reputation Live "trust_property_pct": 70
Would trust them with the care of a person or animal (share) reputation Live "trust_care_pct": 60
Would trust their advice in their field (share) reputation Live "trust_advice_pct": 90
Would meet them alone in person (share) reputation Live "trust_meet_pct": 90
In contact weekly or more (share of referrers) reputation Live "frequent_contact_pct": 50
Have exchanged money or goods with them (share) reputation Live "transacted_pct": 60
Transactions went as agreed (share of those who transacted) reputation Live "settled_as_agreed_pct": 90
“One person controls this account” confidence (referrer average) reputation Live "sole_operator": 90
Respectful and civil (referrer average) reputation Live "conduct": 95
Responds and communicates clearly (referrer average) reputation Live "responsiveness": 85
Average familiarity / years known reputation Live "avg_years_known": "4 years"
Met in person / communicated directly (share of referrers) reputation Live "met_in_person_pct": 40
Video calls (share of referrers) reputation Live "video_calls_pct": 60
Audio calls (share of referrers) reputation Live "audio_calls_pct": 40
Completed a project together (share of referrers) reputation Live "project_together_pct": 50
Would recommend / would work with again (share) reputation Live "recommend_pct": 95
Professional reputation (referrer average) reputation Live "professional_rep": 88
Reliability as a person — shows up, keeps their word (referrer average) reputation Live "personal_reliability": 85
Commitment reliability — do they do what they say (referrer average) reputation Live "commitment_reliability": 85
Confirmed fields of work (≥3 referrers vouched; share who vouched) competence Live · Opt-in "competence": [{"code": "software.web", "domain": "Software & IT", "subdomain": "Web development", "endorsed_pct": 60}]
Any individual referrer's answers — Never available —

Online Presence

How much of the member's social footprint is cryptographically verified — never which accounts.

Data point Scope Availability Example
Coverage score (0–100) presence Live "presence_score": 75
Verified platform count presence Live "verified_platforms": 3
Cryptographic key linkage (Nostr / Vector) presence Live "crypto_linkage": true
Which platforms are verified platforms Live · Opt-in "platforms": ["github", "x"]
Oldest verified identity presence Live "oldest_identity": "5+ years"
Platform usernames, account names, profile URLs — Never available —

Trust Capital

Economic commitment behind the identity. Always ranges: an exact balance would identify the wallet on-chain, which is why exactness is never offered.

Data point Scope Availability Example
Trust Capital pillar score (0–100) commitment Live "capital_score": 61
Per-coin holdings + Coin Age, keyed by chain — native units, never FX commitment Live "capital": { "pivx": {"unit": "PIV", "commitment": "1k–10k", "coin_age_all": "10k–100k", "coin_age_6mo": "10k–100k", "coin_age_3mo": "1k–10k", "coin_age_30d": "100–1k"} }
Backing exists (referrers stake PIV behind them) commitment Live "backed": true
Tips received (distinct members) commitment Live "tipped_by": "5+ members"
Tips given (distinct members) commitment Live "tipped": "10+ members"
Parked Usernames held commitment Live "parked_held": "3+"
Usernames bought (lifetime) commitment Live "names_bought": "1+"
Usernames sold (lifetime) commitment Live "names_sold": "1+"
Which usernames — held, bought, or sold — Never available —
Exact balances, amounts, addresses, transactions — Never available —

Humanity

Is this a person? Two variables and a roll-up. Witnessed = people vouched. Costly = a bot could do this, but it wouldn't be cheap. Use human_confidence if you only want one number.

Data point Scope Availability Example
Witnessed (0–100): referrers' real-person and one-operator confidence, weighted up by video, audio or in-person contact. Absent below the 10-referrer threshold. humanity Live "human_witnessed": 90
Costly to fake (0–100): real-person hurdle, age of the oldest verified outside identity, cryptographic key linkage, PIV backing, and cross-site history (sites active in 90 days). Cost, not proof; available day one. humanity Live "human_costly": 55
Combined (0–100): Witnessed where it exists, else Costly capped at 60 so an unwitnessed profile never reads as confirmed. The one field to use if you want one. humanity Live "human_confidence": 90
A human/bot verdict — these are confidences, never a label — Never available —
Which referrers vouched, or what any one of them answered — Never available —

Cross-site

Reputation earned through this API itself — a network signal no single site can see alone.

Data point Scope Availability Example
Verified on other sites via this API commitment Live "attested_sites": "3+"
Other sites that re-read this member in the last 30 days (a re-read is a "session": the site checked them, typically at login). Never which sites. commitment Live "active_sites_30d": "1+"
Same, over 90 days — cross-site history is the best bot signal we hold. commitment Live "active_sites_90d": "3+"
Which sites those are — Never available —

Track record

Conduct reported by other sites the member verified with — dealings that settled as agreed, as counts of sites and people. Only sites with standing count, so a member's own fork moves nothing. Opt-in, and never an amount.

Data point Scope Availability Example
Sites with standing where dealings settled as agreed (a site earns standing by age and real members, so a member's own fork counts for nothing). track_record Live · Opt-in "settled_sites": "1+"
Distinct people dealt with, across those sites (a pair counts once per site). track_record Live · Opt-in "settled_counterparties": "5+"
Present (true) once at least five dealings settled and none were disputed; absent otherwise — never false. A negative only ever removes a positive. track_record Live · Opt-in "dispute_free": true
Amounts, prices, currencies, or totals — Never available —
Which sites, which counterparties, or when — Never available —
Disputes or flags as a number — a negative only removes a positive — Never available —

Payments

A verified payment endpoint, not payment processing. The pay link resolves to a fresh shielded address derived for your site alone; payments are peer-to-peer on-chain, and Mask.ID is never in the flow of funds.

Data point Scope Availability Example
Verified payment page (pairwise pay link) pay_link Live · Opt-in "pay_link": "https://app.mask.id/pay/hTgkxGkYyBWJow0KrTppBQ"
The member's wallet addresses or balances — Never available —
Any payment, amount, or transaction history — Never available —
The same pay address on any other site — links are pairwise, like subject IDs — Never available —

The full picture — every live field at once

A response with every pack and opt-in granted (generated from the same catalog the server builds payloads from; multi-value rows show a representative key). Real responses carry only the granted, statable fields.

{
  "v": 1,
  "type": "maskid.attestation",
  "key_id": "…", "subject_id": "msub_…",
  "rp_host": "your-site.example", "rp_user_id": "u_8347", "nonce": "…",
  "scopes": ["trust_index", "profile", "referrals", "reputation",
             "presence", "commitment", "humanity", "mno", "location",
             "platforms", "pay_link"],
  "trust_index": 87, "trust_index_tier": "trusted",
  "profile_type": "individual",
  "website": "acme.org",
  "earned_username": true,
  "account_age": "1–2 years",
  "active_within": "30 days",
  "founding_member": true,
  "invited": true,
  "mno_verified": true,
  "region": "North America",
  "referrals_score": 72,
  "referrals_received": "25+",
  "referrals_given": "10+",
  "referrer_avg_ti": 64,
  "referrers_verified_pct": 80,
  "referral_recent": true,
  "trust_score": 78, "trust_tier": "Established",
  "identity_score": 84,
  "real_person": 92,
  "identity_consistent": 90,
  "owns_user_id": 85,
  "familiarity": 80,
  "capacity_mix": {"colleague": 40, "client": 60, "community": 30},
  "active_relationship_pct": 80,
  "long_standing_current_pct": 30,
  "trust_money_pct": 80,
  "trust_property_pct": 70,
  "trust_care_pct": 60,
  "trust_advice_pct": 90,
  "trust_meet_pct": 90,
  "frequent_contact_pct": 50,
  "transacted_pct": 60,
  "settled_as_agreed_pct": 90,
  "sole_operator": 90,
  "conduct": 95,
  "responsiveness": 85,
  "avg_years_known": "4 years",
  "met_in_person_pct": 40,
  "video_calls_pct": 60,
  "audio_calls_pct": 40,
  "project_together_pct": 50,
  "recommend_pct": 95,
  "professional_rep": 88,
  "personal_reliability": 85,
  "commitment_reliability": 85,
  "presence_score": 75,
  "verified_platforms": 3,
  "crypto_linkage": true,
  "platforms": ["github", "x"],
  "oldest_identity": "5+ years",
  "capital_score": 61,
  "capital": {
    "pivx": {"unit": "PIV", "commitment": "1k–10k", "coin_age_all": "10k–100k",
             "coin_age_6mo": "10k–100k", "coin_age_3mo": "1k–10k", "coin_age_30d": "100–1k"}
  },
  "backed": true,
  "tipped_by": "5+ members",
  "tipped": "10+ members",
  "parked_held": "3+",
  "names_bought": "1+",
  "names_sold": "1+",
  "attested_sites": "3+",
  "active_sites_30d": "1+",
  "active_sites_90d": "3+",
  "settled_sites": "1+",
  "settled_counterparties": "5+",
  "dispute_free": true,
  "human_witnessed": 90,
  "human_costly": 55,
  "human_confidence": 90,
  "competence": [{"code": "software.web", "domain": "Software & IT", "subdomain": "Web development", "endorsed_pct": 60}],
  "pay_link": "https://app.mask.id/pay/hTgkxGkYyBWJow0KrTppBQ",
  "issued_at": "2026-08-14T15:02:09Z",
  "api": "…/api/attest/v1/subject/msub_…"
}

Edge cases your integration must expect

  • Absent fields are normal, not errors. A field is omitted when the member declined its scope, the value is unset, or a privacy floor withholds it. Code for presence, never for position or completeness.
  • "trust_scores": "gated" replaces the whole reputation pack while the member has fewer than ten referrers — informative in itself.
  • The subject read answers 404 identically for unknown, revoked, and malformed subjects — treat any 404 as “no longer verified,” and expect it the moment a member revokes.
  • Ban the subject_id, not the account. It is stable for a member on your site: a member who revokes and later re-verifies receives the same subject_id, so a ban you keyed on it holds across revoke-and-rejoin. Getting a fresh one takes a whole new Mask.ID identity — the cost your tier gate sets.
  • Bucket values are display text ("1k–10k PIV", "25+") subject to refinement — compare for presence or equality, never parse them.
  • Webhook delivery retries a few times; any 2xx from you stops it. Verify-then-store must be idempotent per nonce.

How verification works

  1. Your site shows a verification request (QR + copyable string) to a user who clicked “Verify with Mask.ID”:
    {
      "v": 1,
      "rp_user_id": "u_8347",
      "nonce": "d41d8cd98f00b204e9800998",
      "webhook": "https://your-site.example/maskid/callback",
      "redirect": "https://your-site.example/settings/verified",
      "scopes": ["trust_index", "reputation", "commitment"]
    }
  2. The user reviews exactly what you’ll receive and approves it with a wallet signature on their Mask.ID.
  3. Mask.ID POSTs a signed response to your webhook (and/or returns the user to your redirect URL carrying the same response):
    {
      "v": 1,
      "type": "maskid.attestation",
      "subject_id": "msub_k5r2…",
      "rp_user_id": "u_8347",
      "nonce": "d41d8cd98f00b204e9800998",
      "scopes": ["trust_index", "reputation", "commitment"],
      "trust_index": 87, "trust_index_tier": "trusted",
      "trust_score": 78, "real_person": 92, "recommend_pct": 95,
      "capital": {"pivx": {"unit": "PIV", "commitment": "1k–10k", "coin_age_all": "10k–100k"}},
      "issued_at": "2026-08-13T14:02:09Z"
    }
  4. You verify one Ed25519 signature against the published platform key, store the subject_id, and may re-read the granted data while the member’s permission stands.

Endpoints

Endpoint What it does
GET /.well-known/maskid-attest-key The Ed25519 verification key to pin. Currently key_id cdde5c8d68dbb162, public key 5REJlRin5SL5voivThSqUAqup7StIEpqauTQ38iOWgc.
Nostr (no endpoint) For members who opt in, Mask.ID’s Nostr key npub1uhq0ye3a9pjgml864ml38lenthgeuq2z0l3rh2pspl6wqrv8qrjq2mcrhf (NIP-05 _@mask.id) publishes their Trust Index tier as a NIP-85 kind 30382 assertion (d = the member’s pubkey; tags tier, username, NIP-32 label mask.id) plus NIP-58 badges for Building and Trusted. Read it from any relay; verify the signature against that key. Tier only — no number, no addresses.
GET /api/attest/v1/subject/:subject_id Re-read a verified member’s granted data. Answers 404 once the member revokes — and always for login-only subjects. Rate-limited; cache and poll gently.
POST your webhook Revocation notice. When a member revokes, Mask.ID POSTs a signed maskid.revocation payload carrying the subject_id to the webhook the grant was made with — same signature check as the attestation, so branch on type. Retried for about a day; sites without a webhook see the 404 above instead. A Log in with Mask.ID request may name a webhook too (same host as its redirect): it gets this notice and nothing else, as the cue to end that member’s sessions.
POST /api/attest/v1/subject/:subject_id/events Optional write-back: report a count event for a verified member — session, transaction_settled, transaction_disputed or flagged — with a single-use nonce and, for dealings, the other party’s subject_id as counterparty. Answers 204. Counts only: no amounts, names or free text. 404 for revoked and login-only subjects.
GET /login?c=<request> The member’s confirm page for Log in with Mask.ID. Redirect-only; the assertion rides the member’s browser.

API keys & pricing

Log in with Mask.ID is free for every site, forever — no key, never metered. The rest of this page is Verify with Mask.ID: today, during the pilot, it is free and keyless — verification, webhooks, and the subject read. When API keys ship, pricing will meter monthly active verified members. The tier schedule, discounts, founding-site terms, and the free community tier live on the pricing page. Building against the API? We’d like to hear from you (Contact form in the footer) — early integrators get the founding-site terms.

Are you a Mask.ID member? The other half of this flow is yours: Verify Trust Index.