Skip to content

DecideID Verification

DecideID is one of four verification methods on ohshii_governance, and it is now a legacy one. It carries Strong strength and every existing DecideID verification remains valid indefinitely — nothing here is being taken away from anyone who already holds it.

What changes is intake: the provider no longer accepts new proofs since 2026-09-05 (accepts_new_proofs = false). A wallet that wants to become verified uses Google (Standard) or Telegram (Strong). The configuration stays on-chain and is listed in §1, together with the two proposals that would re-activate the provider.

DecideID has never powered the veto-override round and will not; that round needs a per-account anchor, which a per-wallet verification cannot supply. See Capture Resistance.

DecideID (DecideAI Proof-of-Personhood) is one of four identity-verification methods on ohshii_governance, and one of the three Strong ones. Every method is independent: each has its own DAO-controlled config, its own per-method verified set, and its own flags. A user verified via any method satisfies the standard gate; a critical action needs a Strong method.

This document is the developer and DAO-voter reference for the DecideID integration. The parallel provider docs are WORLD_ID_VERIFICATION.md, GOOGLE_VERIFICATION.md and TELEGRAM_VERIFICATION.md; the model that composes them is in GOVERNANCE.md.

Stateissuer_canister_idenabledEffect on ONS votingWhat the user sees on /verify
Not configured2vxsx-fae (anonymous)eitherNo DecideID gate. (Another provider may still gate.) Verification flow refuses to start.Dimmed DecideID method card with the badge “Not configured”. CTA disabled.
Optionala real principalfalseNo DecideID gate. (Another provider may still gate.) Verification is still possible — useful when the DAO wants to A/B test the flow before flipping it required.Lit DecideID card, badge “Optional”, CTA reads “Verify with DecideID (optional)”.
Requireda real principaltruevote() rejects unless is_any_method_verified(caller) returns true.Lit DecideID card, badge “Required for voting”, CTA reads “Start DecideID verification”.

Important persistence rule: switching a method from Required/Optional back to Not Configured does NOT clear existing verifications. Once a principal is in DECIDEID_VERIFIED_USERS, the flag stays until it is cleared via admin_clear_decideid_verified, executed by ohshii_governance through an approved proposal (and, during the transition phase, also by the deploying principals). The gate predicate is_any_method_verified in memory.rs reads the verified-user sets, not the enabled flags.

accepts_new_proofs — the retirement flag

Section titled “accepts_new_proofs — the retirement flag”

DecideIdConfig carries a second, independent flag:

FlagQuestion it answers
enabledDoes this provider make the verification gate mandatory?
accepts_new_proofsWill this provider still register a new proof?

The three states in the table above are all states of enabled; accepts_new_proofs cuts across them. true — the state today, and what a configuration predating the flag resolves to — means everything on this page is live. false is the retired state: every entry point that would register a new proof refuses with a message saying so, in both the VC and the OIDC mode, and everyone already verified stays verified with Strong strength. Retirement removes intake, never standing.

The flag is flipped by set_decideid_accepts_new_proofs, executed by ohshii_governance through an approved proposal (and, during the transition phase, also by the deploying principals). The tier depends on the direction: reopening intake (true) runs the critical route and is guardian-vetoable, because every open provider is another way for one person to enter the electorate again on a second wallet; closing intake (false) runs the standard route, so the defensive move stays fast. The positional configuration setter preserves the flag by construction, so changing any other field cannot silently reopen or close intake.

Important gate semantics — what happens when EVERY method is disabled: the gate composes the enabled flags of all four providers in one place. With every flag false, nothing is mandatory and ONS voting becomes open to any authenticated wallet — no verification required, no per-method check. Anonymous principals are still rejected by the reject_anonymous guard on vote(). All other voting rules (rate limit, voting power, proposal status) still apply. Concretely: a DAO that turns off every gate is choosing to remove the Sybil filter altogether; a DAO that wants to keep voting paused without lifting the filter must leave at least one method enabled = true and use the per-provider intake controls to block new verifications.

Combined state truth table, for an unverified caller on a standard proposal. S is any Strong provider (World ID, DecideID, Telegram); G is Google:

Any S.enabledG.enabledONS vote() on a standard proposalONS vote() on a critical proposal
falsefalseAllowed — nothing is mandatoryAllowed — nothing is mandatory
falsetrueRejected unless verified by any methodRejected unless verified by any method
trueeitherRejected unless verified by any methodRejected unless verified by a Strong method

