Trust,
as a gate.
A drop-in React component that gates content on an Entros Anchor. Wrap any children, set a Trust Score floor and a recency window, and the gate handles wallet connection, identity lookup, and verification prompts.
// PASS
482 ≥ 100Welcome, verified human.
<children /> rendered
// BLOCK
0 < 100Verify your humanness.
<fallback /> rendered
Try it now.
Connect your wallet, dial the Trust Score floor, and watch the gate decide what your wallet can see. It reads recency too, so a wallet that has not verified inside the window does not pass.
// GATE
Try 0 to allow any verified human, or a high number to block. Adjust to test the gate against your connected wallet's Trust Score.
Output
Wrap. Set. Render.
Wrap any component tree. The gate handles wallet connection state, identity lookup, and Trust Score comparison. A default fallback prompts the user to connect, verify, or improve their score.
import { EntrosGate } from "@/components/ui/entros-gate";export function PremiumPage() { return ( <EntrosGate minTrustScore={100}> {/* Children render only when the connected wallet has an Entros Anchor with trust score >= 100 */} <h1>Welcome, verified human.</h1> </EntrosGate> );}Override the prompt.
Pass a fallback prop to override the default verification prompt. Useful for paywalls, branded gates, or progressive disclosure flows.
<EntrosGate minTrustScore={100} fallback={<YourCustomPrompt />}> <PremiumContent /></EntrosGate>Copy. Paste. Ship.
Paste this file at components/ui/entros-gate.tsx. Requires @entros/pulse-sdk, @solana/web3.js, and @solana/wallet-adapter-react.
"use client";/** * <EntrosGate>—drop-in route guard for Entros Trust Score. * * Renders children only when the connected wallet has an Entros Anchor with * trustScore >= minTrustScore. Otherwise renders a fallback (default: a * verification prompt linking to https://entros.io/verify, or a custom node via the * `fallback` prop). * * This component has zero internal UI dependencies. It uses only: * - React + Next.js Link * - @solana/web3.js (PublicKey, Connection) * - @solana/wallet-adapter-react (useWallet, useConnection) * - @solana/wallet-adapter-react-ui (WalletMultiButton, the universal Solana wallet UI) * - @entros/pulse-sdk (PROGRAM_IDS constant) * - lucide-react (icons) * - Tailwind CSS for styling (no custom design system imports) * * Drop into any Next.js + Tailwind project with Solana wallet adapter set up. * * Example: * <EntrosGate minTrustScore={100}> * <PremiumContent /> * </EntrosGate> * * The gate reads two fields, not one. `trustScore` says how consistently this * wallet has verified. `maxVerificationAge` bounds how long ago it last did. * A score on its own describes a history; pairing it with recency keeps the * gate about the operator rather than the record. * * For an action with real stakes, run a verification at the point of the * action and let the gate read the result: `<EntrosVerify />` from * `@entros/verify` opens the capture in place, and this gate passes on the * next render. * * SECURITY: this is a client-side gate, suitable for UI gating, paywalls and * progressive disclosure. A determined user can bypass any client-side check * via DevTools, so for high-stakes access control assert the same two fields * in your own program or server route, reading the Anchor PDA directly. */import type { ReactNode } from "react";import { useEffect, useState } from "react";import Link from "next/link";import { Connection, PublicKey } from "@solana/web3.js";import { useConnection, useWallet } from "@solana/wallet-adapter-react";import { WalletMultiButton } from "@solana/wallet-adapter-react-ui";import { PROGRAM_IDS } from "@entros/pulse-sdk";import { Loader2, ShieldAlert, Wallet } from "lucide-react";/** * Smallest account this component can read. `last_verification_timestamp` ends * at 56 and `trust_score` at 62, so 62 covers both. Every layout the program * has ever written is longer than this. */const EXPECTED_SIZE = 62;/** One day. Long enough for a session, short enough to mean something. */const DEFAULT_MAX_VERIFICATION_AGE = 86_400;/** * Whether an Anchor clears both thresholds. * * Pure, so the gate's decision can be tested without a DOM and read without * tracing a component. `nowSeconds` is injected for the same reason. * * The clock here is the browser's, and a user controls it. That is fine for * what this is: a client-side gate for UI, which a user could bypass with * DevTools regardless. Anything that moves value asserts the same two fields * where it settles, against `Clock::get()` on chain or a server clock. */export function passesEntrosGate( anchor: { trustScore: number; lastVerifiedAt: number }, thresholds: { minTrustScore: number; maxVerificationAge: number }, nowSeconds: number = Math.floor(Date.now() / 1000),): boolean { // A minted Anchor always carries a verification timestamp, so a zero here // means the account is not one, and no threshold should pass it. if (anchor.lastVerifiedAt <= 0) return false; if (anchor.trustScore < thresholds.minTrustScore) return false; return nowSeconds - anchor.lastVerifiedAt <= thresholds.maxVerificationAge;}const ENTROS_PROGRAM_ID = new PublicKey(PROGRAM_IDS.entrosAnchor);interface EntrosGateProps { minTrustScore: number; /** * Seconds since the wallet's last verification, above which the gate does * not pass. Defaults to one day. Pass `Infinity` to gate on score alone, * which is appropriate for display and for anything with no real stakes. */ maxVerificationAge?: number; children: ReactNode; fallback?: ReactNode; loadingFallback?: ReactNode; verifyHref?: string;}type FetchState = | { status: "loading" } | { status: "disconnected" } | { status: "no-identity" } | { status: "ready"; trustScore: number; lastVerifiedAt: number };export function EntrosGate({ minTrustScore, maxVerificationAge = DEFAULT_MAX_VERIFICATION_AGE, children, fallback, loadingFallback, verifyHref = "https://entros.io/verify",}: EntrosGateProps) { const { publicKey, connected } = useWallet(); const { connection } = useConnection(); const [fetchState, setFetchState] = useState<FetchState>({ status: "loading" }); useEffect(() => { if (!connected || !publicKey) { setFetchState({ status: "disconnected" }); return; } setFetchState({ status: "loading" }); let isMounted = true; const timeoutId = setTimeout(() => { (async () => { try { const [identityPda] = PublicKey.findProgramAddressSync( [new TextEncoder().encode("identity"), publicKey.toBuffer()], ENTROS_PROGRAM_ID, ); const conn = connection ?? new Connection("https://api.devnet.solana.com", "confirmed"); const account = await conn.getAccountInfo(identityPda); if (!isMounted) return; if (!account || account.data.length < EXPECTED_SIZE) { setFetchState({ status: "no-identity" }); return; } const view = new DataView( account.data.buffer, account.data.byteOffset, account.data.byteLength, ); setFetchState({ status: "ready", trustScore: view.getUint16(60, true), // i64 at offset 48. Unix seconds fit in a Number until year 275760. lastVerifiedAt: Number(view.getBigInt64(48, true)), }); } catch { if (isMounted) setFetchState({ status: "no-identity" }); } })(); }, 300); return () => { isMounted = false; clearTimeout(timeoutId); }; }, [publicKey, connected, connection]); // Both comparisons happen at render time, not in the fetch effect. Changing // either threshold re-renders without triggering a new RPC call. if ( fetchState.status === "ready" && passesEntrosGate(fetchState, { minTrustScore, maxVerificationAge }) ) { return <>{children}</>; } if (fetchState.status === "loading") { if (loadingFallback) return <>{loadingFallback}</>; return ( <div className="flex items-center justify-center py-16"> <Loader2 className="h-6 w-6 animate-spin text-cyan-500" /> </div> ); } if (fallback) return <>{fallback}</>; const fallbackState = fetchState.status === "ready" ? ({ status: "below-threshold", trustScore: fetchState.trustScore } as const) : fetchState; return ( <DefaultFallback state={fallbackState} minTrustScore={minTrustScore} verifyHref={verifyHref} /> );}type FallbackState = | { status: "disconnected" } | { status: "no-identity" } | { status: "below-threshold"; trustScore: number };function DefaultFallback({ state, minTrustScore, verifyHref,}: { state: FallbackState; minTrustScore: number; verifyHref: string;}) { const statusLabel = state.status === "disconnected" ? "WALLET" : state.status === "no-identity" ? "UNVERIFIED" : "BELOW THRESHOLD"; return ( <div className="w-full"> <div className="flex items-center justify-between"> <p className="font-mono text-[10px] uppercase tracking-[0.2em] text-foreground/40"> // {statusLabel} </p> {state.status === "below-threshold" && ( <span className="font-mono text-[10px] uppercase tracking-[0.2em] text-foreground/40"> {state.trustScore} < {minTrustScore} </span> )} </div> <div className="mt-8 flex flex-col items-start gap-6"> {state.status === "disconnected" ? ( <Wallet className="h-7 w-7 text-cyan/80" strokeWidth={1.5} /> ) : ( <ShieldAlert className="h-7 w-7 text-cyan/80" strokeWidth={1.5} /> )} {state.status === "disconnected" && ( <> <div> <p className="text-base font-medium text-foreground"> Connect your wallet </p> <p className="mt-2 text-sm leading-relaxed text-foreground/60"> This content requires a verified Entros identity. </p> </div> <WalletMultiButton /> </> )} {state.status === "no-identity" && ( <> <div> <p className="text-base font-medium text-foreground"> Verify your humanness </p> <p className="mt-2 text-sm leading-relaxed text-foreground/60"> This content requires an Entros Anchor with Trust Score{" "} <span className="font-mono text-cyan">{minTrustScore}</span>{" "} or higher. </p> </div> <Link href={verifyHref} className="inline-flex items-center gap-2 border border-cyan/40 bg-cyan/[0.08] px-4 py-2 font-mono text-xs uppercase tracking-[0.2em] text-cyan transition-colors hover:border-cyan/70 hover:bg-cyan/[0.15]" > Verify now </Link> </> )} {state.status === "below-threshold" && ( <> <div> <p className="text-base font-medium text-foreground"> Trust Score too low </p> <p className="mt-2 text-sm leading-relaxed text-foreground/60"> Your Trust Score is{" "} <span className="font-mono text-cyan">{state.trustScore}</span>. This content requires{" "} <span className="font-mono text-cyan">{minTrustScore}</span>. Re-verify across multiple days to grow your score. </p> </div> <Link href={verifyHref} className="inline-flex items-center gap-2 border border-cyan/40 bg-cyan/[0.08] px-4 py-2 font-mono text-xs uppercase tracking-[0.2em] text-cyan transition-colors hover:border-cyan/70 hover:bg-cyan/[0.15]" > Re-verify </Link> </> )} </div> </div> );}