Usage Guide

Learn how to use the Charli3 Oracle Data Verification (ODV) Client SDK to collect feed data and submit aggregation transactions.

Prerequisites

  • SDK installed and configured (Setup Guide)
  • Valid config.yaml file
  • Wallet funded with ~5 ADA for transaction fees
  • Oracle network deployed and operational

CLI Usage

The SDK provides three main commands:

  1. validate-config - Validate configuration
  2. feeds - Collect oracle feed data
  3. aggregate - Execute full aggregation workflow

Validate Configuration

Verify your configuration is correct before operations:

charli3 validate-config --config config.yaml

Output:

Configuration Validation

Setting               Value                                      Status
Network               preprod                                    Valid
Oracle Address        addr_test1wp...                           Valid
Policy ID             b00f27e5c228...                           Valid
Validity Length       120000ms                                   Valid
Nodes Count           3                                          Valid

✓ Configuration is valid

Node Endpoints:
    1. https://oracle-node-1.testnet.charli3.io
    2. https://oracle-node-2.testnet.charli3.io
    3. https://oracle-node-3.testnet.charli3.io

Collect Feed Data

Collect current oracle feed data from nodes without submitting a transaction:

charli3 feeds --config config.yaml

Basic usage:

# Collect feeds with default settings
charli3 feeds --config config.yaml
 
# Save feed data to file for later use
charli3 feeds --config config.yaml --output feeds.json
 
# Verbose output showing all node responses
charli3 feeds --config config.yaml --verbose
 
# Override oracle policy ID
charli3 feeds --config config.yaml --policy-id <policy_id>
 
# Custom validity window (in milliseconds)
charli3 feeds --config config.yaml --validity-length 180000

Output:

INFO: Collecting Oracle Feed Data
INFO: Policy ID: b00f27e5c2284f87b29c2b877dd341e3f0c3d06e1e0b02bb9c458f13
INFO: Validity window: 120000ms
INFO: Target nodes: 3

[Collecting feed updates...]

SUCCESS: Feed collection complete
    Received: 3 responses
    Aggregate feeds: 3

Calculated median: 1.234567

With --verbose flag:

Node Feed Responses:
  bd72e960d6237031290e8b4189ea7c6c305ef907d4881729db60e3f3b7c4af5b: 1.234500
  7437e43e7e88c5642569f112f0e22b17c2fae155007254506dd0090dd9312568: 1.234600
  76502e17aba981ecbf09fd5b41bc1b6a3f5a0a85bb47c07300b906a347969702: 1.234600

Saved feed data format (feeds.json):

{
  "node_messages": {
    "bd72e960d6...": {
      "message": "0123abcd...",
      "signature": "4567efgh...",
      "verification_key": "89abijkl..."
    }
  },
  "aggregate_message": {
    "node_feeds_count": 3,
    "feeds": {
      "007df380...": 1234567,
      "b296714e...": 1234600
    }
  }
}

Execute Aggregation Workflow

Submit an aggregation transaction to update on-chain oracle data:

charli3 aggregate --config config.yaml

The SDK automatically:

  1. Collects feed data from oracle nodes
  2. Builds aggregation transaction
  3. Collects signatures from nodes
  4. Prompts for submission confirmation
  5. Signs and submits transaction
  6. Waits for confirmation

Command options:

# Full aggregation with confirmation prompt
charli3 aggregate --config config.yaml
 
# Auto-submit without confirmation
charli3 aggregate --config config.yaml --auto-submit
 
# Use previously saved feed data
charli3 aggregate --config config.yaml --feed-data feeds.json
 
# Save transaction to specific file
charli3 aggregate --config config.yaml --output my_transaction.cbor
 
# Override wallet key
charli3 aggregate --config config.yaml --wallet-key payment.skey
 
# Verbose output showing all details
charli3 aggregate --config config.yaml --verbose

Typical workflow output:

[Loading configuration...]

SUCCESS: Configuration and components initialized
    Oracle Configuration:
      Policy ID: b00f27e5c2284f87b29c2b877dd341e3f0c3d06e1e0b02bb9c458f13
      Oracle Address: addr_test1wpgjxstaqc8am88c89zd72j5rw065ex8l5j7ln4pzngxt8qr4tzdj
      Target nodes: 3

[Collecting feed data from oracle nodes...]

