CA
GHOST

Developer SDK — @ghost/sdk

TypeScript, ESM, isomorphic (Node ≥ 20 and modern browsers). Lives in packages/sdk.

Entry pointUse inPurpose
@ghost/sdkanywhereTypes, encoding, Poseidon, commitments, Merkle tree, request building, attestation checks
@ghost/sdk/verifieryour backendGhostVerifier (stateful, replay-protected) and verifyGhostProof (stateless)
@ghost/sdk/clientyour frontendrequestGhostProof — opens GHOST, returns the bundle
@ghost/sdk/proverGHOST client / power usersgenerateGhostProof
@ghost/sdk/operatorGHOST operatorsSnapshot building from Solana RPC
@ghost/sdk/artifacts/*—verification_key.json, ghost.wasm, ghost_final.zkey, manifest.json

Integrating verification (relying party)

You need three things from the GHOST deployment you trust:

  1. its verification key (artifacts/verification_key.json, check the sha256 in manifest.json),
  2. its attester public key(s) (GET /api/config → trustedAttesters, or published out-of-band — pin them, don't fetch them at runtime from the same server you're verifying against),
  3. its origin (for the popup).

1. Backend: create a verifier

import { GhostVerifier, type ChallengeStore, type NullifierStore } from "@ghost/sdk/verifier";
import vkey from "@ghost/sdk/artifacts/verification_key.json" with { type: "json" };

export const verifier = new GhostVerifier({
  appId: "https://dao.example/vote-7",        // origin + optional path; scopes nullifiers
  verificationKey: vkey,
  trustedAttesters: ["9keoPHR6qMFqZYJMAWEj8DMgmRXrZmKnec2Gq5dx7m1Y"],
  challengeStore: myRedisChallengeStore,      // default: in-memory (single process only!)
  nullifierStore: myPostgresNullifierStore,   // default: in-memory
  enforceUniqueNullifier: true,               // one proof per wallet/identity per appId
});

ChallengeStore.consume and NullifierStore.add must be atomic (e.g. SET NX in Redis, a unique index in Postgres). The in-memory defaults are for tests and single-process demos.

2. Backend: issue a request

app.post("/ghost/request", async (req, res) => {
  const request = await verifier.createRequest({
    mint: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",
    minBalance: 1_000_000_000n,        // raw base units (here: 1,000 JUP at 6 decimals)
    nullifierMode: "wallet",           // or "identity"
    ttlSeconds: 600,                   // ≤ 3600
    maxSnapshotAgeSeconds: 86_400,
    callbackOrigin: "https://dao.example",   // must equal the origin of appId
    label: "Vote in proposal 7",
  });
  res.json(request);
});

3. Frontend: ask the user for a proof

import { requestGhostProof } from "@ghost/sdk/client";

const request = await fetch("/ghost/request", { method: "POST" }).then((r) => r.json());
const bundle = await requestGhostProof(request, { ghostUrl: "https://ghost.example" });
await fetch("/ghost/verify", { method: "POST", body: JSON.stringify(bundle) });

requestGhostProof opens a popup, accepts messages only from the GHOST origin and only for the matching challenge. Never trust a client-side result — always verify on your backend.

Alternative without popups: buildProveUrl(ghostUrl, request) gives a link; the user copies the resulting bundle back to you.

4. Backend: verify

app.post("/ghost/verify", async (req, res) => {
  const result = await verifier.verify(req.body);
  if (!result.valid) return res.status(403).json(result.checks.filter((c) => !c.passed));
  // result.statement: { appId, mint, minBalance, nullifierMode, nullifier,
  //                     snapshotId, snapshotCreatedAt, snapshotSlotMax, cluster,
  //                     anonymitySetCeiling }
  grantAccess(result.statement.nullifier);
  res.json({ ok: true });
});

result.checks lists every check with id, label, passed, detail:

request-issued, structure, request-match, not-expired, statement, attestation, snapshot-fresh, mint-covered, groth16, challenge-unused, nullifier-unique.

Stateless verification

import { verifyGhostProof } from "@ghost/sdk/verifier";
const r = await verifyGhostProof(bundle, { verificationKey, trustedAttesters, expectedRequest });

Without a challenge store this cannot prevent replay. Use it for audits, explorers and debugging.

Integration checklist

  • Pin the verification key hash and attester keys in config.
  • Use atomic, persistent challenge and nullifier stores.
  • Use fixed, coarse thresholds (avoid letting thresholds become a balance oracle — see threat model).
  • Choose nullifierMode deliberately: wallet for one-per-wallet, identity for personas.
  • Put an epoch in appId if you don't want to recognise returning users.
  • Display snapshotCreatedAt to your users/admins; proofs are historical.
  • Rate-limit /ghost/request — each call stores a challenge.

Prover API (for completeness)

import { generateGhostProof, ProverError } from "@ghost/sdk/prover";

const bundle = await generateGhostProof({
  request,                // ProofRequest
  identitySecret,         // bigint
  walletKey,              // bigint (from deriveWalletKeys)
  snapshot,               // PublishedSnapshot (full, from GET /api/snapshots/latest)
  artifacts: { wasm: "/circuits/ghost.wasm", zkey: "/circuits/ghost_final.zkey" },
  trustedAttesters,
});

Throws ProverError with code in INVALID_REQUEST | REQUEST_EXPIRED | UNTRUSTED_SNAPSHOT | SNAPSHOT_TOO_OLD | MINT_NOT_IN_SNAPSHOT | NOT_IN_SNAPSHOT | INSUFFICIENT_BALANCE.

Operator API

import { buildSnapshot, createRpcBalanceSource } from "@ghost/sdk/operator";

const { snapshot } = await buildSnapshot({
  records,                                   // [{ wallet, walletKeyCommitment, identityCommitments }]
  mints: ["…"],
  source: createRpcBalanceSource(RPC_URL),   // finalized commitment, minContextSlot pinned
  cluster: "mainnet-beta",
  attesterSecretKey,                         // 64-byte Ed25519 secret key
});

Types

See packages/sdk/src/core/types.ts: ProofRequest, ProofBundle, SnapshotAttestation, PublishedSnapshot, SnapshotEntry, Groth16Proof.

Building and testing

npm run build -w @ghost/sdk
npm run test  -w @ghost/sdk                                    # 41 tests, real proofs
GHOST_RPC_TESTS=1 GHOST_RPC_TEST_OWNER=<devnet holder> npx vitest run test/rpc.integration.test.ts