Docs

Smart Contract Integration

Two ways smart contracts and APIs work together on Opsalis — use them independently or combine them.

Two Patterns

PatternWhat it doesUse case
SC‑as‑Server Register any smart contract as a REST API Let HTTP clients call blockchain functions with a simple GET or POST
SC‑as‑Client Smart contracts call APIs via an oracle On-chain logic that needs off-chain data (insurance, derivatives, automation)

SC-as-Server: Any Smart Contract as a REST API

Register any read-only or write function from any supported blockchain as a standard REST endpoint in the Opsalis catalog. Callers use plain HTTP — no Solidity, ABI encoding, or wallet required.

Supported Chains

Base, Ethereum, Arbitrum, Polygon, BNB Chain, Avalanche (mainnets and testnets).

How It Works

HTTP client | | GET /v1/chainlink-eth-usd/latestAnswer v Opsalis DAG node | 1. Looks up API in catalog -> backend_type = "contract" | 2. Reads contract address, chain, ABI | 3. Maps URL path to ABI function name | 4. Calls contract.latestAnswer() via eth_call (free, no gas) | 5. Returns JSON response v {"success": true, "result": "186423000000", "chain_id": 1}

View/pure functions are free and instant (eth_call). Write functions use the node operator's wallet and return the transaction hash.

The Chainlink ETH/USD Price Feed on Ethereum mainnet (0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419) exposes latestAnswer() and decimals().

Step 1 — Register via the web console:

Step 2 — Call it:

curl https://your-node.example.com:3000/v1/chainlink-eth-usd/latestAnswer

Response:

{
  "success": true,
  "view": true,
  "function": "latestAnswer",
  "result": "186423000000",
  "chain_id": 1
}

Chainlink uses 8 decimals: 186423000000 / 1e8 = $1,864.23.

Example: Uniswap V3 Swap Quotes

Register the Uniswap V3 QuoterV2 on Base (0x3d4e44Eb1374240CE5F1B136aa68B6A5f2f0Caa3). Expose quoteExactInputSingle() to let HTTP clients get real-time swap quotes.

Call it (how much USDC for 1 WETH?):

curl -X POST https://your-node.example.com:3000/v1/uniswap-quote/quoteExactInputSingle \
  -H "Content-Type: application/json" \
  -d '{
    "tokenIn":  "0x4200000000000000000000000000000000000006",
    "tokenOut": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "amountIn": "1000000000000000000",
    "fee": 500,
    "sqrtPriceLimitX96": "0"
  }'

Response:

{
  "success": true,
  "function": "quoteExactInputSingle",
  "result": {
    "amountOut": "1863410241",
    "sqrtPriceX96After": "1412854891823641928374918",
    "initializedTicksCrossed": "2",
    "gasEstimate": "127400"
  },
  "chain_id": 8453
}

amountOut is raw USDC (6 decimals): 1863410241 / 1e6 = $1,863.41.

SC-as-Client: Smart Contracts That Call APIs

Any smart contract on Base can request data from any API in the Opsalis catalog. The result is delivered on-chain via a callback — the same request/fulfill pattern used by Chainlink VRF and other oracles.

How It Works

Your smart contract (Base) | | 1. Approve USDC to OpsalisOracleRouter | 2. Call requestData(wrapperUid, apiUid, params, ...) | -> USDC split atomically: 95% to API owner, 5% royalty | -> Emits DataRequested event v OpsalisOracleRouter (Base) | | 3. Opsalis DAG node detects the event | 4. Node calls the real API (HTTP or another contract) | 5. Node submits fulfillData(requestId, result) on-chain v OpsalisOracleRouter delivers result to your callback function

Real-World Use Cases

Parametric Earthquake Insurance
A smart contract auto-pays policyholders when the USGS reports an earthquake above magnitude 6.0 within 100 km of an insured location. No claims process, no adjusters — the contract reads the API and settles instantly.
Weather Derivatives
Agricultural hedging contracts that settle based on actual temperature or rainfall data from a weather API. Farmers lock in a price; if drought hits, the contract pays out automatically.
Flight Delay Insurance
Travelers buy coverage for a specific flight. If the flight status API reports a delay over 2 hours, the contract refunds the ticket automatically — before the traveler even lands.

Example: Parametric Earthquake Insurance

This contract monitors earthquake data from the USGS API (registered in the Opsalis catalog). When an earthquake above the configured magnitude threshold occurs near an insured location, the contract automatically pays out the policyholder.

Step 1 — The Solidity contract:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

