CipherExplain Python SDK
Register your own models and get encrypted SHAP explanations via the CipherExplain API.
Install
pip install cipherexplain
Dependencies installed automatically: httpx, numpy, scikit-learn.
Optional: local FHE mode
For client-side CKKS encryption (fhe_mode='ckks') install the fhe extra:
pip install 'cipherexplain[fhe]'
This adds OpenFHE so the SDK can encrypt inputs locally and decrypt results without the server ever seeing plaintext.
Quick start
from cipherexplain_sdk import CipherExplainClient, extract_spec
client = CipherExplainClient(api_key="vb_...")
# 1. Train locally — nothing leaves your machine
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
model = LogisticRegression().fit(scaler.fit_transform(X_train), y_train)
# 2. Extract weights only (no pickle, no training data)
spec = extract_spec(model, "my_model", feature_names=["age", "income", "score"],
scaler=scaler)
# 3. Register with the API
client.register(spec)
# 4. Get SHAP explanations — pass raw values, scaler is applied server-side
result = client.explain_raw("my_model", x_raw)
print(result["shap_values"]) # per-feature attributions
print(result["prediction"]) # 0.0–1.0 probability
Counterfactuals + ECOA Reg-B reason codes (v0.5.1)
For credit-denial / adverse-action workflows: after /explain, ask the
server for a SHAP-guided minimal-L2 counterfactual + Form C-1 reason
codes. The server constructs Enc(x') = Enc(x) ⊕ Enc(δ*) from its own
session-stored Enc(x); the client never gets to submit Enc(x')
directly, so the forged-CF attack is closed at the schema layer.
exp = client.explain_raw("my_lr", x_denied)
cf = client.counterfactual("my_lr", x_denied, exp)
print(cf["x_prime"]) # x + δ* — decision-flipping recourse
print(cf["decision_flipped"]) # True
for c in cf["reason_codes"]:
print(c["form_c1_code"], "—", c["form_c1_text"])
# Optional: protect class / sticky features
from cipherexplain_sdk import FeatureManifest
manifest = FeatureManifest(immutable=[True, False, ..., False]) # age fixed
cf = client.counterfactual("my_lr", x_denied, exp,
feature_manifest=manifest)
cf_attestation_mode is "UNATTESTED" by default. The Pedersen homomorphism
- real BLS12-381 G1 Σ-IPA π_CF + CRDC audit chain + composition β are real
(verified on EU production cloud infrastructure in 0.5 ms / 0.8 ms p50). For the lattice-arm
attestation upgrade (
"ATTESTED_BGV_ZK"), see the next section.
v1-B lattice attestation: bgv_zk=True (v0.6.0)
counterfactual(bgv_zk=True) attaches a del Pino-Lyubashevsky-Seiler (PKC
2019) lattice binding proof to the request. The SDK generates a fresh
BGV keypair per call, encrypts the integer-quantised attribution vector
δ_int into ct_BGV (at n'=512, q≈2^60), and runs the Σ-protocol locally;
the server verifies the lattice-arm equation in ~50 ms and upgrades
cf_attestation_mode from "UNATTESTED" to "ATTESTED_BGV_ZK".
cf = client.counterfactual("my_lr", x_denied, exp, bgv_zk=True)
assert cf["cf_attestation_mode"] == "ATTESTED_BGV_ZK"
Honest-client assumption (important): bgv_zk=True requires that
ct_CKKS (used by the SHAP computation) and ct_BGV (used by the proof)
encode the same δ_int. This is cryptographically unenforceable per
scheme-switching hardness (eprint 2023/988). The threat model is
adversarial-server output, honest client — appropriate for B2B
internal attestation where the client and the protected party are the
same entity (a bank attesting its own SHAP to an internal auditor or
third party). For consumer-facing Reg-B / ECOA adverse-action use cases,
stay on the default bgv_zk=False — the v1-A G1 Σ-IPA path is the right
fit there.
Server-side, the bgv_zk payload is consumed only when the deployment
sets CE_CF_USE_BGV_ZK=1. When the flag is off (default during rollout),
the SDK can still send bgv_zk=True payloads — the server accepts but
ignores them and the response stays on the v1-A path. No client changes
are needed when the server flips the flag.
Cost profile: ~16.5 KB proof on the wire (well under the 50 KB budget), ~100 ms prover latency on Mac M1 (pure-Python reference; LaZer C extension on Linux AMD64 + AVX-512 reduces this 50-100× when bundled).
Supported model types
Tree ensembles (new)
RandomForestClassifier, GradientBoostingClassifier, DecisionTreeClassifier — and their regressor variants — are now fully supported. Use serialize_model from cipherexplain_eval to build the registration payload:
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.preprocessing import StandardScaler
import cipherexplain_eval as ce
# Train locally
rf = RandomForestClassifier(n_estimators=100).fit(X_train, y_train)
# Serialize to JSON (no pickle, no training data)
payload = ce.serialize_model(
rf,
feature_names=["age", "income", "score"],
model_id="my_rf",
)
# Register with the API (use the raw API or SDK client)
client.register(payload)
# Explain — fhe_mode is ignored for tree models, plaintext KernelSHAP runs
result = client.explain("my_rf", x)
print(result["shap_values"])
GradientBoostingClassifier works the same way — serialize_model captures the learning rate and initial estimator needed for correct probability reconstruction.
Linear classifiers
Weights serialised to JSON — model must be described by a coefficient matrix and intercept vector.
| Framework | How |
|---|---|
sklearn LogisticRegression |
extract_spec(model, ...) |
sklearn LinearSVC |
extract_spec(model, ...) |
Any sklearn-compatible object with coef_ / intercept_ |
extract_spec(model, ...) |
PyTorch nn.Linear |
extract_spec(layer, ...) |
PyTorch pure-linear nn.Sequential |
extract_spec(seq, ...) |
TensorFlow / Keras Dense (no activation) |
from_weights(layer.get_weights()[0].T, layer.get_weights()[1], ...) |
| JAX / statsmodels / R / anything else | from_weights(coef, intercept, ...) |
All supported model types
Supported: logistic regression, linear SVM, RandomForest, GradientBoosting, DecisionTree, and MLP (ReLU, 2-3 hidden layers). Any framework via weight export.
model_type |
Class | SHAP method |
|---|---|---|
logistic_regression |
sklearn.LogisticRegression |
KernelSHAP (FHE-simulated) |
linear_svc |
sklearn.LinearSVC |
KernelSHAP (FHE-simulated) |
decision_tree |
sklearn.DecisionTreeClassifier/Regressor |
KernelSHAP (plaintext) |
random_forest |
sklearn.RandomForestClassifier/Regressor |
KernelSHAP (plaintext) |
gradient_boosting |
sklearn.GradientBoostingClassifier/Regressor |
KernelSHAP (plaintext) |
mlp (PANCE) |
sklearn.MLPClassifier (ReLU, 2-3 hidden layers) |
KernelSHAP via LP-optimal polynomial activation |
TensorFlow / Keras example
from cipherexplain_sdk import from_weights
# Keras model with a single Dense output layer
import numpy as np
w, b = keras_model.layers[-1].get_weights() # w shape: (n_features, n_classes)
spec = from_weights(
coef=w.T, # transpose to (n_classes, n_features)
intercept=b,
model_id="keras_model",
feature_names=[...],
classes=[0, 1],
)
client.register(spec)
JAX / statsmodels / R example
from cipherexplain_sdk import from_weights
# Pass coefficient array and intercept directly
spec = from_weights(
coef=[[0.42, -0.17, 0.93]], # shape (1, n_features) for binary
intercept=[-0.31],
model_id="my_model",
feature_names=["f1", "f2", "f3"],
classes=[0, 1],
scaler_mean=[0.0, 0.0, 0.0], # optional: embed scaler for /explain_raw
scaler_scale=[1.0, 1.0, 1.0],
)
client.register(spec)
Client reference
client = CipherExplainClient(api_key="vb_...", base_url="https://cipherexplain.vaultbytes.com")
Model management
| Method | Description |
|---|---|
client.register(spec) |
Register a model (spec from extract_spec or from_weights) |
client.list_models() |
List all models available to your key |
client.delete(model_id) |
Delete a registered model and free its slot |
Slot limits: free = 1 model · developer = 10 · enterprise = unlimited
Explanations
| Method | Description |
|---|---|
client.explain(model_id, features) |
SHAP explanation — features must be pre-scaled |
client.explain_raw(model_id, features) |
SHAP explanation — raw values, auto-scaled server-side |
Both return:
{
"prediction": 0.74, # model probability
"base_rate": 0.31, # baseline (training set average)
"shap_values": [...], # per-feature attribution (sum ≈ prediction - base_rate)
"feature_names": [...],
"metadata": {...}
}
MLP fast lane — linear_surrogate=True
For MLP models only, opt in per-request to a rank-1 Jacobian linear surrogate evaluated as a single FHE matmul against the registered baseline:
result = client.explain("my_mlp", x, fhe_mode="ckks", linear_surrogate=True)
print(result["metadata"]["method"]) # "openfhe_ckks_mlp_jacobian_surrogate"
| Path | Wall (EU production cloud) | SHAP L∞ vs exact |
|---|---|---|
| Default (diagonal-encoded coalition-packed, d=50, K=390) | ~73 s | 0 (exact path) |
linear_surrogate=True |
~7 s | 0.062 measured · error_bound=0.15 reported |
The 0.15 reported error_bound is the conservative empirical floor
we sign customer SLAs against. No-op for non-MLP models. If your
accuracy budget is tighter than 0.15, omit the flag and accept the
~73 s wall.
Account
| Method | Description |
|---|---|
client.usage() |
Monthly quota: used / remaining |
client.rotate_key() |
Issue new key, deactivate current. Models migrate automatically. |
client.health() |
API health check — no key required |
client.load_demo_model() |
Load the demo credit model (credit_model) |
Key rotation
result = client.rotate_key()
print(result["new_key"]) # vb_... — save this immediately
print(result["models_migrated"]) # all your models move automatically
# Your old key is now inactive — create a new client
client = CipherExplainClient(api_key=result["new_key"])
Error handling
All methods raise httpx.HTTPStatusError on API errors. Check exc.response.status_code:
import httpx
try:
client.register(spec)
except httpx.HTTPStatusError as exc:
if exc.response.status_code == 401:
print("Missing X-API-Key")
elif exc.response.status_code == 403:
print("Invalid key or tier restriction (e.g. PDF reports need Developer+)")
elif exc.response.status_code == 404:
print("Model not found")
elif exc.response.status_code == 409:
print("Model ID already registered — delete it first or use a different ID")
elif exc.response.status_code == 422:
print("Validation error:", exc.response.json()["detail"])
elif exc.response.status_code == 429:
print("Quota exceeded — model slot limit or monthly call limit reached")
else:
raise
Differential Privacy for Published SHAP Values
Use DP only when you publish SHAP values externally (dashboards, reports, partners). For internal use inside a trust boundary it is unnecessary.
Via the API — apply_dp=True
# Enable DP protection before sharing SHAP values externally
result = client.explain_raw(
"my_model",
x_raw,
apply_dp=True,
dp_epsilon=10.0,
dp_delta=1e-5,
dp_clip_C=0.2,
)
print(result["shap_values"]) # DP-protected values
print(result["dp_sigma"]) # noise std used
print(result["dp_epsilon_used"]) # epsilon applied
# WARNING: only enable apply_dp=True when sharing SHAP values
# with external parties. If using SHAP values internally only,
# FHE already protects the input — DP is not needed.
Client-side — bring your own phi
from cipherexplain_sdk import clip_and_noise, PrivacyAccountant
# phi_hat is the decrypted post-regression SHAP vector (length d).
phi_noisy, sigma = clip_and_noise(phi_hat, C=0.2, epsilon=10.0, delta=1e-5)
acct = PrivacyAccountant(epsilon_per_query=10.0, delta=1e-5)
acct.record_query()
print(acct.total_epsilon(), acct.total_delta())
Recommended parameters: C=0.2, epsilon=10-20, delta=1e-5.
Warnings:
- Do not re-normalize
phi_noisy(e.g. by dividing so the values sum toy_hat - baseline). Re-normalization multiplies noise by an input-dependent factor and breaks DP. The efficiency axiom holds in expectation via zero-mean noise. - Noise is calibrated against the post-regression SHAP sensitivity, which is
Theta(1)in K (Paper 4 column-cancellation lemma), not the pre-regressionTheta(sqrt(K)/n). - Compose budget across queries via
PrivacyAccountant(zCDP accounting).
Tier limits
| Free | Developer | Enterprise | |
|---|---|---|---|
| SHAP API calls / month | 50 | 5,000 | 50,000 |
| Oracle runs / month | 3 | 500 | 5,000 |
| Model slots | 1 | 10 | Unlimited |
| PDF audit reports | — | Yes | Yes |
Get an API key
Metadata
Release files for cipherexplain 0.8.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| cipherexplain-0.8.1.tar.gz | 126.3 kB | With provenance | Details for cipherexplain-0.8.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| cipherexplain-0.8.1-py3-none-any.whl | Python 3 | none | any | Details for cipherexplain-0.8.1-py3-none-any.whl |
Total release size: 271.8 kB
Release files / cipherexplain-0.8.1.tar.gz
| Download URL | cipherexplain-0.8.1.tar.gz |
|---|---|
| Size | 126.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7c21c935a260eb576781cadda1eb17586dafbe68dfa702ff92e8d8ad9b6433e2
|
|
BLAKE2b-256 checksum How to use checksums |
14347c7a7cebcf0091456ec8d5df25c09810cd4ff4eb569f9d0a8f06ad6e3a26
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 19, 2026.
Transparency logRelease files / cipherexplain-0.8.1-py3-none-any.whl
| Download URL | cipherexplain-0.8.1-py3-none-any.whl |
|---|---|
| Size | 145.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8d3b6787577e465494000dece0931d52638a1487e9eda2247264354cc588a0d3
|
|
BLAKE2b-256 checksum How to use checksums |
d499e8bfaab7eb5b29671bdbfc5d75f2a61381c8b58649dfba8d8fdefc2b3d1d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 19, 2026.
Transparency logSecurity for cipherexplain 0.8.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for cipherexplain-0.8.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 19, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.