L4 Compliance Bundle
L4 is the bundle. It is the single payload that travels with every payment. Identity from L1, policy verdict from L2, compliance attestation from L3, revocation witness from L5, all bound into one signed object. v1 signs with Ed25519. v2 will add ZK proofs without changing the wire format.
What it does
The Bundle Assembler is an orchestrator. Given a tx_intent, it:
- Fetches the policy verdict from L2 (with the Merkle inclusion proof).
- Calls L3 for the Veris attestation (BLS signed).
- Reads the L5 revocation root and produces a non-membership witness.
- Reads L1 for the agent DID and tenant node.
- Encodes a canonical 196-byte public input.
- Signs the bundle with an Ed25519 recursive signature (96 bytes).
- Hashes the bundle twice (Keccak256 + SHA-256) for cross-chain ID parity.
- Returns the bundle plus a sealed envelope copy for L7 audit.
Wire format
The schema is locked. Every field has a fixed offset in the public input. Adding a field requires a protocol-level version bump.
ComplianceBundle v2 (1.5 KB total, 196-byte public input)
version: "v2" agent_did: "did:ethr:<chain_id>:<agent_addr>" // L1 tenant_node: bytes32 // L1, namehash
policy_root: bytes32 // L2 policy_proof: bytes // L2 inclusion proof
veris_attestation: { // L3, BLS-signed subject: did_or_address sanctions_clean: bool risk_tier: enum drift_score_bp: u16 evaluated_at: u64 expires_at: u64 signer_pubkey: bytes signature: bytes }
tx_intent: { counterparty: address amount_usd_e6: u64 stablecoin: bytes4 chain_id: u64 category: bytes32 nonce: bytes32 // Redis SETNX 30 s expires_at: u64 }
revocation_witness: { // L5 tier: u8 // 1 immediate, 2 behavioural root: bytes32 not_present_proof: bytes tree_size: u64 }
public_verdict: { kya_level: u8 kya_status: bytes32 not_revoked: bool sanctions_clean: bool counterparty_allowed:bool amount_under_cap: bool }
proof_type: u8 // 0x01 Ed25519, 0x02 SP1, 0x03 Halo2 proof: bytes // 96 bytes for Ed25519The 196 bytes of public_verdict plus the canonical hash of every other field is what the proof signs. Verifiers read the public input from the bundle, recompute the canonical hash, and check the proof.
Bundle assembly latency
| Step | Budget |
|---|---|
| L1 identity fetch | 2 ms |
| L2 policy verdict | 4 ms |
| L3 Veris attestation (network) | 6 ms |
| L5 revocation witness | 2 ms |
| Canonical bytes encode | 1 ms |
| Ed25519 sign | 1 ms |
| Dual-hash bundle id | 1 ms |
| AGE seal for audit | 4 ms |
| Audit log write (async) | 0 ms (fire and forget) |
| p95 total | < 50 ms |
Bundle id
Every bundle has a deterministic id used as a primary key across all layers:
bundle_id_keccak = keccak256(canonical_bytes)bundle_id_sha256 = sha256(canonical_bytes)Two hashes because the L1 chains expect Keccak256 but some non-EVM rails verify SHA-256 by default. Both ids are recorded. Either resolves to the same bundle.
Sealed envelope
L4 also produces a sealed envelope for the audit log. The envelope is AGE-encrypted with the regulator quorum’s public keys (typically 3-of-5 threshold). Inside the envelope:
- the full unredacted bundle,
- the KYC artifacts that back the agent identity,
- the operator signing identity that approved the policy,
- any escalation approvals that fired.
The envelope is unsealable only with the regulator quorum’s signatures. Oris cannot read it. See sealed envelope for the disclosure flow.
SDK example
from oris import OrisClient
client = OrisClient(...)
# Build a bundle without sending it (e.g. to verify offline first)bundle = client.bundles.assemble( agent_id=agent.id, to_address="0xA1b2...", amount=12.50, chain="base-sepolia", category="api_consumption",)
print(bundle.id_keccak) # 0x...print(bundle.id_sha256) # 0x...print(bundle.proof_type) # 1 (Ed25519)print(bundle.canonical_bytes_hex)print(bundle.signature_hex) # 96 bytesimport { OrisClient } from 'oris-sdk';
const client = new OrisClient({ ... });
const bundle = await client.bundles.assemble({ agentId: agent.id, toAddress: '0xA1b2...', amount: 12.50, chain: 'base-sepolia', category: 'api_consumption',});
console.log(bundle.idKeccak);console.log(bundle.idSha256);console.log(bundle.proofType);console.log(bundle.canonicalBytesHex);console.log(bundle.signatureHex);Proof types and the ZK boundary
proof_type = 0x01 → Ed25519 recursive sig (LIVE)proof_type = 0x02 → SP1 zkVM (v2 candidate)proof_type = 0x03 → Halo2 recursive (v2 candidate)The wire format does not change between v1 and v2. Only the proof field flips from 96 bytes of Ed25519 to whatever the ZK system produces. The L6 verifier dispatches on proof_type and runs the right verification path.
v2 is partner-led. SDK code does not change.
Where to go next
- L5 Revocation for the witness that proves no party is revoked at bundle time.
- L6 Verifier for how the bundle is verified by the network.
- Bundle verification guide for the end-to-end fetch + verify flow.