interface IOpsalisOracle {
    function requestData(
        bytes32 wrapperUid, bytes32 apiUid, bytes calldata params,
        address callbackAddr, bytes4 callbackSel,
        address apiOwnerWallet, uint256 fee
    ) external returns (bytes32);
}

interface IERC20 {
    function approve(address, uint256) external returns (bool);
    function transfer(address, uint256) external returns (bool);
}

contract EarthquakeInsurance {
    IOpsalisOracle public immutable oracle;
    IERC20         public immutable usdc;

    struct Policy {
        address holder;
        uint256 payout;       // USDC amount
        int32   lat;          // latitude x 1000 (e.g. 37770 = 37.77)
        int32   lon;          // longitude x 1000
        uint16  radiusKm;
        uint16  minMagnitude; // x 10 (e.g. 60 = M6.0)
        bool    active;
    }

    mapping(uint256 => Policy) public policies;
    mapping(bytes32 => uint256) public requestToPolicy;
    uint256 public nextPolicyId;

    // ... (full contract below)
}

Step 2 — Deploy and fund:

# Deploy to Base Sepolia
forge create EarthquakeInsurance \
  --rpc-url https://sepolia.base.org \
  --private-key $KEY \
  --constructor-args $ORACLE_ADDRESS $USDC_ADDRESS

# Fund with USDC for payouts + oracle fees
cast send $USDC "transfer(address,uint256)" $CONTRACT 10000000 \
  --rpc-url https://sepolia.base.org --private-key $KEY

Step 3 — Create a policy:

# Insure San Francisco (lat 37.77, lon -122.42) for M5.0+, $50 payout
cast send $CONTRACT \
  "createPolicy(address,uint256,int32,int32,uint16,uint16)" \
  $POLICYHOLDER 50000000 37770 -122420 200 50 \
  --rpc-url https://sepolia.base.org --private-key $KEY

Step 4 — Check for earthquakes (anyone can trigger):

# This calls the USGS API through the Opsalis oracle
cast send $CONTRACT \
  "checkEarthquake(uint256,bytes32,bytes32,address)" \
  0 $WRAPPER_UID $USGS_API_UID $API_OWNER \
  --rpc-url https://sepolia.base.org --private-key $KEY

The oracle fetches live USGS data. If a qualifying earthquake is found, the contract pays the policyholder automatically.

Payment Flow

PartyRoleEarns
API ownerRegistered the USGS earthquake API95% of each oracle call fee
OpsalisProtocol royalty (immutable, on-chain)5% of each oracle call fee
Node operatorRuns the node, fulfills the requestReimbursed via the API owner's pricing

Both transfers happen atomically in a single transaction. If either fails, the entire request reverts — no funds are ever held by any intermediary.

Full Circle: SC Calls SC Through Opsalis

The two patterns can combine. A Base smart contract uses the oracle to call a Chainlink price feed that is registered as an API in Opsalis.

DeFi contract (Base) | requestData(wrapperUid, chainlinkApiUid, ...) | USDC: 95% -> Chainlink API registrant, 5% -> royalty v OpsalisOracleRouter (Base) | emits DataRequested v Opsalis DAG node | detects event | calls Chainlink ETH/USD on Ethereum mainnet (eth_call, free) | submits fulfillData() on Base v OpsalisOracleRouter -> DeFi contract callback | receives ETH/USD price on Base v DeFi contract uses the price for lending, liquidation, etc.

The Chainlink protocol call is free (read-only eth_call). The Opsalis fee covers the node operator's infrastructure costs, paid in USDC on Base. This creates a cross-chain oracle bridge — read data from any chain, deliver it to Base.

API Reference

OpsalisOracleRouter Interface

Params Encoding

The params field is ABI-encoded as a single string containing a JSON object:

// View function, no arguments
bytes memory params = abi.encode(
    '{"path":"/latestAnswer","method":"GET"}'
);

// Function with arguments
bytes memory params = abi.encode(
    '{"path":"/balanceOf","method":"GET","account":"0xabc...123"}'
);

// REST API endpoint
bytes memory params = abi.encode(
    '{"path":"/v1/earthquakes/recent","method":"GET","min_magnitude":"5","limit":"10"}'
);

Events

event DataRequested(
    bytes32 indexed wrapperUid,
    bytes32 indexed apiUid,
    bytes32 requestId,
    bytes   params,
    address callbackAddress,
    bytes4  callbackSelector,
    address requester
);

event DataFulfilled(
    bytes32 indexed requestId,
    bytes32 indexed wrapperUid
);

