Metadata-Version: 2.4
Name: invarians
Version: 0.10.0
Summary: 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-URL: Homepage, https://invarians.com
Project-URL: API-docs, https://invarians.com/developers.html
Project-URL: Source, https://github.com/InvariansLabs/invarians-py
License: MIT
Keywords: arbitrum,attestation,autonomous-agents,blockchain,delta-precursors,ethereum,execution-context,infrastructure,optimism,regime-classification
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: pytest-httpx; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: responses; extra == 'dev'
Provides-Extra: httpx
Requires-Dist: httpx>=0.24; extra == 'httpx'
Provides-Extra: requests
Requires-Dist: requests>=2.28; extra == 'requests'
Description-Content-Type: text/markdown

# 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](https://invarians.com/blog/delta-recalibration-eth-arb-cctp-2025.html).

**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).

```python
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

```bash
pip install invarians[requests]  # default
pip install invarians[httpx]     # async-friendly
```

Requires Python 3.9+. Get an API key at [invarians.com](https://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.

```python
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.

```python
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.

```python
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:

```python
# 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

```python
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.

```python
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

```python
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.

```python
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.

```python
# 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.

```python
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

```python
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 when `fires == True`
- `p.cross_chain_held`: True if `cross_chain_status` starts with `PASS_`

Convenience helpers:

```python
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). Shows `cross_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](https://invarians.com/blog/delta-recalibration-eth-arb-cctp-2025.html).

---

## 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 via `client.get_cctp_attestation(message_hash)` and independently verifiable against Circle's published attester public key. `metrics.latency_p90_s / latency_p99_s / success_rate_1h` computed 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`). Source `CCIPSendRequested` matched against destination `ExecutionStateChanged` via bytes32 `messageId`. `metrics.execute_latency_p90_s`, `metrics.sequence_gap`, `metrics.messages_confirmed_1h` derived from per-message data. Per-message rows retrievable via `client.get_ccip_message(message_id)`. `crypto.anchor: null` (no per-message cryptographic anchor captured yet; DON multi-sig `CommitReport` capture 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.

```python
# 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.

```python
# 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

```python
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](https://invarians.com/developers.html)
- Consuming precursors guide: [invarians.com/developers.html#consume-precursors](https://invarians.com/developers.html#consume-precursors)
- Research note (Delta chain-type-exclusivity): [invarians.com/blog/delta-recalibration-eth-arb-cctp-2025.html](https://invarians.com/blog/delta-recalibration-eth-arb-cctp-2025.html)

---

## License

MIT