SUCCESS: Collected 3 feed responses
    Node Feed Responses:
      bd72e960d6...c4af5b: 1.234500
      7437e43e7e...312568: 1.234600
      76502e17ab...969702: 1.234600

[Building aggregation transaction...]

SUCCESS: Transaction constructed
    Transaction ID: a1b2c3d4e5f6...
    Median value: 1.234567

[Collecting signatures from oracle nodes...]

SUCCESS: Collected 3 signatures

INFO: Attaching signatures to transaction

SUCCESS: Transaction saved to odv_transaction_b00f27e5.cbor

┌─ Transaction Preview ─────────────────────────────────────┐
│                                                            │
│ Transaction Ready for Submission                          │
│                                                            │
│ Transaction ID:                                           │
│ a1b2c3d4e5f67890...                                       │
│                                                            │
│ Oracle Feed Details:                                      │
│   • Median Value: 1.234567                               │
│   • Node Responses: 3                                     │
│   • Signatures Collected: 3                               │
│                                                            │
│ Transaction Details:                                      │
│   • Saved to: odv_transaction_b00f27e5.cbor              │
│                                                            │
│ ⚠️  This action will submit the transaction to the       │
│     blockchain                                            │
│                                                            │
└────────────────────────────────────────────────────────────┘

Submit transaction to blockchain? [Y/n]: 

After confirmation:

[Submitting transaction to blockchain...]

SUCCESS: Transaction confirmed on blockchain

┌─ Aggregation Summary ──────────────────────────────────────┐
│                                                             │
│ ODV Aggregation Complete                                   │
│                                                             │
│ Transaction ID:                                            │
│ a1b2c3d4e5f67890...                                        │
│                                                             │
│ Median Value: 1.234567                                     │
│ Node Responses: 3                                          │
│ Signatures: 3                                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Auto-submit mode (--auto-submit):

charli3 aggregate --config config.yaml --auto-submit

Skips confirmation prompt and immediately submits the transaction.

Using saved feed data:

# First, collect and save feeds
charli3 feeds --config config.yaml --output feeds.json
 
# Later, use saved data for aggregation
charli3 aggregate --config config.yaml --feed-data feeds.json

This is useful for:

  • Testing transaction building without fresh data collection
  • Reusing feed data if initial submission failed
  • Debugging aggregation logic

Transaction Output Files

All transactions are saved to .cbor files:

Default naming:

odv_transaction_<policy_id_prefix>.cbor

Manual submission:

# Using cardano-cli
cardano-cli transaction submit \
  --tx-file odv_transaction_b00f27e5.cbor \
  --testnet-magic 1
 
# Or via block explorer
# Copy CBOR content and submit via web interface

Programmatic Usage

Integrate ODV Client SDK directly into your Python applications:

Basic Integration

import asyncio
from pathlib import Path
from charli3_odv_client.config import ODVClientConfig
from charli3_odv_client.core.client import ODVClient
from charli3_odv_client.models.requests import OdvFeedRequest
from charli3_odv_client.models.base import TxValidityInterval
 
async def collect_feeds():
    # Load configuration
    config = ODVClientConfig.from_yaml(Path("config.yaml"))
    
    # Initialize client
    client = ODVClient()
    
    # Create feed request
    feed_request = OdvFeedRequest(
        oracle_nft_policy_id=config.policy_id,
        tx_validity_interval=TxValidityInterval(
            start=1640000000000,  # POSIX ms
            end=1640000120000
        )
    )
    
    # Collect feeds from nodes
    node_messages = await client.collect_feed_updates(
        nodes=config.nodes,
        feed_request=feed_request
    )
    
    print(f"Collected {len(node_messages)} feed responses")
    
    for pub_key, msg in node_messages.items():
        feed_value = msg.message.feed / 1_000_000
        print(f"Node {pub_key[:16]}...: {feed_value:.6f}")
    
    return node_messages
 
# Run
asyncio.run(collect_feeds())

Full Aggregation Workflow

import asyncio
from pathlib import Path
from charli3_odv_client.config import ODVClientConfig, ReferenceScriptConfig, KeyManager
from charli3_odv_client.core.client import ODVClient
from charli3_odv_client.models.requests import OdvFeedRequest
from charli3_odv_client.cli.utils.shared import (
    create_chain_query,
    setup_transaction_builder
)
 