EarthquakeInsurance.sol — Full Contract

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

interface IOpsalisOracle {
    function requestData(
        bytes32 wrapperUid,
        bytes32 apiUid,
        bytes calldata params,
        address callbackAddress,
        bytes4  callbackSelector,
        address apiOwnerWallet,
        uint256 fee
    ) external returns (bytes32 requestId);
}

interface IERC20 {
    function approve(address spender, uint256 amount) external returns (bool);
    function transfer(address to, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}

/**
 * @title EarthquakeInsurance
 * @notice Parametric insurance that auto-pays when USGS reports a qualifying earthquake.
 *
 * How it works:
 * 1. Admin creates policies for insured locations (lat/lon, magnitude threshold, payout)
 * 2. Anyone calls checkEarthquake() which queries the USGS API via Opsalis oracle
 * 3. If a qualifying earthquake is found, the contract pays the policyholder automatically
 * 4. No claims, no adjusters, no delays
 */
contract EarthquakeInsurance {
    IOpsalisOracle public immutable oracle;
    IERC20         public immutable usdc;
    address        public admin;

    struct Policy {
        address holder;       // Who gets paid
        uint256 payout;       // USDC amount (6 decimals)
        int32   lat;          // Latitude x 1000 (e.g. 37770 = 37.770)
        int32   lon;          // Longitude x 1000 (e.g. -122420 = -122.420)
        uint16  radiusKm;     // Coverage radius in km
        uint16  minMagnitude; // Minimum magnitude x 10 (e.g. 60 = M6.0)
        bool    active;       // Can still trigger
        uint256 lastChecked;  // Timestamp of last check
    }

    mapping(uint256 => Policy) public policies;
    mapping(bytes32 => uint256) public requestToPolicy;
    uint256 public nextPolicyId;

    event PolicyCreated(uint256 indexed policyId, address holder, uint256 payout);
    event EarthquakeChecked(uint256 indexed policyId, bytes32 requestId);
    event PolicyTriggered(uint256 indexed policyId, address holder, uint256 payout);
    event PolicyExpired(uint256 indexed policyId);

    modifier onlyAdmin() {
        require(msg.sender == admin, "Only admin");
        _;
    }

    constructor(address _oracle, address _usdc) {
        oracle = IOpsalisOracle(_oracle);
        usdc   = IERC20(_usdc);
        admin  = msg.sender;
        // Approve oracle to spend USDC for API call fees
        IERC20(_usdc).approve(_oracle, type(uint256).max);
    }

    /// @notice Create an insurance policy for a specific location.
    function createPolicy(
        address holder,
        uint256 payout,
        int32   lat,
        int32   lon,
        uint16  radiusKm,
        uint16  minMagnitude
    ) external onlyAdmin returns (uint256 policyId) {
        policyId = nextPolicyId++;
        policies[policyId] = Policy({
            holder: holder,
            payout: payout,
            lat: lat,
            lon: lon,
            radiusKm: radiusKm,
            minMagnitude: minMagnitude,
            active: true,
            lastChecked: 0
        });
        emit PolicyCreated(policyId, holder, payout);
    }

    /// @notice Query the USGS API for recent earthquakes near the insured location.
    ///         Anyone can call this (e.g. a keeper bot or the policyholder).
    function checkEarthquake(
        uint256 policyId,
        bytes32 wrapperUid,
        bytes32 usgsApiUid,
        address apiOwnerWallet
    ) external returns (bytes32 requestId) {
        Policy storage p = policies[policyId];
        require(p.active, "Policy not active");
        require(block.timestamp > p.lastChecked + 1 hours, "Too soon");

        p.lastChecked = block.timestamp;

        // Build USGS query: earthquakes near this location, above threshold
        // lat/lon are stored as x1000, convert back for the API
        bytes memory params = abi.encode(
            string(abi.encodePacked(
                '{"path":"/v1/earthquakes/search","method":"GET",',
                '"lat":"', _intToString(p.lat), '",',
                '"lon":"', _intToString(p.lon), '",',
                '"radius_km":"', _uintToString(p.radiusKm), '",',
                '"min_magnitude":"', _magToString(p.minMagnitude), '",',
                '"limit":"1"}'
            ))
        );

        requestId = oracle.requestData(
            wrapperUid,
            usgsApiUid,
            params,
            address(this),
            this.onEarthquakeData.selector,
            apiOwnerWallet,
            0 // use default fee
        );

        requestToPolicy[requestId] = policyId;
        emit EarthquakeChecked(policyId, requestId);
    }

    /// @notice Oracle callback with USGS earthquake data.
    function onEarthquakeData(bytes32 requestId, bytes calldata result) external {
        require(msg.sender == address(oracle), "Only oracle");

        uint256 policyId = requestToPolicy[requestId];
        Policy storage p = policies[policyId];
        if (!p.active) return;

        // Decode the API response (JSON string)
        string memory json = abi.decode(result, (string));

        // Check if any earthquake was returned (simple: look for "magnitude" in response)
        // In production, use a proper JSON parser or have the API return structured data
        if (_containsMatch(bytes(json))) {
            // Qualifying earthquake found - pay out!
            p.active = false;
            usdc.transfer(p.holder, p.payout);
            emit PolicyTriggered(policyId, p.holder, p.payout);
        }
    }

    /// @notice Deactivate a policy and return funds.
    function cancelPolicy(uint256 policyId) external onlyAdmin {
        Policy storage p = policies[policyId];
        require(p.active, "Already inactive");
        p.active = false;
        emit PolicyExpired(policyId);
    }

    /// @notice Withdraw unused USDC from the contract.
    function withdraw(address to, uint256 amount) external onlyAdmin {
        usdc.transfer(to, amount);
    }

    // --- Internal helpers ---

    function _containsMatch(bytes memory b) internal pure returns (bool) {
        // Look for "magnitude" key in JSON response (indicates earthquake data present)
        // Empty response = no matching earthquakes
        bytes memory key = bytes('"magnitude"');
        if (b.length < key.length + 10) return false;
        for (uint i = 0; i + key.length <= b.length; i++) {
            bool found = true;
            for (uint j = 0; j < key.length; j++) {
                if (b[i + j] != key[j]) { found = false; break; }
            }
            if (found) return true;
        }
        return false;
    }

    function _intToString(int32 val) internal pure returns (string memory) {
        if (val < 0) return string(abi.encodePacked("-", _uintToString(uint32(-val))));
        return _uintToString(uint32(val));
    }

    function _uintToString(uint256 val) internal pure returns (string memory) {
        if (val == 0) return "0";
        uint256 temp = val;
        uint256 digits;
        while (temp != 0) { digits++; temp /= 10; }
        bytes memory buffer = new bytes(digits);
        while (val != 0) {
            digits--;
            buffer[digits] = bytes1(uint8(48 + val % 10));
            val /= 10;
        }
        return string(buffer);
    }

    function _magToString(uint16 mag) internal pure returns (string memory) {
        // mag is x10, e.g. 60 = "6.0"
        return string(abi.encodePacked(
            _uintToString(mag / 10), ".", _uintToString(mag % 10)
        ));
    }
}

OpsalisOracleRouter — Full Interface

interface IOpsalisOracleRouter {
    /// @notice Register your node's wallet as an authorized fulfiller.
    /// @param wrapperUid  keccak256 of your node's unique ID.
    function registerWrapper(bytes32 wrapperUid) external;

