Skip to content

DecideID Verification

DecideID (DecideAI Proof-of-Personhood) is the second identity-verification method on ohshii_governance, alongside World ID. The two methods are independent: each has its own DAO-controlled config, its own per-method verified set, and its own enabled flag. The voting gate fires whenever either method is Required, and a user verified via either method satisfies the gate.

This document is the operator + developer manual for the DecideID integration. The World ID parallel doc is WORLD_ID_VERIFICATION.md; read both.

Stateissuer_canister_idenabledEffect on ONS votingWhat the user sees on /verify
Not configured2vxsx-fae (anonymous)eitherNo DecideID gate. (World ID may still gate.) Verification flow refuses to start.Dimmed DecideID method card with the badge “Not configured”. CTA disabled.
Optionala real principalfalseNo DecideID gate. (World ID 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 an admin/DAO clears it via admin_clear_decideid_verified. This is enforced by the load-bearing comment on is_any_method_verified in memory.rs.

Important gate semantics — what happens when BOTH methods are disabled: the vote-gate condition is (world_id_cfg.enabled || decideid_cfg.enabled) && !is_any_method_verified(caller). With both enabled flags false, the first half of the AND is false, so the gate does not fire 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 both gates 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 (typically with set_decideid_daily_cap(0) and/or set_world_id_daily_cap(0) to also block new verifications).

Combined state truth table (World ID enabled / DecideID enabled / outcome for an unverified caller):

WorldID .enabledDecideID .enabledONS vote() for unverified caller
falsefalseAllowed — gate is off, anyone authenticated can vote
truefalseRejected unless caller is in VERIFIED_USERS OR DECIDEID_VERIFIED_USERS
falsetrueRejected unless caller is in VERIFIED_USERS OR DECIDEID_VERIFIED_USERS
truetrueRejected unless caller is in VERIFIED_USERS OR DECIDEID_VERIFIED_USERS

In every gated row, the OR over the verified-user sets is what answers “is this caller verified?” — 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 values below are the current production-aligned configuration. They are the same on staging and production; only derivation_origin differs (each environment pins to its own canister’s native URL).

FieldValueNotes
issuer_canister_idqgxyr-pyaaa-aaaah-qdcwq-caiDecideAI’s ProofOfUniqueness issuer; same canister hosts id.decideai.xyz.
issuer_originhttps://id.decideai.xyz/WITH trailing slash — backend compares verbatim against the JWT iss claim.
credential_typeProofOfUniquenessW3C credential type checked verbatim by the validator.
derivation_origin (staging)https://v5mnw-oaaaa-aaaal-qsica-cai.icp0.ioNative canister URL — DNS-renewal-independent.
derivation_origin (production)https://v5mnw-oaaaa-aaaal-qsica-cai.icp0.ioSame strategy, production canister.
modevariant { Vc }Selects synchronous canister-side validation.
enabledtrueEngages the OR-gate in vote().
vc_minimum_verification_date""Empty → arguments: None. Set via set_decideid_vc_arguments only if the DAO wants to enforce a floor.
vc_minimum_reputation_level""Same as above. Accepted values when set: "gold" or "silver".

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.

We pin 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, NFID, or Plug.
  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, we store:

  • 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, our 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, we do 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 we rely on; we do not enforce it ourselves.
  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: we additionally hash 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 OR over World ID and DecideID (is_any_method_verified), so completing DecideID is fully sufficient — a DecideID-only user is treated identically to a World-ID-only user by every downstream gate. 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.

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; we only verify a ZK-attested Verifiable Credential. But the privacy claim “fully on-chain” applies to OUR 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 we add 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 replica caveat: DecideID’s issuer canister exists only on mainnet. Local-replica testing requires deploying a stub issuer canister against the local II (out of scope for v1). Recommendation: test in the Optional state on mainnet first, confirm the flow end-to-end, then flip to Required.

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. Without this, an attacker generates N distinct invalid JWTs (any one-byte tweak → unique digest → bypasses dedup) from a single principal and burns the canister-wide daily cap. Bounds blast-radius at 5 attempts/hour per principal.
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: today the function is fully synchronous, so concurrent calls from the same principal cannot interleave anyway. Becomes load-bearing if a future change introduces an await in the flow.

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.

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, nat64StandardFull 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 }StandardSelects the verification path. Vc = synchronous on-canister JWT-VP validation (default, free, no outcalls). Oidc = OAuth 2.0 path (parallel implementation, requires DCD; 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).