async def submit_aggregation():
    # Load configuration
    config = ODVClientConfig.from_yaml(Path("config.yaml"))
    ref_script_config = ReferenceScriptConfig.from_yaml(Path("config.yaml"))
    
    # Initialize components
    client = ODVClient()
    chain_query = create_chain_query(config)
    tx_manager, tx_builder = setup_transaction_builder(
        config, ref_script_config, chain_query
    )
    
    # Load wallet
    signing_key, _, _, change_address = KeyManager.load_from_config(
        config.wallet
    )
    
    # Calculate validity window
    validity_window = tx_manager.calculate_validity_window(
        config.odv_validity_length
    )
    
    # Step 1: Collect feeds
    feed_request = OdvFeedRequest(
        oracle_nft_policy_id=config.policy_id,
        tx_validity_interval=TxValidityInterval(
            start=validity_window.validity_start,
            end=validity_window.validity_end
        )
    )
    
    node_messages = await client.collect_feed_updates(
        nodes=config.nodes,
        feed_request=feed_request
    )
    
    if not node_messages:
        raise Exception("No valid node responses received")
    
    print(f"Collected {len(node_messages)} feed responses")
    
    # Step 2: Build transaction
    odv_result = await tx_builder.build_odv_tx(
        node_messages=node_messages,
        signing_key=signing_key,
        change_address=change_address,
        validity_window=validity_window
    )
    
    print(f"Transaction built: {odv_result.transaction.id}")
    print(f"Median value: {odv_result.median_value / 1_000_000:.6f}")
    
    # Step 3: Collect signatures
    from charli3_odv_client.models.requests import OdvTxSignatureRequest
    
    tx_request = OdvTxSignatureRequest(
        node_messages=node_messages,
        tx_body_cbor=odv_result.transaction.transaction_body.to_cbor_hex()
    )
    
    signatures = await client.collect_tx_signatures(
        nodes=config.nodes,
        tx_request=tx_request
    )
    
    if not signatures:
        raise Exception("No valid signatures received")
    
    print(f"Collected {len(signatures)} signatures")
    
    # Step 4: Attach signatures
    odv_result.transaction = client.attach_signature_witnesses(
        original_tx=odv_result.transaction,
        signatures=signatures,
        node_messages=node_messages
    )
    
    # Step 5: Submit transaction
    status, submitted_tx = await tx_manager.sign_and_submit(
        odv_result.transaction,
        signing_keys=[signing_key],
        wait_confirmation=True
    )
    
    if status == "confirmed":
        print(f"Transaction confirmed: {submitted_tx.id}")
        return submitted_tx
    else:
        raise Exception(f"Transaction failed with status: {status}")
 
# Run
asyncio.run(submit_aggregation())

Custom Feed Collection

import asyncio
from charli3_odv_client.config import ODVClientConfig, NodeConfig
from charli3_odv_client.core.client import ODVClient
from charli3_odv_client.models.requests import OdvFeedRequest
from charli3_odv_client.models.base import TxValidityInterval
 
async def collect_from_specific_nodes():
    # Create custom node configuration
    nodes = [
        NodeConfig(
            root_url="https://oracle-node-1.testnet.charli3.io",
            pub_key="bd72e960d6237031290e8b4189ea7c6c305ef907d4881729db60e3f3b7c4af5b"
        ),
        NodeConfig(
            root_url="https://oracle-node-2.testnet.charli3.io",
            pub_key="7437e43e7e88c5642569f112f0e22b17c2fae155007254506dd0090dd9312568"
        )
    ]
    
    # Initialize client
    client = ODVClient()
    
    # Create request
    feed_request = OdvFeedRequest(
        oracle_nft_policy_id="b00f27e5c2284f87b29c2b877dd341e3f0c3d06e1e0b02bb9c458f13",
        tx_validity_interval=TxValidityInterval(
            start=1640000000000,
            end=1640000120000
        )
    )
    
    # Collect feeds
    node_messages = await client.collect_feed_updates(
        nodes=nodes,
        feed_request=feed_request
    )
    
    return node_messages
 
# Run
asyncio.run(collect_from_specific_nodes())

Calculate Aggregate Statistics

from charli3_odv_client.core.aggregation import (
    build_aggregate_message,
    calculate_median
)
 