The row that surprises people is the second one: with only Google enabled, a critical proposal needs only Google. There is no Strong provider available to demand, and demanding one anyway would make the critical tier unreachable.

In every gated row, what answers “is this caller verified?” is the union over the verified-user sets — the per-method enabled flags are ignored for that answer (persistence-on-disable). A caller verified via World ID passes a DecideID-only gate, and vice versa.

State transitions:

  • Not Configured → Optional / Required: ONS proposal set_decideid_config(<issuer principal>, "<origin>", "<credential_type>", "<derivation_origin>", true|false, 300).
  • Optional ↔ Required: same proposal, flipping enabled.
  • Emergency pause: ONS proposal set_decideid_daily_cap(0) halts new verifications without touching enabled or any existing verifications.
  • Rollback to Not Configured: ONS proposal set_decideid_config(2vxsx-fae, "", "", "", false, 300) returns the integration to dormant. Existing verified users keep voting access.

The record below is the mainnet configuration as read with the anonymous query on 2026-09-05, the day the provider was closed to new proofs. Mainnet runs the OIDC path; the VC rows further down describe the alternative path, which mainnet does not run. None of these values is a secret — the OIDC client secret is a separate cell that no query returns. Read the live record at any time:

icp canister call xnsdw-yaaaa-aaaal-qsqua-cai get_decideid_config '()' -n ic --query --identity anonymous -o candid
FieldValueNotes
issuer_canister_idqbw6f-caaaa-aaaah-qdcwa-caiDecideAI’s issuer canister.
issuer_originhttps://id.decideai.xyzNo trailing slash on mainnet: the OIDC path compares it to the token issuer; the trailing-slash rule below applies to the VC path only.
credential_typeProofOfPersonhood
derivation_originhttps://v5mnw-oaaaa-aaaal-qsica-cai.icp0.ioNative canister URL — DNS-renewal-independent.
modevariant { Oidc }The OAuth 2.0 path (see §13).
enabledtrue until the switch-off step, then falseEngages the gate; does not affect who is already verified.
accepts_new_proofsunset (accepting) until 2026-09-05, then falseThe retirement flag.
presentation_max_age_seconds0No extra freshness bound beyond the token’s own.
oidc_expected_issuerhttps://id.decideai.xyz
oidc_client_id = oidc_expected_audience36f24e91-5686-4f42-aa38-50f9c024bdafA public OAuth client id, not a secret.
oidc_authorize_urlhttps://id.decideai.xyz/#/authorize
oidc_token_urlhttps://rlz47-aqaaa-aaaah-qdcra-cai.icp0.io/token
oidc_userinfo_urlhttps://rlz47-aqaaa-aaaah-qdcra-cai.icp0.io/userinfo
oidc_jwks_urlhttps://rlz47-aqaaa-aaaah-qdcra-cai.icp0.io/.well-known/jwks.json
oidc_redirect_urihttps://v5mnw-oaaaa-aaaal-qsica-cai.icp0.io/decideid-callback
vc_minimum_verification_date""VC path only. Empty → arguments: None.
vc_minimum_reputation_level""VC path only. Accepted values when set: "gold" or "silver".

Retired to new proofs on 2026-09-05 — the configuration stays on-chain

Section titled “Retired to new proofs on 2026-09-05 — the configuration stays on-chain”

On 2026-09-05 the provider was closed to new proofs (set_decideid_accepts_new_proofs(false)), and its gate flag is switched off once Telegram and Google are enabled (set_decideid_config with enabled = false) — in that order, so the gate never drops in between. Neither step erases anything: set_decideid_config is a read-modify-write that replaces only its six positional fields and keeps the OIDC fields, the mode and the retirement flag, and the verified set is a separate store that no configuration change touches.

Re-activating the provider takes two approved ONS proposals (during the transition phase, the deploying principals can also call both setters directly), and neither needs a value that is not in the table above or in the query:

  1. set_decideid_accepts_new_proofs(true) — a CriticalGovernanceOperation proposal reopens intake. Reopening is refused on the standard ExecuteAdminMethod route; only closing (false) travels it.
  2. set_decideid_config(principal "qbw6f-caaaa-aaaah-qdcwa-cai", "https://id.decideai.xyz", "ProofOfPersonhood", "https://v5mnw-oaaaa-aaaal-qsica-cai.icp0.io", true, 0) — a CriticalGovernanceOperation proposal makes the gate mandatory again. The setter is positional, so all six values are passed again with enabled as the one that changes.

