Skip to main content
This example shows the production pattern: client generates keys, uploads only the evaluation key (FHE-blind mode), and the WAVIS API performs gate evaluations without ever seeing plaintext.

What you’ll build

A program that:
  1. Generates an FHE keypair locally.
  2. Uploads the evaluation key to api.wavis.xyz (40 MB).
  3. Encrypts bits locally and sends ciphertexts to the server.
  4. Calls /api/v1/tfhe/gate to evaluate NAND on the server.
  5. Receives the result ciphertext and decrypts locally.
The server holds: eval-key + ciphertexts. The server never holds: secret key, plaintexts.

Setup

pip install wavis-fhe requests
export WAVIS_API_KEY="wvs_trial_..."   # or wvs_live_...
Get a trial key with no signup:
export WAVIS_API_KEY=$(curl -s -X POST https://api.wavis.xyz/api/v1/onboarding/temp-key | jq -r .api_key)

Full program

import os
import base64
import requests
import wavis_fhe as wv

API_KEY = os.environ["WAVIS_API_KEY"]
BASE_URL = "https://api.wavis.xyz"
HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

# ─── Helpers ───────────────────────────────────────────────────────────────

def b64e(b: bytes) -> str: return base64.b64encode(b).decode()
def b64d(s: str) -> bytes: return base64.b64decode(s)

# ─── Step 1 — Local keygen ─────────────────────────────────────────────────

print("[client] generating keypair...")
keys = wv.keygen("fast_128")
ek_bytes = keys.eval_key_bytes()
print(f"[client] eval key: {len(ek_bytes) / 1e6:.1f} MB")

# ─── Step 2 — Upload eval-key (FHE-Blind session) ──────────────────────────

print("[client] uploading eval key — server enters FHE-blind mode")
resp = requests.post(
    f"{BASE_URL}/api/v1/tfhe/eval-session",
    headers=HEADERS,
    json={"eval_key_b64": b64e(ek_bytes)},
)
resp.raise_for_status()
session = resp.json()
session_id = session["session_id"]

print(f"[server] session_id={session_id}, is_blind={session['is_blind']}")
assert session["is_blind"] is True

# ─── Step 3 — Local encryption ─────────────────────────────────────────────

ct_a = keys.encrypt(False)
ct_b = keys.encrypt(True)
print("[client] encrypted (False, True)")

# ─── Step 4 — Server-side gate evaluation ──────────────────────────────────

print("[client] requesting NAND on the server")
resp = requests.post(
    f"{BASE_URL}/api/v1/tfhe/gate",
    headers=HEADERS,
    json={
        "session_id": session_id,
        "op": "NAND",
        "a": b64e(ct_a.to_bytes()),
        "b": b64e(ct_b.to_bytes()),
    },
)
resp.raise_for_status()
gate = resp.json()
print(f"[server] computed in {gate['compute_ms']:.1f} ms (cost: ¥{gate['cost_jpy']:.2f})")

# ─── Step 5 — Local decryption ─────────────────────────────────────────────

result_ct = wv.ciphertext_from_bytes(b64d(gate["result_b64"]))
result = keys.decrypt(result_ct)
print(f"[client] decrypted: NAND(False, True) = {result}")

assert result is True

# ─── Cleanup — release server resources ────────────────────────────────────

requests.delete(
    f"{BASE_URL}/api/v1/tfhe/session/{session_id}",
    headers=HEADERS,
).raise_for_status()
print("[client] session deleted")
Output:
[client] generating keypair...
[client] eval key: 38.7 MB
[client] uploading eval key — server enters FHE-blind mode
[server] session_id=sess_a1b2c3..., is_blind=True
[client] encrypted (False, True)
[client] requesting NAND on the server
[server] computed in 14.2 ms (cost: ¥0.10)
[client] decrypted: NAND(False, True) = True
[client] session deleted

What just happened

┌─────── CLIENT ──────────┐                 ┌─────── api.wavis.xyz ───────┐
│                         │                 │                              │
│  secret_key  ★          │                 │  (does not have secret_key)  │
│  public_key             │                 │                              │
│  eval_key   ────────────┼─POST eval-key──▶│  eval_key (cached in session)│
│                         │                 │                              │
│  ct_a = enc(False)      │                 │                              │
│  ct_b = enc(True)       │                 │                              │
│                         │ ──POST /gate──▶ │                              │
│                         │  {a, b, NAND}   │  ct_result = NAND(ct_a, ct_b)│
│                         │ ◀── ct_result ──│                              │
│  result = dec(ct_result)│                 │                              │
│  → True                 │                 │                              │
└─────────────────────────┘                 └──────────────────────────────┘
The symbol marks the secret key. It exists only on the client. If api.wavis.xyz is breached, an attacker recovers:
  • The evaluation key — useful only for forward operations, not decryption.
  • Cached ciphertexts — encrypted under the secret key the attacker doesn’t have.
  • API access logs — endpoint timestamps, no plaintext.
The plaintext bits False and True never existed on the server. There is nothing the server could leak.

Production checklist

Before deploying an FHE-blind workload to production, verify:
  • Secret key storage. Where does the client persist the secret key? KMS, Vault, OS keychain, etc.
  • Eval-key reuse. Upload once per long session, not per gate. Eval keys are 40 MB.
  • Session cleanup. Always DELETE /tfhe/session/{id} when done.
  • Idempotency. Add Idempotency-Key headers to all POST /tfhe/gate calls so retries don’t double-charge.
  • Webhooks for long jobs. Use /api/v1/webhooks instead of polling for jobs >5 s.
  • Budget cap. Set monthly_cap_usd via /billing/budget so a runaway loop can’t drain your account.
  • Rate-limit handling. Implement exponential backoff on 429 (the SDKs do this automatically).

Batched server-side calls

For multiple independent gates, use /tfhe/batch — gets a 30% price discount for ≥32 ops:
gates = [
    {"op": "NAND", "a": b64e(a.to_bytes()), "b": b64e(b.to_bytes())}
    for a, b in pairs
]

resp = requests.post(
    f"{BASE_URL}/api/v1/tfhe/batch",
    headers=HEADERS,
    json={"session_id": session_id, "gates": gates},
)
batch = resp.json()
print(f"discount applied: {batch['batch_discount_applied']}")
print(f"total cost: ¥{batch['total_cost_jpy']:.2f}")

results = [
    keys.decrypt(wv.ciphertext_from_bytes(b64d(r)))
    for r in batch["results_b64"]
]

Comparing v1 (server-managed) vs. v2 (FHE-blind)

Aspectv1 — /tfhe/sessionv2 — /tfhe/eval-session
Server gets secret key?Briefly (for keygen)Never
EncryptionServer-side via /encryptClient-side (this example)
DecryptionServer-side via /decryptClient-side (this example)
Best forDemos, quick testsProduction
Always use v2 (FHE-blind) for production. v1 exists for backward compatibility and demo simplicity.

Next Steps

GPU Batch

Local GPU acceleration for batched workloads

Webhooks

Async events for long-running jobs