@uzproof/verify-reader
v0.1.4
Published
Reader SDK for UZPROOF proof-of-use attestations. Check whether a wallet has a signed SAS attestation for a specific on-chain action. Zero on-chain writes, zero API keys.
Maintainers
Readme
@uzproof/verify-reader
Reader SDK for UZPROOF proof-of-use attestations on Solana.
Check whether a wallet has a signed SAS attestation for a specific completed UZPROOF quest — no API keys, no UZPROOF account, zero on-chain writes. Direct reads against Solana mainnet via any RPC endpoint.
Why
UZPROOF writes proof-of-use attestations to the Solana Attestation Service (SAS) every time a wallet completes a verified UZPROOF quest. These attestations are portable, permanent, and cryptographically signed.
This package is the reader half: any dApp can check a wallet's quest completion without onboarding into UZPROOF's writer flow.
Use cases:
- Airdrop eligibility: filter a candidate list to "verified real users" before distribution
- Loyalty tiers: grant benefits to wallets that completed N UZPROOF quests
- Anti-sybil gates: require a UZPROOF attestation to enter a reward campaign
- Leaderboards: show "verified" badges next to wallets with live quest-completion attestations
Install
npm install @uzproof/verify-reader @solana/web3.js@solana/web3.js is a peer dependency so you share one copy across
packages.
Quickstart — quest completion check
import { hasQuestCompletion } from "@uzproof/verify-reader";
const completed = await hasQuestCompletion(
"FX3Zs3jKRs8cQcjymCJRwmzwNh11PxnxSXTmLA1Dci8Q",
31, // UZPROOF quest id
);
console.log(completed); // true | falseOne getAccountInfo call against a deterministically-derived PDA.
No API key, no config, no UZPROOF account.
Data model
UZPROOF's current Paid Claim flow writes one attestation per quest, not per underlying action type. The SAS account's data layout is:
| Field | Type | Example |
|----------------|--------|--------------------------------------------|
| wallet | string | "FX3Zs3jK..." (base58 wallet address) |
| protocol | string | "DeFi" / "NFT" / "Solana" (category) |
| action | string | "quest_31" (UZPROOF quest id prefix) |
| amountUsd | number | USD-denominated at claim time |
| genuineScore | number | 0 – 100, UZPROOF anti-sybil engine |
| verifiedAt | Date | Unix timestamp of verification |
The UZPROOF writer enforces: one attestation per (wallet, quest)
pair, permanent unless explicitly revoked. If you need to check a
specific quest, use hasQuestCompletion(wallet, questId).
API
hasQuestCompletion(wallet, questId, connection?)
Returns Promise<boolean>. True iff UZPROOF has issued an
attestation on mainnet for the wallet completing the given quest id.
The canonical entry point for typical dApp integrations.
const isGoldTier = await hasQuestCompletion(userWallet, 42);getQuestCompletion(wallet, questId, connection?)
Returns Promise<VerificationHit | null>. Full attestation details
- on-chain address when available,
nullwhen no attestation exists.VerificationHit.dataisnullwhen the account exists but the reader version cannot parse the SAS header layout — theattestationAddressis always available so the caller can cite the on-chain proof directly.
const hit = await getQuestCompletion(userWallet, 42);
if (hit) {
console.log(hit.attestationAddress); // Base58 address
console.log(hit.data?.amountUsd); // USD at claim time
console.log(hit.data?.genuineScore); // 0-100 anti-sybil
console.log(hit.data?.verifiedAt); // Date
console.log(hit.data?.protocol); // "DeFi" | "NFT" | "Solana"
console.log(hit.data?.action); // "quest_42"
}verifyBatch(wallets, action, connection?)
Returns Promise<Map<string, boolean>>. Batched existence check —
ideal for airdrop filtering. Uses getMultipleAccountsInfo and
automatically chunks inputs of any size into 100-account RPC calls.
Note this takes the raw action string (e.g. "quest_42") so you
can batch-check a single quest across many wallets:
import { Connection } from "@solana/web3.js";
import { verifyBatch } from "@uzproof/verify-reader";
const conn = new Connection("https://mainnet.helius-rpc.com/?api-key=...");
const results = await verifyBatch(candidateWallets, "quest_42", conn);
const eligible = candidateWallets.filter((w) => results.get(w));hasVerification(wallet, action, connection?) (low-level)
Returns Promise<boolean>. Existence check against an arbitrary
action string. Use this if UZPROOF ever publishes a new attestation
format beyond the current quest_<id> convention — this primitive
won't change, just the calling conventions built on top.
getVerification(wallet, action, connection?) (low-level)
Returns Promise<VerificationHit | null>. Low-level counterpart to
getQuestCompletion. Same return shape.
Constants
import {
SAS_PROGRAM_ID, // 22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG
UZPROOF_CREDENTIAL, // 2chgBfvkwhnHQVVAyXKDK6CBjbCRMQ8aLWrysL5UQyyF
UZPROOF_SCHEMA_V2, // 8yW2BboQuhp2MMmrQLFz35V6VSqC48MF7wZ5bmzcTeTF
} from "@uzproof/verify-reader";Use these to cite UZPROOF's anchor accounts in your own tooling or block explorers. Verified against Solana mainnet: both credential and schema accounts exist and are owned by the SAS program.
RPC choice
The default connection uses Solana's public mainnet RPC. Fine for
occasional lookups, not recommended for airdrop-scale batching.
Pass your own Connection:
import { Connection } from "@solana/web3.js";
const conn = new Connection("https://your-rpc-endpoint");
await verifyBatch(wallets, "quest_42", conn);Helius, Triton, and QuickNode all work. The reader does NOT use DAS
or any enhanced API — only getAccountInfo and
getMultipleAccountsInfo.
Design
- Deterministic PDA lookup — no
getProgramAccountsscan; every check is an exact account address derivation + single RPC read. - Zero writer dependency — this package has no code path that can write to SAS. Safe to import in browser + server.
- Schema-pinned — reader is locked to UZPROOF schema v2. A future schema v3 ships alongside a major version bump of this SDK.
- Header-tolerant decode —
findAndDecodePoUscans past the SAS account header to locate the PoU payload without hard-coding an offset that would break if SAS ever pads the header.
Security
- Never send a wallet address over a channel you wouldn't trust with an on-chain read. The check itself is public information.
- The
attestationAddressreturned fromgetQuestCompletionis public and anyone can verify it against mainnet; you can show it to users as cryptographic proof.
Known upstream advisories
npm audit on a project that installs this SDK will surface
5 MODERATE findings that all trace to a single CVE in uuid<14.0.0
pulled in transitively through @solana/web3.js → jayson / rpc-websockets.
The CVE (GHSA-9jp5-9hw6-mrmp) is in the v3/v5/v6 UUID generators when
a caller passes a custom buf. The Solana libraries above only use
v4 (random) UUIDs, so the vulnerable code path is never reached in
practice. The advisory appears because the installed version is
< 14.0.0, not because the vulnerable function is invoked.
To silence the audit warning in your app, add an overrides block to
your package.json:
{
"overrides": {
"uuid": "^14.0.0"
}
}This forces the whole tree to uuid@14+ (which patches the CVE) and
causes npm audit to report zero MODERATE findings. The override
lives in the application's package.json because overrides in a
library's package manifest are ignored by npm — that is why this SDK
documents the mitigation rather than shipping it.
UZPROOF's own codebase uses this exact override (see package.json
of the main repo) and our production npm audit has 0 MODERATE.
Input constraints
The nonce used to derive attestation PDAs is sha256("wallet:action")
— a single : delimiter. To prevent ambiguity attacks, the SDK
rejects inputs where either field is empty or contains a : itself:
hasVerification("", "quest_31") // throws: wallet must be non-empty
hasVerification(wallet, "") // throws: action must be non-empty
hasVerification("wallet:abc", "quest") // throws: wallet must not contain ':'
hasVerification(wallet, "quest:v2") // throws: action must not contain ':'In practice this is invisible — Solana wallets are base58
(colon-free by alphabet) and UZPROOF quest actions are
quest_<numeric-id>. The guards catch programmer error rather
than constrain real use cases.
Changelog
0.1.4 — 2026-04-23
- Completes the 0.1.3 isomorphic pass.
deriveAttestationPdastill calledBuffer.from(ATTESTATION_SEED)andPublicKey#toBuffer()— the exact Node-global dependency 0.1.3's changelog claimed was gone. Swapped both tonew TextEncoder().encode(...)andPublicKey#toBytes()so the SDK's own runtime code has zero references toBuffer.findProgramAddressSyncacceptsUint8ArrayalongsideBufferso this is byte-identical — a hardcoded regression test pins two known PDA outputs to make any future drift loud. Consumers on modern bundlers (Next/Vite/webpack) were unaffected because their Buffer polyfill masked the issue; raw ES-module browsers and strict Deno/Workers contexts are now truly clean.
0.1.3 — 2026-04-23
- Real browser + Deno + Edge compat (decoder layer). Decoder
previously used Node-only Buffer methods (
readUInt32LE,readUInt8,readBigInt64LE) which crashed in Deno and raw (non-bundled) browser contexts despite the "isomorphic" claim. Swapped the decoder toDataView+TextDecoder— both are Web-standard and available everywhere JS runs with zero polyfill. Input type relaxed fromBuffertoUint8Array(Buffer still works since it extends Uint8Array). New test pins a pure- Uint8Array input path that never touches Node Buffer. Note: 0.1.3 missed the same bug pattern inpda.ts— use 0.1.4+.
0.1.2 — 2026-04-23
- Docs-only release: added "Known upstream advisories" section
documenting the
uuid<14.0.0transitive CVE that surfaces innpm auditon consumer projects + theoverridesmitigation they can apply. No code changes.
0.1.1 — 2026-04-23
- Corrected
@solana/web3.jspeer-dependency range to^1.95.0. Earlier 0.1.0 claimed^1.95.0 || ^2.0.0, which was wrong — @solana/web3.js v2 is a modular rewrite (@solana/kit) with noConnectionclass and a differentPublicKeyAPI, so the reader SDK is v1-only until a future v2 port ships.
0.1.0 — 2026-04-23
- Initial release
- Typed helpers for UZPROOF's Paid Claim flow:
hasQuestCompletion,getQuestCompletion - Low-level primitives:
hasVerification,getVerification,verifyBatch - Full decoder for PoU payload (wallet, protocol, action, amountUsd, genuineScore, verifiedAt)
License
MIT © UZPROOF
Related packages
@uzproof/verify— the writer SDK (pay-per-call via x402)