The cfg field issuer_origin is stored with a trailing slash because two consumers need the string in different forms:

ConsumerRequired formFailure if wrong
Backend validate_ii_presentation_and_claims"https://id.decideai.xyz/" (slash)Validator returns InconsistentCredentialJwtClaims("inconsistent claim in VC").
Frontend requestVerifiablePresentation SDK call"https://id.decideai.xyz" (no slash)Issuer canister’s derivation_origin API rejects with UnsupportedOrigin.

The frontend modal applies .replace(/\/+$/, '') before passing the value to the SDK. Backend reads cfg verbatim. One source of truth, two readers, asymmetric handling isolated to the frontend.

The configuration pins derivation_origin to the native canister URL rather than a custom domain. This makes verification independent of custom-domain DNS / TLS renewals. DecideAI’s id.decideai.xyz advertises a matching ii-alternative-origins document that lists the canister URL, so the II alias-derivation path is consistent.

Implication: VC verification only succeeds when the user’s browser runtime origin matches the configured derivation_origin. Users on custom domains (e.g. launcher.ohshii.io) are bounced to the canonical native canister origin by the modal’s cross-origin guard before the flow starts.

Optional arguments (reputation gate, verification-date floor)

Section titled “Optional arguments (reputation gate, verification-date floor)”

The ProofOfUniqueness credential supports two optional arguments:

ArgumentTypeSemantics
minimumVerificationDateISO 8601 stringReject presentations whose underlying user verification predates this date.
minimumReputationLevel"gold" | "silver"Two-tier reputation gate. A user with a higher tier still passes a lower-tier check.

Both are optional. Empty cfg fields produce arguments: None. When set via set_decideid_vc_arguments, the same values are forwarded by the frontend into the SDK request and verified verbatim by the backend — per DecideAI’s security note that requested and verified values must match.

  1. User connects to OhShii with Internet Identity 2.0, the recommended sign-in method (Plug Wallet is still available but not recommended, and is hidden from the sign-in window unless the page is opened with ?wallets=all; NFID Wallet sign-in is not offered). See the “Wallets and Sign-in” user guide.
  2. User opens /verify and clicks Start DecideID verification.
  3. DecideIDVerificationModal reads get_decideid_config() from canister and calls requestVerifiablePresentation from @dfinity/verifiable-credentials.
  4. The library opens an Internet Identity popup. II authenticates the user, then opens the DecideID flow as the issuer (II → DecideID is a credential-fetching round-trip; OhShii is not in this loop).
  5. DecideID issues a JWT credential signed with its canister signature. II wraps it together with an id_alias credential (signed by II) into a Verifiable Presentation (JWT-VP).
  6. The popup closes; the JWT-VP is delivered to OhShii’s frontend via onSuccess.
  7. The frontend forwards the raw JWT-VP to register_decideid_verification(jwt_vp) on canister.
  8. Canister validates synchronously (no inter-canister calls), records dedup digest, marks caller in DECIDEID_VERIFIED_USERS, and writes a verification event to EVENT_LOG with empty details.
  9. Frontend re-fetches am_i_verified(); the universal banner flips to “Verified”.

On canister, OhShii stores:

  • Per-principal verified = true flag in DECIDEID_VERIFIED_USERS (no timestamp, no method-source metadata).
  • 32-byte SHA-256 of the JWT-VP in USED_DECIDEID_PRESENTATIONS (dedup; cannot be reversed to recover the JWT).
  • DecideIdConfig (issuer canister id, origin, credential type, the launcher’s derivation origin, enabled, max age) — public, returned by get_decideid_config().
  • A verification event in EVENT_LOG with empty details — caller is NOT logged.

On canister, OhShii does NOT store:

  • The JWT-VP itself.
  • The id_alias credential payload.
  • Any DecideAI-side principal, timestamp, or biometric metadata.
  • A timestamp of the verification (would let observers time-correlate).