def analyze_feed_data(node_messages):
    # Build aggregate message
    aggregate_message = build_aggregate_message(
        list(node_messages.values())
    )
    
    # Get feed values
    feeds = [msg.message.feed for msg in node_messages.values()]
    
    # Calculate median
    median_value = calculate_median(feeds)
    
    print(f"Total nodes: {aggregate_message.node_feeds_count}")
    print(f"Median value: {median_value / 1_000_000:.6f}")
    print(f"Min value: {min(feeds) / 1_000_000:.6f}")
    print(f"Max value: {max(feeds) / 1_000_000:.6f}")
    
    return aggregate_message

Error Handling

import asyncio
from charli3_odv_client.exceptions import (
    NetworkError,
    ValidationError,
    OracleTransactionError,
    StateValidationError
)
 
async def robust_aggregation():
    try:
        # Your aggregation logic here
        pass
        
    except NetworkError as e:
        print(f"Network error: {e}")
        print("Check node connectivity and API endpoints")
        
    except StateValidationError as e:
        print(f"State validation error: {e}")
        print("Oracle state may not be ready for aggregation")
        
    except ValidationError as e:
        print(f"Validation error: {e}")
        print("Check configuration parameters")
        
    except OracleTransactionError as e:
        print(f"Transaction error: {e}")
        print("Review transaction parameters and try again")
        
    except Exception as e:
        print(f"Unexpected error: {e}")
        raise

Integration Patterns

Scheduled Aggregations

import asyncio
import schedule
import time
from datetime import datetime
 
async def scheduled_aggregation():
    print(f"[{datetime.now()}] Running scheduled aggregation")
    try:
        # Your aggregation logic
        await submit_aggregation()
        print(f"[{datetime.now()}] Aggregation successful")
    except Exception as e:
        print(f"[{datetime.now()}] Aggregation failed: {e}")
 
def run_scheduler():
    # Schedule aggregation every hour
    schedule.every().hour.at(":00").do(
        lambda: asyncio.run(scheduled_aggregation())
    )
    
    while True:
        schedule.run_pending()
        time.sleep(60)
 
# Run scheduler
run_scheduler()

Conditional Aggregation

async def conditional_aggregation(threshold_change=0.01):
    """
    Submit aggregation only if median value changed significantly.
    """
    
    config = ODVClientConfig.from_yaml(Path("config.yaml"))
    client = ODVClient()
    
    # Get current on-chain value
    chain_query = create_chain_query(config)
    # ... query current oracle state
    current_value = 1.234567  # Retrieved from chain
    
    # Collect new feeds
    validity_window = calculate_validity_window(config.odv_validity_length)
    feed_request = OdvFeedRequest(
        oracle_nft_policy_id=config.policy_id,
        tx_validity_interval=TxValidityInterval(
            start=validity_window.validity_start,
            end=validity_window.validity_end
        )
    )
    
    node_messages = await client.collect_feed_updates(
        nodes=config.nodes,
        feed_request=feed_request
    )
    
    # Calculate new median
    feeds = [msg.message.feed for msg in node_messages.values()]
    new_median = calculate_median(feeds) / 1_000_000
    
    # Check if change exceeds threshold
    change = abs(new_median - current_value) / current_value
    
    if change > threshold_change:
        print(f"Significant change detected: {change:.2%}")
        print(f"Submitting aggregation: {current_value}{new_median}")
        # Submit aggregation
        await submit_aggregation()
    else:
        print(f"Change below threshold: {change:.2%}")
        print("Skipping aggregation")

Common Workflows

1. One-Time Aggregation

# Validate configuration
charli3 validate-config --config config.yaml
 
# Submit aggregation with confirmation
charli3 aggregate --config config.yaml

2. Automated Aggregations

# Save as aggregation script
cat > aggregate.sh << 'EOF'
#!/bin/bash
charli3 aggregate \
  --config /path/to/config.yaml \
  --auto-submit \
  --output "/logs/tx_$(date +%Y%m%d_%H%M%S).cbor"
EOF
 
chmod +x aggregate.sh
 
# Schedule with cron (every hour)
crontab -e
# Add: 0 * * * * /path/to/aggregate.sh >> /logs/aggregation.log 2>&1

Troubleshooting

No Node Responses

Symptoms:

ERROR: Feed collection failed
    Received: 0 responses