All admin methods carry the is_admin_or_self_guard and are callable both by the admin allow-list and by ohshii_governance self-calls (DAO proposal execution).

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. Misuse weakens replay protection — use only for integration testing or documented incident response.

There is no DecideID equivalent of admin_reset_world_id_rate_limit because DecideID has no per-caller rate-limit map; the per-caller DecideIdRegisterLockGuard lives only in memory and is automatically cleared on canister upgrade or after the in-flight register completes.

The vote-gate decision MUST use the universal am_i_verified() query, never the per-method is_world_id_verified / is_decideid_verified. This is enforced by:

  • Naming convention: am_i_verified() for gating, am_i_verified_via(method) for badge display.
  • Code comments in VotePanel.jsx explicitly forbidding per-method gating.
  • Load-bearing comment on is_any_method_verified in memory.rs explaining why the canister-side gate 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. Documented with load-bearing comment.
Privacy: no caller in event logThe verification event is logged with empty details.

Before flipping to Required:

  1. Deploy the canister with set_decideid_config proposal in Optional state.
  2. Confirm credential_type empirically: have a tester run a real verification against a DecideID account (any authenticated principal can do this), observe the canister logs (or get_recent_events) for a successful verification event vs. validation errors.
  3. If validation rejects with a CredentialMismatch-shaped error, iterate via another set_decideid_config proposal with the corrected string.
  4. Test the dual-CTA banner in VotePanel with an unverified principal (gate must offer both World ID and DecideID; CTAs must be disabled if the corresponding method is “Not Configured”).
  5. Test the “Verified” banner on /verify after a successful DecideID flow (am_i_verified() must return true; is_decideid_verified() must return true; is_world_id_verified() must return false unless the user also did World ID).
  6. Once confident, submit set_decideid_config(..., enabled = true, ...) to flip to Required.

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. Method disable: ONS proposal set_decideid_config(<same principal>, ..., enabled = false, ...). Drops the gate to Optional; new verifications still possible but vote-gate doesn’t fire from this method. Existing verifications kept.
  3. Full rollback: ONS proposal set_decideid_config(2vxsx-fae, "", "", "", false, 300). Returns to Not Configured. Existing verifications kept.

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

The commands below configure DecideID in VC mode, enabled = true (Required-for-voting). In production these are executed by ohshii_governance via an approved ONS ExecuteAdminMethod proposal. The direct-dfx commands below are a bootstrap/recovery path only — the method names and argument order are identical when wrapped in a proposal.

Always re-run set_decideid_config after every deploy that adds fields to DecideIdConfig. The stable-storage migration may lossy-reset the cell, and get_decideid_config is the only authoritative confirmation that the values are live.

Terminal window
# Verify you are on the secure_identity profile (bootstrap/recovery path; in production this config flows through an ONS proposal).
dfx identity whoami
# Expected: secure_identity
# Confirm the canister principal mapping for production.
grep -n "ohshii_governance" canister_ids.json
# Expected production: xnsdw-yaaaa-aaaal-qsqua-cai

Sets issuer, origin, credential type, derivation origin, and flips enabled = true in a single call.

Terminal window
dfx canister --network ic call ohshii_governance set_decideid_config '(
principal "qgxyr-pyaaa-aaaah-qdcwq-cai",
"https://id.decideai.xyz/",
"ProofOfUniqueness",
"https://v5mnw-oaaaa-aaaal-qsica-cai.icp0.io",
true,
0 : nat64
)'