Unlinkability across relying parties is preserved by Internet Identity’s id_alias mechanism: DecideAI sees a per-(user, RP) alias that is different on every RP. DecideAI cannot correlate “user X on OhShii” with “user X on OpenChat”. OhShii cannot correlate either.

Per-method exposure on canister: the per-method introspection queries (is_world_id_verified, is_decideid_verified) are caller-only — they reject any call where the queried principal does not match msg_caller(). External enumeration of which provider a third-party principal used is not exposed. The universal gate query is_verified(p) returns only yes/no without provider information; that is the value used by the vote gate (see §9 Frontend integration).

DecideID’s anti-sybil guarantee comes from two layers:

  1. DecideAI’s biometric uniqueness check (off-canister, off-OhShii). DecideAI verifies the user via biometric/document and refuses to issue a second credential to the same natural person. This is the upstream invariant OhShii relies on; the canister does not enforce it itself.
  2. Alias-tuple binding (on-canister, in validate_ii_presentation_and_claims). The id_alias credential commits to (caller’s RP-side principal, RP origin, alias principal). Validation rejects if the JWT’s effective_vc_subject does not match msg_caller(), so user A’s JWT cannot be replayed by user B.

Defense-in-depth: the canister additionally hashes the JWT-VP (SHA-256 → USED_DECIDEID_PRESENTATIONS) and reject any duplicate digest. This catches a class of bugs where a future change might weaken the alias-tuple binding.

What this does NOT prevent: a single user controlling multiple Internet Identity anchors (and getting DecideAI to verify each one separately) could in theory verify multiple OhShii principals. DecideAI’s published one-biometric-one-credential rule is the upstream defense; if DecideAI fails it, OhShii inherits the gap.

is_verified is the union over all four providers (is_any_method_verified), so completing DecideID is fully sufficient for every gate that asks for verification — a DecideID-only user is treated identically to a World-ID-only user everywhere, and DecideID additionally carries Strong strength, so it also satisfies the gates a Google-only user cannot reach (critical proposals, the LGE purchase-tier ladder, the veto-override ballot). In particular, the Sponsor/referral system is verification-gated on this same OR: a sponsor↔user referral connection is only persisted on-canister, and a sponsor only earns, once the invited user is verified by either method. Until then the referrer code stays pending client-side (no canister storage) and the referral reward stays with the OhShii treasury. The DecideID side carries the same attribution trigger as World ID — a successful register_decideid_verification confirms a pending referral (the same piggyback_attribute_sponsor path register_world_id_verification uses). The check is made at purchase time and is not retroactive to that buyer’s earlier unverified purchases. See SPONSOR_SYSTEM.md → Referral registration & verification gating and the symmetric note in WORLD_ID_VERIFICATION.md §4.

⚠ SCOPE — read this before the section below. Everything in section 5 describes the Verifiable-Credential (VC) path. DecideAI has discontinued that flow in favour of OIDC; it is a product decision on their side.

The live path is OIDC, and one property below does NOT carry over to it. Section 5 states “No HTTPS outcalls. No threshold signing. No external dependencies from ohshii_governance.” That is accurate for VC verification, which is fully on-canister. The OIDC path does make HTTPS outcalls — /token, /.well-known/jwks.json and /userinfo — and uses threshold signing for the RP assertion. Section 5’s guarantees do not apply to the OIDC flow.

Section 5 is kept because the mechanism is real, the vendored crate is still in tree, and the checks it documents are the ones a VC-based integration would perform. Read it as a description of that mechanism, not of what runs today.

Verification of the credential on the OhShii side is fully synchronous on-canister. No HTTPS outcalls. No threshold signing. No external dependencies from ohshii_governance. The pipeline lives in decideid_verification.rs and wraps the ic_verifiable_credentials crate (vendored under src/external_crates/, see VENDORED.md).

Important scope clarification: “no off-chain server” applies only to the canister-side verification of the JWT-VP. The underlying biometric check that DecideAI performs to issue the credential in the first place runs on DecideAI’s own off-chain infrastructure (face capture, liveness detection, document validation). Per the DecideAI Whitepaper, only a Zero-Knowledge proof of the result is then attested on-canister:

“The use of Zero-Knowledge (ZK) proofs means that in this process, the user’s credentials can be verified safely without the risk of sharing private information (e.g. biometric data). […] Decide ID sends back ZK proof — an encrypted version of the biometric and the verifier hash.”