    /// @notice Request data from any API in the Opsalis catalog.
    ///         Payment (USDC) is split atomically: 95% to API owner, 5% royalty.
    /// @param wrapperUid       Target node's UID (keccak256 of its ID)
    /// @param apiUid           Target API's UID (keccak256 of its public ID)
    /// @param params           ABI-encoded JSON string with path, method, and arguments
    /// @param callbackAddress  Contract that receives the result
    /// @param callbackSelector Function selector for the callback (e.g. this.onData.selector)
    /// @param apiOwnerWallet   Wallet that owns the API (receives 95%)
    /// @param fee              USDC fee in raw units (6 decimals). 0 = use defaultFee.
    /// @return requestId       Unique ID for this request
    function requestData(
        bytes32 wrapperUid,
        bytes32 apiUid,
        bytes calldata params,
        address callbackAddress,
        bytes4  callbackSelector,
        address apiOwnerWallet,
        uint256 fee
    ) external returns (bytes32 requestId);

    /// @notice Deliver the API result. Only the registered node wallet can call this.
    /// @param requestId  The request to fulfill
    /// @param result     ABI-encoded response data
    function fulfillData(bytes32 requestId, bytes calldata result) external;

    /// @notice Check if a request is still pending.
    function isPending(bytes32 requestId) external view returns (bool);

    /// @notice Check if a request has been fulfilled.
    function isFulfilled(bytes32 requestId) external view returns (bool);

    /// @notice Preview the 95/5 USDC split for a given fee.
    function calculateSplit(uint256 fee) external pure
        returns (uint256 ownerPayment, uint256 royalty);
}