MirrorProof API

Generate and verify zero-knowledge proofs about financial data. Connect bank accounts and crypto exchanges via Plaid, attest balances, and create shareable proof links — all without exposing actual numbers.

Base URL  https://api.mirrorzkp.com

Authentication

Protected endpoints require a Clerk JWT in the Authorization header.

Authorization: Bearer <clerk_jwt_token>

Public endpoints (proof lookup, verification, health) require no authentication.

Errors

All errors return a JSON object with an error field.

{
  "error": "Description of what went wrong"
}
StatusMeaning
400Bad request — missing fields or invalid proof
401Not authenticated — missing or invalid JWT
404Resource not found
500Server error
POST /auth/plaid/exchange
Authenticated

Exchanges a Plaid public token for an access token after the user completes Plaid Link. Creates or updates the user record with institution info and masked email.

Request Body

{
  "public_token": "public-sandbox-xxxxxxxx",
  "metadata": {
    "institution": {
      "name": "Chase",
      "institution_id": "ins_3"
    }
  }
}

Response

{
  "user_id": "550e8400-e29b-41d4-a716-446655440000",
  "institution_name": "Chase",
  "account_type": "bank",
  "accounts": [
    {
      "id": "abc123",
      "name": "Checking",
      "type": "depository",
      "subtype": "checking",
      "mask": "1234"
    }
  ]
}
POST /v1/attest/balance
Authenticated

Fetches current account balances from Plaid and returns attested data for client-side proof generation. Balances are not stored on the server.

Request Body

Empty.

Response

{
  "accounts": [
    {
      "account_id": "abc123",
      "name": "Checking",
      "type": "depository",
      "subtype": "checking",
      "mask": "1234",
      "currency": "USD",
      "current_balance": 15250.50,
      "available_balance": 15100.00
    }
  ],
  "timestamp": 1711497600,
  "attestation_hash": "a1b2c3d4...",
  "source": "plaid"
}
POST /v1/attest/holdings
Authenticated

Fetches investment and crypto holdings from Plaid with per-security breakdowns and per-account totals.

Request Body

Empty.

Response

{
  "accounts": [
    {
      "account_id": "xyz789",
      "name": "Coinbase",
      "type": "investment",
      "current_balance": 42500.00,
      ...
    }
  ],
  "holdings": [
    {
      "ticker": "BTC",
      "name": "Bitcoin",
      "quantity": 0.5,
      "price": 67000.00,
      "value": 33500.00,
      "is_crypto": true,
      ...
    }
  ],
  "timestamp": 1711497600,
  "attestation_hash": "e5f6g7h8...",
  "source": "plaid_investments"
}
POST /v1/proofs
Authenticated

Submits a zero-knowledge proof for verification. The proof is verified server-side using snarkjs, stored, and a shareable link is generated.

Request Body

{
  "proof": { // Groth16 proof object (pi_a, pi_b, pi_c, protocol, curve) },
  "publicSignals": ["10000000000", "...", ...],
  "circuit": "balance-threshold",
  "attestation_hash": "a1b2c3d4..."  // optional
}

Response

{
  "proof_id": "550e8400-...",
  "circuit": "balance-threshold",
  "verified": true,
  "share_token": "aBcDeFgH",
  "share_url": "https://app.mirrorzkp.com/verify/aBcDeFgH",
  "created_at": "2026-03-26T12:00:00.000Z"
}
GET /v1/proofs
Authenticated

Lists all proofs belonging to the authenticated user, newest first. Includes computed freshness for each proof.

Response

{
  "proofs": [
    {
      "id": "550e8400-...",
      "circuit": "balance-threshold",
      "public_inputs": { ... },
      "verified": true,
      "institution_name": "Chase",
      "share_token": "aBcDeFgH",
      "expires_at": "2026-03-27T12:00:00.000Z",
      "created_at": "2026-03-26T12:00:00.000Z",
      "freshness": { ... }  // see Freshness Object
    }
  ]
}
GET /v1/proofs/:id
Public

Returns details for a single proof by its UUID.

Response

{
  "id": "550e8400-...",
  "circuit": "balance-threshold",
  "public_inputs": { ... },
  "verified": true,
  "institution_name": "Chase",
  "source_type": "bank",
  "freshness": { ... },
  "created_at": "2026-03-26T12:00:00.000Z"
}
POST /v1/proofs/:id/verify
Public

Re-verifies a proof's cryptographic validity and checks freshness. Optionally override the freshness window.

Request Body

{
  "max_age_hours": 48  // optional, overrides default window
}

Response

{
  "valid": true,
  "circuit": "balance-threshold",
  "public_inputs": { ... },
  "freshness": { ... }
}
GET /v1/proofs/wallet/:address
Public

Looks up all proofs associated with a wallet address. Case-insensitive. Designed for DeFi protocol integrations — any protocol can check if a wallet has valid proofs without the user being present.

Response

{
  "proofs": [
    {
      "id": "550e8400-...",
      "circuit": "balance-threshold",
      "public_inputs": { ... },
      "verified": true,
      "share_token": "aBcDeFgH",
      "created_at": "2026-03-26T12:00:00.000Z"
    }
  ]
}
GET /verify/:shareToken
Public

Public verification by share token. Returns a human-readable claim, the masked owner email, wallet address (if available), and freshness data.

Response

{
  "verified": true,
  "circuit": "balance-threshold",
  "claim": "Balance ≥ $10,000",
  "owner": "s•••••@gmail.com",
  "wallet": "0xABC...DEF",
  "institution": "Chase",
  "source_type": "bank",
  "freshness": { ... },
  "proof_id": "550e8400-..."
}
POST /v1/user/sync
Authenticated

Ensures a database user record exists for the authenticated Clerk user. Called automatically by the frontend after sign-in. Safe to call multiple times.

Response

{
  "ok": true
}
GET /health
Public

Health check. Returns API status and available circuits.

Response

{
  "status": "ok",
  "version": "1.0.0",
  "circuits": ["balance-threshold"]
}

Freshness Object

Included in proof responses to indicate how current the underlying attestation is.

{
  "attestation_time": "2026-03-26T12:00:00.000Z",
  "proof_age_seconds": 3600,
  "proof_age_human": "1 hours ago",
  "is_fresh": true,
  "freshness_window_hours": 24,
  "warning": "This proof is 3 days ago..."  // only if stale
}

Default freshness windows

CircuitWindow
balance-threshold24 hours
average-balance7 days
account-agePermanent

Public Inputs

The public_inputs field is parsed from the circuit's public signals. The shape depends on the circuit type.

balance-threshold

{
  "threshold": "10000000000",        // scaled by 1,000,000 ($10,000)
  "attestation_commitment": "12345...", // Poseidon hash
  "source_id": "1",                   // 1 = Plaid
  "timestamp": "1711497600",           // Unix seconds
  "account_hash": "67890..."           // Poseidon(account_uuid)
}

To convert a threshold to dollars: divide by 1,000,000.