Python SDK for the Invarians panel API: three primitives in one signed payload (Attestation, Regime + Bridge State, Delta with per-chain calibrated precursors) plus per-message CCTP (Circle ECDSA) and CCIP (source-dest matched by messageId) retrieval
Project description
invarians-py
Cross-chain infrastructure context for autonomous agents. Three primitives in one signed payload: Attestation, Regime + Bridge State, Delta (per-chain calibrated precursors).
L2 activity no longer shows up in L1 gas fees. Sequencer slowdowns and bridge delays leave no economic trace. Fee monitors stay silent. Invarians detects them.
v0.10.0 (2026-05-20): per-chain calibrated Delta precursors. The v2.0 composite drift block did not pass independent validation on the 2025 ETH-ARB-CCTP and ETH-OP-CCTP corpora (648 pre-engaged configurations, combined Benjamini-Hochberg FDR). Each L1/L2 panel entry now exposes a precursors[] array of axis-specific calibrated configurations, scoped per chain. Six precursors live on Arbitrum (calibrated on ETH-ARB-CCTP 2025, lift 1.53 to 2.36x), one on Optimism (calibrated on ETH-OP-CCTP 2025, lift 3.72x). The two sets are disjoint and configurations do not transfer across corpora: Delta calibration is chain-type-exclusive. The legacy drift.* composite block remains exposed for backward compatibility with a deprecated_unvalidated flag in v3. Full research note: Delta calibration is chain-type-exclusive.
v0.9.0 (2026-05-12): per-message CCIP capture. CCIPSendRequested (source OnRamp) is matched against ExecutionStateChanged (destination OffRamp) via the bytes32 messageId, deriving real send-to-execute latency per lane per direction. CCIP bridges now carry capability_level: per_message_attested, matching the depth previously achieved on CCTP routes in v0.8.0. Per-message rows retrievable via client.get_ccip_message(message_id). crypto.anchor for CCIP is null today (no per-message cryptographic anchor captured yet).
v0.8.0 (2026-05-11): per-message CCTP attestation retrieval with Circle ECDSA crypto-grounding. Each CCTP message exposes its independently verifiable ECDSA signature from Circle's attester. Bridge entries carry a capability_level semantic plus structured metrics and crypto objects.
Since v0.7.0 (2026-05-04), the SDK targets the production endpoint at https://api.invarians.com and exposes the v2.0 panel: a single direction-agnostic payload with axis-grouped metric blocks, 12 signed regime codes per chain, and a per-chain precursors array. Bridge classification scope is variable-latency surfaces only (Chainlink CCIP, Circle CCTP).
from invarians import InvariansClient
client = InvariansClient(api_key="inv_your_key_here")
panel = client.get_panel_v2(include="diagnostic")
eth = panel.l1_by_chain("ethereum")
arb = panel.l2_by_chain("arbitrum")
br = panel.bridge_by_id("arbitrum-ethereum/cctp")
# Regime: 12 signed codes (S1D1, S1D2+, S1D2-, S1D2±, S2+D1, S2-D1, S2+D2+, ...)
if eth.regime and (eth.regime.startswith("S2") or eth.regime.endswith("D2-")):
pause_agent_execution()
# Delta: per-chain calibrated precursors (v3 design)
for p in arb.precursors:
if p.is_firing and p.baseline_lift >= 1.5:
defer_action(reason=p.axis, horizon_hours=p.lead_hours)
print(panel.oracle_status) # "OK" | "DEGRADED"
print(eth.regime, eth.status) # e.g. "S1D1" "OK"
print(br.state, br.calibrated) # e.g. "BS1" True (CCTP per-message)
Install
pip install invarians[requests] # default
pip install invarians[httpx] # async-friendly
Requires Python 3.9+. Get an API key at invarians.com.
The three primitives
The v2.0 panel separates three independent concerns. Every panel response carries all three.
1. Attestation (HMAC integrity)
Every panel response carries a signed_execution_context with payload_hash, signature, key_id, and an optional on-chain anchor. Independently verifiable.
panel = client.get_panel_v2()
sec = panel.signed_execution_context
ok = client.verify_panel_v2(panel_raw_dict, sec.signature)
# True if HMAC matches the canonical JSON of the payload
2. Regime + Bridge State
Per-chain regime is a 2-axis tuple on the SxDx grid. Structure axis: S1 nominal, S2+ structural high, S2- structural low. Demand axis: D1 nominal, D2+ demand high, D2- demand low, D2± composition split. Twelve codes total per chain on both L1 and L2. Bridge state is binary BS1 / BS2 per direction, calibrated on per-message attestation latency.
eth = panel.l1_by_chain("ethereum")
br = panel.bridge_by_id("arbitrum-ethereum/cctp")
if eth.regime == "S2+D1":
# Structural high stress, demand nominal
return {"action": "hold", "reason": eth.regime}
elif br.state == "BS2":
return {"action": "reroute", "reason": "bridge stressed"}
elif eth.regime and eth.regime.startswith("S1") and not eth.regime.endswith("D1"):
# Nominal infrastructure, asymmetric or elevated demand
proceed_with_caution()
3. Delta (per-chain calibrated precursors)
Each L1/L2 entry exposes a precursors[] array of calibrated configurations scoped to that chain. Each precursor carries everything needed to act: the boolean fire flag, the calibration metadata (axis, threshold, lead horizon, predicted outcome, validated lift, precision), and the cross-chain test status. There is no composite Delta score and no aggregation across chains: the agent reads the precursors belonging to the chain it is acting on, applies its own decision policy, and routes accordingly.
arb = panel.l2_by_chain("arbitrum")
# Iterate precursors calibrated on this chain
for p in arb.precursors:
print(p.axis, p.lead_hours, p.outcome_category)
print(p.baseline_lift, p.baseline_precision, p.cross_chain_status)
if p.is_firing:
defer_action(
reason=f"{p.axis} fired",
horizon_hours=p.lead_hours,
expected_precision=p.baseline_precision,
)
# Per-metric raw shifts remain exposed (diagnostic mode)
print(arb.demand.tx.shift) # per-metric deviation vs 30d baseline
print(arb.structural.rhythm.shift) # per-metric rhythm shift
# Legacy v2.0 composite drift block kept for backward compatibility (deprecated_unvalidated in v3)
# print(eth.drift.demand_magnitude_delta) # avoid in new code, use precursors[] instead
Three reference policies for consuming precursors, from strict to permissive:
# Policy A — strict: defer only on cross-chain-validated, high-lift precursors.
for p in arb.precursors:
if p.is_firing and p.baseline_lift >= 2.0 and p.cross_chain_held:
defer_action()
# Policy B — chain-validated default (recommended): defer on any firing precursor with lift >= 1.5.
for p in arb.precursors:
if p.is_firing and p.baseline_lift >= 1.5:
defer_action(reason=p.axis, horizon_hours=p.lead_hours)
# Policy C — max signal (aggressive): flag any firing precursor with lift > 1.0.
for p in arb.precursors:
if p.is_firing and p.baseline_lift > 1.0:
flag_for_review(p)
K-consecutive condition. The payload exposes a single-hour fire check. If a precursor's calibration requires k_consecutive_hours: 2, the agent reads two consecutive panel responses (or polls at 1h intervals) and treats the configuration as fully engaged when both report fires == True.
Common patterns
Hold on structural stress
panel = client.get_panel_v2()
eth = panel.l1_by_chain("ethereum")
if eth.regime and eth.regime.startswith("S2"):
# Structural stress, regardless of polarity (S2+ or S2-)
return {"action": "hold", "reason": eth.regime}
Detect silent slowdown (no fee signal)
S2+D1 and S2-D1 are the codes where infrastructure degrades without any demand signature. Fee monitors stay silent.
eth = client.get_panel_v2().l1_by_chain("ethereum")
if eth.regime in ("S2+D1", "S2-D1"):
# No gas spike, no price move, but the chain is structurally stressed
alert_ops(f"Silent stress on ethereum: {eth.regime}")
Certify execution conditions on chain
panel = client.get_panel_v2()
sec = panel.signed_execution_context
if panel.oracle_status == "OK":
result = execute_trade(...)
audit_log.append({
"tx": result.hash,
"panel_version": panel.version, # "2.0.0"
"issued_at": panel.issued_at,
"payload_hash": sec.payload_hash, # "0x{sha256}"
"signature": sec.signature, # "hmac-sha256:{hex}"
"key_id": sec.key_id,
"anchor": sec.anchor, # on-chain anchor slot when available
})
Route around bridge stress
Bridge IDs are canonical: {chainA}-{chainB}/{type} with type ∈ {ccip, cctp}. Bridge classification scope is variable-latency surfaces only.
panel = client.get_panel_v2(bridges=["ccip", "cctp"])
# CCTP: per-message capture since 2026-05-11
# capability_level = "per_message_attested", crypto.anchor = "circle_ecdsa"
# metrics.latency_p90_s / latency_p99_s / success_rate_1h are computed on per-message latencies
br = panel.bridge_by_id("arbitrum-ethereum/cctp")
if br and br.state == "BS2":
use_fallback_route()
if br and br.metrics and br.metrics.latency_p90_s and br.metrics.latency_p90_s > 1200:
log.warning(f"{br.id}: attestation P90 {br.metrics.latency_p90_s:.0f}s above 20-min threshold")
# CCIP: capability_level = "per_message_attested" since 2026-05-12
# metrics.execute_latency_p90_s = source-to-execute P90 derived from messageId matching
# metrics.sequence_gap and metrics.messages_confirmed_1h derived from per-message data
# crypto.anchor = null (no per-message cryptographic anchor captured yet)
ccip = panel.bridge_by_id("ethereum-arbitrum/ccip")
if ccip and ccip.state == "BS2":
use_fallback_route()
if ccip and ccip.metrics and ccip.metrics.execute_latency_p90_s and ccip.metrics.execute_latency_p90_s > 1800:
log.warning(f"{ccip.id}: send-to-execute P90 {ccip.metrics.execute_latency_p90_s:.0f}s above 30-min threshold")
# RMN cursed override (CCIP only), absolute binary, lane is frozen
if ccip and ccip.is_frozen:
block_ccip_route()
Retrieve a CCTP per-message ECDSA attestation
Each attested CCTP message exposes a Circle ECDSA signature (65-byte secp256k1), independently verifiable against Circle's published attester public key.
# Lookup by message hash (32-byte keccak256, 0x prefix optional)
att = client.get_cctp_attestation("0x654c0c87fb7895ec703d200469e8ef2b57876e06ad88b65e74e6e515f0ee510e")
print(att["status"]) # "attested" or "pending"
print(att["source_chain"], "→", att["dest_chain"])
print(att["attestation"]["signature"]) # "0x..." 65-byte ECDSA secp256k1
print(att["attestation"]["latency_ms"]) # observed source_block → Iris attestation latency
print(att["attestation"]["verification_url"]) # https://iris-api.circle.com/attestations/...
Retrieve a CCIP per-message row
Each CCIP message tracked by Invarians can be retrieved by its bytes32 messageId. Source send metadata is always present; destination metadata is filled when the message is executed.
msg = client.get_ccip_message("0x03b7a89cf45aeac5898c57fcb4deafabf3b6d3ac1ecb5e70cebd86e769fea5b1")
print(msg["status"]) # "pending" or "executed"
print(msg["source_chain"], "→", msg["dest_chain"])
print(msg["sequence_number"], msg["nonce"])
print(msg["sender"], "→", msg["receiver"])
if msg["status"] == "executed":
print(msg["dest_tx_hash"], msg["execution_state"]) # 2 = Success, 3 = Failure
Handle degraded data gracefully
panel = client.get_panel_v2()
if panel.oracle_status == "DEGRADED":
for entry in panel.l1 + panel.l2:
if entry.status != "OK":
log.warning(f"{entry.chain}: {entry.status}")
for br in panel.bridges:
if br.status in ("STALE", "UNAVAILABLE"):
log.warning(f"{br.id}: {br.status}")
fall_back_to_conservative_mode()
Per-item status values:
| Status | Meaning |
|---|---|
OK |
Signal fresh and calibrated |
STALE |
Last update older than the freshness window (1h) |
UNAVAILABLE |
Signal temporarily missing |
UNCALIBRATED |
Collector running, thresholds not yet published. Does not trigger DEGRADED. |
Regime grid
12 signed codes per chain on both L1 and L2.
| Code | Structure | Demand | What it captures |
|---|---|---|---|
S1D1 |
nominal | nominal | Within calibrated norms |
S1D2+ |
nominal | high | Demand surge, infrastructure healthy |
S1D2- |
nominal | low | Demand depressed, infrastructure healthy |
S1D2± |
nominal | split | Asymmetric demand composition |
S2+D1 |
high stress | nominal | Silent structural slowdown. No fee signal. |
S2-D1 |
low stress | nominal | Silent structural underrun. No fee signal. |
S2+D2+ |
high stress | high | Combined upward stress |
S2+D2- |
high stress | low | Stress with depressed demand |
S2+D2± |
high stress | split | Stress with asymmetric demand |
S2-D2+ |
low stress | high | Underrun with elevated demand |
S2-D2- |
low stress | low | Underrun with depressed demand |
S2-D2± |
low stress | split | Underrun with asymmetric demand |
Bridge states (variable-latency surfaces only):
| Code | Type | Meaning |
|---|---|---|
BS1 |
ccip / cctp | Within calibrated latency threshold |
BS2 |
ccip / cctp | Above calibrated latency threshold |
null |
any | Not yet calibrated. Raw signals still exposed on the entry. |
Delta precursors (v0.10.0)
Each L1/L2 entry exposes precursors: List[DeltaPrecursor]. Each DeltaPrecursor carries:
| Field | Type | Meaning |
|---|---|---|
axis |
str | Substrate metric axis (e.g. "arb_struct_seq_publish_latency_shift") |
fires |
Optional[bool] | True if current shift_magnitude_delta on axis exceeds smd_threshold_value. None if upstream unavailable. |
current_smd |
Optional[float] | Current value of shift_magnitude_delta |
smd_threshold_value |
Optional[float] | Calibrated threshold (P-quantile on calibration corpus) |
k_consecutive_hours |
int | Number of consecutive hours required for full engagement |
pctl_threshold |
float | Calibrated quantile (e.g. 0.90) |
lead_hours |
int | Horizon over which the outcome is predicted |
outcome_category |
str | What the precursor predicts: latency_high_only, bs2_only, bridge_stress_full, or directional bridge_<src>_to_<dst> |
bridge_corridor |
str | Corridor on which the outcome was evaluated, e.g. "ETH-ARB-CCTP" |
baseline_lift |
float | Lift on the calibration corpus (precision / unconditional outcome rate) |
baseline_p_adj |
float | Combined BH FDR-adjusted p-value |
baseline_precision |
Optional[float] | Precision on the calibration corpus |
baseline_alert_rate |
Optional[float] | Alert rate on the calibration corpus |
cross_chain_status |
str | NOT_TESTED, PASS_on_<chain>, or FAIL_on_<chain> |
cross_chain_lift |
Optional[float] | Lift observed in the cross-chain test |
cross_chain_placebo_p |
Optional[float] | Placebo p-value in the cross-chain test |
calibrated_at |
str | ISO timestamp of calibration registry entry |
Convenience properties:
p.is_firing: True only whenfires == Truep.cross_chain_held: True ifcross_chain_statusstarts withPASS_
Convenience helpers:
from invarians import firing_precursors
active = firing_precursors(arb.precursors)
Calibration status (2026-05-20):
- arbitrum: 6 precursors calibrated on ETH-ARB-CCTP 2025 (lift 1.53 to 2.36x). All show
cross_chain_status: FAIL_on_optimism(do not generalize to OP). - optimism: 1 precursor calibrated on ETH-OP-CCTP 2025 (axis
eth_struct_continuity_shift, lift 3.72x). Showscross_chain_status: FAIL_on_arbitrum. - ethereum / base / polygon / avalanche / solana: no precursors calibrated yet (per-chain registry, each chain warrants its own discovery pass).
Delta calibration is chain-type-exclusive. Full empirical evidence: Delta calibration is chain-type-exclusive: ETH-ARB-CCTP and ETH-OP-CCTP, 2025.
Bridge calibration status
- CCTP: per-message capture since 2026-05-11 (
capability_level: per_message_attested). Each message exposes a Circle ECDSA signature (crypto.anchor: "circle_ecdsa"), retrievable viaclient.get_cctp_attestation(message_hash)and independently verifiable against Circle's published attester public key.metrics.latency_p90_s / latency_p99_s / success_rate_1hcomputed on per-message latencies. Confidence MEDIUM (EVM only; Solana routes scheduled 2026-Q3). - CCIP: per-message capture since 2026-05-12 (
capability_level: per_message_attested). SourceCCIPSendRequestedmatched against destinationExecutionStateChangedvia bytes32messageId.metrics.execute_latency_p90_s,metrics.sequence_gap,metrics.messages_confirmed_1hderived from per-message data. Per-message rows retrievable viaclient.get_ccip_message(message_id).crypto.anchor: null(no per-message cryptographic anchor captured yet; DON multi-sigCommitReportcapture is the next step). RMN cursed override remains absolute.
Chain coverage
| Chain | Layer | Confidence | Status |
|---|---|---|---|
| ethereum | L1 | HIGH | live |
| polygon | L1 | MEDIUM | live |
| arbitrum | L2 | MEDIUM | live |
| base | L2 | MEDIUM | live |
| optimism | L2 | MEDIUM | live |
| avalanche | L1 | LOW | observation |
| solana | L1 | LOW | calibration target Q3 2026 |
Bridges live (variable-latency scope, 20 lanes total): 10 CCTP routes (per-message ECDSA capture since 2026-05-11, capability_level: per_message_attested) and 10 CCIP lanes (per-message capture since 2026-05-12, capability_level: per_message_attested).
Migrating from v0.9.x to v0.10.0
v0.10.0 is additive. No breaking change. The legacy drift.* composite block stays exposed for backward compatibility, flagged deprecated_unvalidated in the v3 payload. New code should iterate on the precursors[] array per chain.
# v0.9.x pattern (still works, deprecated)
if eth.drift.demand_magnitude_delta > 0.05:
defer()
# v0.10.0 pattern (recommended)
for p in arb.precursors:
if p.is_firing and p.baseline_lift >= 1.5:
defer(reason=p.axis, horizon_hours=p.lead_hours)
The composite drift block will be removed in a future v0.11.0 release. No exact date set; the deprecation window stays open as long as some consumers still read it.
Migrating from v0.6.x to v0.7.x
v0.7.0 narrowed the bridge scope to variable-latency surfaces (CCIP, CCTP). Native L2-to-L1 bridges operate on protocol-defined timeframes outside any observability lever and are removed from the panel. Several types and fields are removed accordingly.
# Removed types
# from invarians import CcipState, CctpState, AnyBridgeState # gone, use BridgeState
# Removed fields on BridgeEntry
# br.last_batch_age_seconds # gone (native-only signal)
# Removed fields on Coverage / V2Coverage
# coverage.bridges_native # gone
# BridgeType is now Literal["ccip", "cctp"] (was: "native" | "ccip" | "cctp")
# State codes CS1 / CS2 (CCIP) and TS1 / TS2 (CCTP) are unified as BS1 / BS2
If you previously did bridge_by_id("arbitrum-ethereum/native"), switch to a CCTP route ("...-ethereum/cctp") or a CCIP lane ("...-ethereum/ccip").
Error handling
from invarians.exceptions import AuthError, RateLimitError, ServerError
try:
panel = client.get_panel_v2()
except AuthError:
print("Invalid API key")
except RateLimitError:
print("Quota exceeded. Free tier: 20 req/day")
except ServerError as e:
print(f"Service unavailable: {e}")
Documentation
- API reference: invarians.com/developers.html
- Consuming precursors guide: invarians.com/developers.html#consume-precursors
- Research note (Delta chain-type-exclusivity): invarians.com/blog/delta-recalibration-eth-arb-cctp-2025.html
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file invarians-0.10.0.tar.gz.
File metadata
- Download URL: invarians-0.10.0.tar.gz
- Upload date:
- Size: 31.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
41665f89e4494f6382804b5b9e43a32368359dbdc62d3f08e4325597f1c17e3d
|
|
| MD5 |
dd39b936f5f1b39b8c61b07eee57d4b5
|
|
| BLAKE2b-256 |
9c6e0d8dc80bd277b188d5fb708186d9b554186876883c683cf9d26765cc2f88
|
File details
Details for the file invarians-0.10.0-py3-none-any.whl.
File metadata
- Download URL: invarians-0.10.0-py3-none-any.whl
- Upload date:
- Size: 27.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
810fe2ccfef49fbec191d9f3c1d75f21542cc409b3bac4540d5faf028223fde9
|
|
| MD5 |
6c7e2f9f05197718a1b20e3a2965f5e7
|
|
| BLAKE2b-256 |
377a20ffda7b79efee8a4c6cbe4d0b520e9cd89b71d5bd3b0e1addc3b5debaef
|