Solutions:

  1. Verify node URLs are accessible:
    curl https://oracle-node-1.testnet.charli3.io/health
  2. Check node public keys match configuration
  3. Ensure validity window is reasonable (not in past/future)
  4. Verify oracle is operational on-chain

Insufficient Signatures

Symptoms:

ERROR: No valid signatures received

Solutions:

  1. Verify nodes can access transaction body
  2. Check node public keys match oracle deployment
  3. Ensure transaction is valid (check node logs)
  4. Verify validity window overlaps with node response times

Transaction Building Failed

Symptoms:

ERROR: Failed to build ODV transaction

Solutions:

  1. Check oracle has available UTXO pairs:
    # Query oracle address for UTXOs
    cardano-cli query utxo --address <oracle_address>
  2. Verify reference script exists and is accessible
  3. Ensure wallet has sufficient funds
  4. Check oracle isn’t paused or in closing state

State Validation Error

Symptoms:

ERROR: No empty transport UTxO found

Solutions:

  1. Wait for current aggregations to complete
  2. Check if oracle needs reward processing
  3. Verify oracle is deployed and operational
  4. Scale up oracle capacity if needed

Network Connectivity Issues

Symptoms:

ERROR: Network error: Connection refused

Solutions:

  1. For Blockfrost:

    • Verify project ID is correct
    • Check API rate limits
    • Test API connectivity: curl -H "project_id: <id>" https://cardano-preprod.blockfrost.io/api/v0/health
  2. For Ogmios/Kupo:

    • Verify services are running: curl http://localhost:1442/health
    • Check ports are accessible
    • Ensure correct network (testnet/mainnet)

Transaction Submission Failed

Symptoms:

ERROR: Transaction submission failed
    Status: failed

Solutions:

  1. Check wallet has sufficient ADA
  2. Verify transaction isn’t expired
  3. Ensure reference script is accessible
  4. Check oracle state is valid for aggregation
  5. Review node logs for rejection reasons

Best Practices

1. Configuration Management

  • Use environment variables for sensitive data
  • Version control config templates (without secrets)
  • Separate configs per environment (dev/staging/prod)
  • Document custom settings and their purposes

2. Error Recovery

  • Save transaction CBORs before submission
  • Log all operations with timestamps
  • Implement retry logic with exponential backoff
  • Monitor for failed aggregations and alert

3. Cost Optimization

  • Use reference scripts to reduce tx sizes
  • Batch operations when possible
  • Monitor ADA consumption
  • Set up alerts for low wallet balance

4. Monitoring

  • Track feed collection success rates
  • Monitor signature collection latency
  • Log transaction submissions and confirmations
  • Alert on failures or anomalies

5. Security

  • Rotate wallet periodically
  • Limit funds in hot wallets
  • Audit configurations regularly
  • Use read-only wallets for monitoring

Advanced Usage

Custom Validity Windows

from charli3_odv_client.blockchain.transactions import ValidityWindow
 
# Create custom validity window
validity_window = ValidityWindow(
    validity_start=1640000000000,  # Custom start time
    validity_end=1640000120000,     # Custom end time (must be start + validity_length)
    current_time=1640000060000      # Current time for calculations
)
 
# Use in aggregation
odv_result = await tx_builder.build_odv_tx(
    node_messages=node_messages,
    signing_key=signing_key,
    change_address=change_address,
    validity_window=validity_window
)

Transaction Fee Estimation

# Build transaction
odv_result = await tx_builder.build_odv_tx(...)
 
# Get transaction size
tx_size = len(odv_result.transaction.to_cbor())
print(f"Transaction size: {tx_size} bytes")
 
# Estimate execution units
execution_units = await tx_manager.estimate_execution_units(
    odv_result.transaction
)
print(f"Execution units: {execution_units}")

Parallel Feed Collection

import asyncio
 
async def collect_from_multiple_oracles(configs):
    """Collect feeds from multiple oracles in parallel."""
    
    tasks = [
        collect_feeds_for_oracle(config)
        for config in configs
    ]
    
    results = await asyncio.gather(*tasks, return_exceptions=True)
    
    for config, result in zip(configs, results):
        if isinstance(result, Exception):
            print(f"Failed for {config.policy_id}: {result}")
        else:
            print(f"Success for {config.policy_id}: {len(result)} feeds")
    
    return results