So OhShii never receives biometric data; the canister only verifies a ZK-attested Verifiable Credential. But the privacy claim “fully on-chain” applies to the OhShii verification step, not to DecideAI’s biometric pipeline.

What validate_ii_presentation_and_claims checks:

  1. JWT-VP is well-formed (id_alias credential + issuer credential, both as JWS).
  2. Both credentials’ canister signatures verify against the IC root key (fetched via ic_cdk::api::root_key(), DER prefix stripped via extract_raw_root_pk_from_der).
  3. effective_vc_subject == msg_caller() (alias-tuple binding).
  4. effective_derivation_origin == cfg.derivation_origin (RP-binding).
  5. Issuer credential’s credential_type == cfg.credential_type (issuer-binding).
  6. JWT exp claim is in the future relative to current_time_ns.

What the canister adds on top of the crate:

  • 16 KB cap on jwt_vp.len() to bound CPU and storage.
  • 32-byte SHA-256 dedup against USED_DECIDEID_PRESENTATIONS.

Reserved (NOT currently enforced):

  • cfg.presentation_max_age_seconds is a placeholder for a future wallclock-style freshness bound stricter than the JWT’s intrinsic exp claim. Today freshness relies solely on the validator’s internal current_time_ns < exp check — whatever TTL DecideAI puts on its credential is the actual ceiling. Setting a non-zero value has no runtime effect; the field is preserved in the Candid surface so adding the check later does not require a config migration. The missing check is non-exploitable in the current flow because: (a) same-caller idempotency short-circuits at is_decideid_verified(caller), and (b) cross-caller alias-tuple binding inside the validator rejects user A’s JWT submitted as user B.

Local testing: DecideID’s issuer canister exists only on mainnet, so a local replica cannot run the real flow without a stub issuer canister.