Field-by-field:

PositionArgumentValueWhy
1issuer_canister_idqgxyr-pyaaa-aaaah-qdcwq-caiDecideAI’s ProofOfUniqueness issuer.
2issuer_originhttps://id.decideai.xyz/WITH trailing slash — required for backend JWT iss match.
3credential_typeProofOfUniquenessVerbatim W3C credential type.
4derivation_originhttps://v5mnw-oaaaa-aaaal-qsica-cai.icp0.ioProduction canister native URL (DNS-renewal-independent).
5enabledtrueEngages the OR-gate in vote(). Required for production.
6presentation_max_age_seconds0Reserved field, no runtime effect today.
Terminal window
dfx canister --network ic call ohshii_governance set_decideid_mode '(variant { Vc })'

Skip this step unless the DAO wants to enforce a verification-date floor or a reputation tier. Empty strings restore arguments: None (the OpenChat-equivalent default).

Terminal window
# No extra constraint (default; same as never calling it):
dfx canister --network ic call ohshii_governance set_decideid_vc_arguments '("", "")'
# Example: require gold-tier reputation:
# dfx canister --network ic call ohshii_governance set_decideid_vc_arguments '("", "gold")'
# Example: require verification dated on or after 2026-01-01:
# dfx canister --network ic call ohshii_governance set_decideid_vc_arguments '("2026-01-01T00:00:00Z", "")'
Terminal window
dfx canister --network ic call ohshii_governance get_decideid_config

Expected fields:

record {
issuer_canister_id = principal "qgxyr-pyaaa-aaaah-qdcwq-cai";
issuer_origin = "https://id.decideai.xyz/";
credential_type = "ProofOfUniqueness";
derivation_origin = "https://v5mnw-oaaaa-aaaal-qsica-cai.icp0.io";
enabled = true;
mode = variant { Vc };
vc_minimum_verification_date = "";
vc_minimum_reputation_level = "";
...
}
  1. Open https://v5mnw-oaaaa-aaaal-qsica-cai.icp0.io/verify (or whichever canonical URL you advertise) with a wallet that has completed DecideID Proof-of-Personhood at id.decideai.xyz.
  2. Click Start DecideID verification.
  3. The II popup opens, completes the credential round-trip, and closes.
  4. The page flips to “Verified” and am_i_verified() returns true.

If the popup completes but the modal shows a validation error, inspect the canister with:

Terminal window
dfx canister --network ic call ohshii_governance get_recent_events '(opt 20 : opt nat32)'

OIDC is an alternative DecideAI verification path that uses HTTPS outcalls + DCD billing. It exists for environments where the on-canister VC pipeline cannot be used. The toggle is a single proposal:

Terminal window
dfx canister --network ic call ohshii_governance set_decideid_mode '(variant { Oidc })'

Switching modes does not invalidate existing verifications — DECIDEID_VERIFIED_USERS is persisted independently of which path produced the entry.

LeverCommandEffect
Halt new verifications, keep gate activedfx canister --network ic call ohshii_governance set_decideid_daily_cap '(0 : nat64)'Existing verified users keep voting; new attempts rejected.
Disable the gate, keep verificationsdfx canister --network ic call ohshii_governance set_decideid_config '(principal "qgxyr-pyaaa-aaaah-qdcwq-cai", "https://id.decideai.xyz/", "ProofOfUniqueness", "https://v5mnw-oaaaa-aaaal-qsica-cai.icp0.io", false, 0 : nat64)'Method becomes Optional. Vote-gate from DecideID stops firing.
Full rollback to Not Configureddfx canister --network ic call ohshii_governance set_decideid_config '(principal "2vxsx-fae", "", "", "", false, 0 : nat64)'Method dormant. Existing verifications kept (clear via admin_clear_decideid_verified if needed).