LimitDefaultWhere enforcedPurpose
Per-call max JWT length16 KBregister_decideid_verification early-returnBounds memory + hashing cost per call
Per-caller register attempts5 / hour, 30s cooldowncheck_decideid_op_rate_limit (heap, non-persistent)Primary DoS defense. Bounds the validation work any single principal can cause, including with distinct invalid JWTs that the digest dedup would not catch.
Daily cap on successful verifications1000 / 24hconsume_decideid_daily_cap_slot (stable memory)Canister-wide ceiling on successful registrations. Consumed AFTER validate() succeeds, so invalid JWTs cannot drain it.
Per-caller in-flight register lock1DecideIdRegisterLockGuard (RAII, in-memory, #[must_use])Defensive: the VC function is fully synchronous, so concurrent calls from the same principal cannot interleave; the lock keeps that property for any flow that awaits.

Order of defenses in register_decideid_verification (matters for DoS posture):

1. Reject anonymous (guard)

2. Idempotency short-circuit (already DecideID-verified → no-op)

3. Read DecideIdConfig, reject if Not Configured

4. DecideIdRegisterLockGuard

5. Per-caller rate limit ← bumps counter regardless of outcome

6. JWT size cap (16 KB)

7. SHA-256 dedup against USED_DECIDEID_PRESENTATIONS

8. validate_ii_presentation_and_claims (BLS crypto)

9. consume_decideid_daily_cap_slot ← only after validate succeeds

10. record digest + mark verified + log event (atomic, stable writes)

The critical ordering is 5 BEFORE 8 BEFORE 9: per-caller limit caps CPU spent by one attacker; cap consumed only on successful validation prevents canister-wide cap drain by invalid JWTs.

In VC mode DecideID has no HTTPS outcalls and no threshold-ECDSA signing, so the per-verification cycle cost is dominated by the BLS canister-signature checks inside validate_ii_presentation_and_claims — single-digit billion cycles per call, not the ~100 B+ that a World ID verification consumes. The daily cap default is correspondingly higher (1000 vs. World ID’s 500).

ONS proposal types (all ExecuteAdminMethod, all target ohshii_governance):

MethodArgsThresholdPurpose
set_decideid_configprincipal, text, text, text, bool, nat64Critical (→ CriticalGovernanceOperation)Full config update (issuer canister, origin, credential_type, derivation_origin, enabled, presentation_max_age_seconds). Refuses to enable while issuer is anonymous.
set_decideid_modevariant { Vc; Oidc }Critical (→ CriticalGovernanceOperation)Selects the verification path. Vc = synchronous on-canister JWT-VP validation (default, free, no outcalls). Oidc = OAuth 2.0 path with HTTPS outcalls (the path mainnet runs; see §13).
set_decideid_vc_argumentstext, textStandardSets vc_minimum_verification_date (ISO 8601) and vc_minimum_reputation_level ("" / "gold" / "silver"). Empty strings disable the constraint and produce arguments: None.
set_decideid_daily_capnat64StandardDaily-cap update; 0 pauses.

Important: the canister enforces that enabled cannot be true while issuer_canister_id == Principal::anonymous() — this prevents a misconfigured proposal from locking everyone out of voting (the gate would fire but no one could verify).

Across code upgrades: the configuration the DAO set is kept as it is. An upgrade whose code cannot read the stored configuration is refused, and the canister keeps running its previous code with the stored value untouched; while the value cannot be read, the configuration reads as disabled and closed to new proofs, and set_decideid_config refuses to write. The canonical default configuration is applied at most once per canister, by the first upgrade with this code and only if no configuration exists, so a configuration the DAO cleared back to “Not configured” stays cleared. The marker that records this is protected the same way: an upgrade whose code cannot read it is refused.

All of these admin_* methods carry is_admin_or_self_guard: they are reached by ohshii_governance self-calls (DAO proposal execution) and, during the transition phase, by the deploying principals — which are removed at the DAO handover, after which a proposal is the only route.

MethodEffect
admin_clear_decideid_verified(principal)Removes a principal from DECIDEID_VERIFIED_USERS. Returns true if the principal was previously verified.
admin_clear_decideid_presentation(jwt_digest_hex)Frees a recorded JWT-VP digest so the same JWT can be submitted again. Freeing a digest weakens replay protection for that JWT, which is why it is a governance action logged with its argument.
admin_reset_decideid_rate_limit(opt principal)Clears the heap per-caller rate-limit counter for one principal, or for everyone if null. The heap map is already cleared on upgrade; this is the no-upgrade equivalent.

The per-caller DecideIdRegisterLockGuard lives only in memory and is cleared on canister upgrade or when the in-flight register completes.

A client deciding whether a wallet may vote should use the universal am_i_verified() query, not the per-method is_world_id_verified / is_decideid_verified: am_i_verified() is the answer the canister’s own gate uses, and the per-method queries exist for badge display. The launcher’s VotePanel.jsx follows this rule, and the canister-side gate (is_any_method_verified in memory.rs) reads the verified-user sets directly and not the enabled flags.

CSP additions in .ic-assets.json5:

  • connect-src adds https://id.decideai.xyz, https://qbw6f-caaaa-aaaah-qdcwa-cai.icp0.io, and https://qbw6f-caaaa-aaaah-qdcwa-cai.raw.icp0.io.
  • frame-src adds https://id.decideai.xyz and https://qbw6f-caaaa-aaaah-qdcwa-cai.icp0.io.
  • https://identity.ic0.app was already present (II popup hosts the actual VC flow UI).

npm dep: @dfinity/verifiable-credentials@^1.0.0 (lazy-loaded by VotePanel.jsx via React.lazy, so the chunk is fetched on demand only when an unverified user opens the modal).

PropertyHow it’s guaranteed
Anonymous callers rejected#[ic_cdk::update(guard = "reject_anonymous")] on register_decideid_verification.
Caller binding (no cross-principal replay)effective_vc_subject = msg_caller() passed to validate_ii_presentation_and_claims; the alias-tuple binding inside the validator rejects mismatches.
JWT replay (same caller, same JWT)USED_DECIDEID_PRESENTATIONS dedup map; checked before the expensive crypto validation.
TOCTOU between dedup and commitregister_decideid_verification is synchronous (no .await boundaries). Dedup-check, validate, dedup-record, mark-verified are atomic from the IC scheduler’s perspective.
In-flight double-register from same principalDecideIdRegisterLockGuard (per-caller BTreeSet<Principal>, released in Drop so a callback trap still frees the lock). Named binding required at the call site (let _register_lock = ...).
Storable-bound panic on oversized input16 KB cap on jwt_vp.len() checked before any storage write.
Daily CPU exhaustion under spamDECIDEID_DAILY_CAP (default 1000) consumed atomically before validation.
Verification persists across method-disableis_any_method_verified reads the verified-user sets directly, not the enabled flags.
Privacy: no caller in event logThe verification event is logged with empty details.

Legacy. The provider is retired: intake has been closed (accepts_new_proofs = false) since 2026-09-05, which takes nothing away from anyone already verified (§1).

Pause levers (in order of severity):

  1. Rate-limit pause: ONS proposal set_decideid_daily_cap(0). Halts new verifications instantly. Existing verified users keep voting access.
  2. Retire intake: ONS proposal set_decideid_accepts_new_proofs(false). Refuses every new proof, in both VC and OIDC mode, permanently rather than by exhausting a cap, and leaves the gate contribution untouched. Existing verifications kept.
  3. Method disable: ONS proposal set_decideid_config(<same principal>, ..., enabled = false, ...). Drops the gate to Optional; new verifications still possible but the vote gate doesn’t fire from this method. Existing verifications kept.
  4. Full rollback: ONS proposal set_decideid_config(2vxsx-fae, "", "", "", false, 300). Returns to Not Configured. Existing verifications kept.

Levers 2 and 3 are the two axes and they are independent: retiring intake does not change whether the gate is mandatory, and disabling the gate does not close intake.

What you cannot pause without a governance action: a verified user retains voting access until their flag is cleared via admin_clear_decideid_verified (executed by ohshii_governance via an approved proposal) or the canister is downgraded.

ConcernFile
Vendored cratesrc/external_crates/ic-verifiable-credentials/
DecideIdConfig Rust typesrc/ohshii_governance/src/types.rs
Stable storage layoutsrc/ohshii_governance/src/memory.rs (MemoryIds 59–63 for VC core; 64–69 for OIDC parallel path)
VC verification logicsrc/ohshii_governance/src/decideid_verification.rs
OIDC verification logicsrc/ohshii_governance/src/decideid_oidc.rs
Public methods + vote gatesrc/ohshii_governance/src/lib.rs
Candid interfacesrc/ohshii_governance/ohshii_governance.did
Frontend modalsrc/frontend/src/components/Governance/DecideIDVerificationModal.jsx
OIDC callback routesrc/frontend/src/components/Governance/DecideIDOidcCallback.jsx
/verify pagesrc/frontend/src/pages/VerifyPage.jsx
Vote panel gatesrc/frontend/src/components/Governance/VotePanel.jsx
Proposal form entriessrc/frontend/src/components/Governance/CreateProposalForm.jsx
CSP allowancessrc/frontend/.ic-assets.json5
Bootstrap constantssrc/frontend/src/config/decideid.js

set_decideid_mode selects the verification path, through a critical proposal (§7):

  • Vc — synchronous on-canister validation of the JWT-VP (§5). No HTTPS outcalls.
  • Oidc — the OAuth 2.0 path, which makes HTTPS outcalls to DecideAI’s /token, /.well-known/jwks.json and /userinfo endpoints. This is the path mainnet runs; its configured endpoints are listed in §1.

Switching modes does not invalidate existing verifications — DECIDEID_VERIFIED_USERS is persisted independently of which path produced the entry. After any configuration proposal executes, get_decideid_config (an anonymous query, §1) shows the live values.

The id_token is opaque, by DecideAI’s design

Section titled “The id_token is opaque, by DecideAI’s design”

This page describes JWTs throughout, but the OIDC token DecideAI returns is not one. The shape is recorded here so the difference is not mistaken for a fault.

DecideAI’s /oauth/token endpoint returns id_token as a 36-character UUID (for example 698ff224-f6d5-46df-96dd-c6ae8819340c), not as a signed JWT (header.payload.signature in base64url). According to DecideAI this is intentional, because of the ECDSA signing cost per token.

What that means mechanically. The flow is OAuth 2.0 semantics wearing OIDC field names. An opaque token carries no claims and no signature of its own, so identity attributes cannot be read out of it — they are obtained from the provider’s /userinfo endpoint after the exchange. That is why the pipeline calls /userinfo rather than parsing the token, and it is not a workaround: it is the shape the provider ships.

What it does not mean. The JWT path described elsewhere on this page is not dead — it is the branch taken when a signed token IS returned, and it verifies the signature against the provider’s published keys. Both branches exist because the provider’s response shape decides which one runs.