Skip to content
Lumoracle

Docs

Lumoracle sells signed data per request. A buyer pays in USDG over x402, an open standard built on the HTTP status 402, and gets back a report that a contract can check without trusting Lumoracle.

Buy a report over HTTP

  1. Ask the feed for its latest report. The answer is 402, with the price in the PAYMENT-REQUIRED header (base64 JSON).
  2. Sign the USDG authorization it describes. It is an EIP-3009 transfer to the provider’s vault. You pay no gas and need no ETH.
  3. Ask again with the signature in PAYMENT-SIGNATURE. Lumoracle settles the payment on Robinhood Chain, then returns the report.
terminal
curl -i https://lumoracle.io/gw/v1/feeds/lumoracle/eur-usd-ecb-reference/latest
# HTTP/1.1 402 Payment Required
# PAYMENT-REQUIRED: eyJ4NDAyVmVyc2lvbiI6Mi…

curl https://lumoracle.io/gw/v1/feeds/lumoracle/eur-usd-ecb-reference/latest \
  -H "PAYMENT-SIGNATURE: <base64 payment payload>"

Or let the SDK do all three steps, within a spending limit you set:

buy.ts
import {Lumoracle} from '@lumoracle/sdk'

const lumoracle = new Lumoracle({
  gatewayUrl: 'https://lumoracle.io/gw',
  account,                       // any viem account holding USDG
  maxSpendUsdg: 5_000_000n,      // five dollars, for the life of this process
  rpcUrl,                        // optional: check the vault and the signer on chain
})
const report = await lumoracle.read('lumoracle/eur-usd-ecb-reference')
report.value     // bigint, scaled by report.decimals
report.text()    // exact decimal text
report.encoded   // pass with report.signature to a contract

The SDK signs in first, so a pack you bought earlier is spent before anything is bought again. Before it signs a payment it refuses another network, any token but USDG, an amount that is not the price times the calls, and anything past your limit. With rpcUrl set it also checks that the address to pay is the provider’s vault as the registry derives it. A page that asks you to pay any other address is not Lumoracle.

A stock x402 client works too. Its spend controls only know the tokens it ships with, so allow USDG explicitly: spendControls.allowedAssets with the network, the USDG address and a maximum per payment.

Check a report in a contract

Pass the encoded report and its signature into your function and let LumoracleConsumer do the checks: the signer is authorized for that feed, the report is no older than you allow, it is not older than the last one you accepted, and the value is inside your bounds.

CardVault.sol
import {LumoracleConsumer} from "@lumoracle/contracts/src/LumoracleConsumer.sol";

contract CardVault is LumoracleConsumer {
    bytes32 constant CARD_FEED = 0x…;

    function settle(bytes calldata report, bytes calldata sig) external {
        int256 cents = _readOnce(CARD_FEED, report, sig, Policy({
            maxAgeSec: 3_600,
            allowedKinds: KIND_PROVIDER,   // first party only
            minValue: 1,
            maxValue: 10_000_000
        }));
    }
}

There is no way to read a report without naming a maximum age. A stale number used as a fresh one is how oracles lose money.

_readOnce accepts each observation once, which is what a function that pays out wants: the same purchased report cannot be submitted again until it ages out. Use _read only where the contract just keeps a value and being handed the current one twice changes nothing, and _peek to check without recording.

Let an agent buy data

The MCP server gives an agent four tools: search feeds, read a feed’s details, buy a report, and check one. It spends from a wallet you control, up to the limit you set.

mcp.json
{
  "mcpServers": {
    "lumoracle": {
      "command": "npx",
      "args": ["-y", "@lumoracle/mcp"],
      "env": {
        "LUMORACLE_GATEWAY_URL": "https://lumoracle.io/gw",
        "LUMORACLE_PRIVATE_KEY": "<a wallet made for this agent>",
        "LUMORACLE_MAX_SPEND_USDG": "1000000"
      }
    }
  }
}

The limit is enforced inside the server before anything is signed, so nothing the agent reads can raise it. Give the agent a wallet of its own, funded with what you are willing to lose. It needs USDG only.

Publish a feed

Open the dashboard, register as a provider (one transaction) and create a feed (one transaction each). Two ways to sign:

  • Lumoracle, from your API. Paste an HTTPS endpoint and a JSON path. Lumoracle fetches it on a schedule and signs what it finds, as a notary. Every surface says so: “Signed by Lumoracle from the provider’s API”.
  • You, with your own key. Run the signer on your server. Reports are first party: “Signed by the provider”. Lumoracle asks your signer once per heartbeat and checks every report against the registry before selling it.
signer.ts
import {createSigner} from '@lumoracle/signer'

export const handler = createSigner({
  feedId: '0x…',                 // from the wizard
  decimals: 2,
  requester: '0x…',              // Lumoracle's requester, from the wizard
  privateKey: process.env.LUMORACLE_SIGNER_KEY,
  async observe() {
    const sale = await db.medianSale('psa9-charizard')
    return {value: BigInt(sale.cents), payload: {sales: sale.count}}
  },
})

Set requester: the signed report is what you sell, and with it your signer answers Lumoracle and nobody else. When observe() throws, no report is stored and the feed stops selling until it recovers. A missing report costs a sale; a wrong one costs the feed.

A provider cannot register its own key as “signed by Lumoracle”. The registry accepts that kind of signer only with Lumoracle’s attestation, so the label on a report means what it says.

Payments go straight from the buyer’s wallet into your own vault. Lumoracle never holds them. The platform fee is 10%, taken when you withdraw, at a rate locked when you registered. Lumoracle’s policy for the fees it receives is 80% to buy back its token and burn it and 20% to operations; that split begins once the token exists, and each buyback will be a transaction anyone can check.

The report

A report is eleven fields signed with EIP-712 under the domain Lumoracle, version 1. The domain names no chain and no contract: a report states a fact about the world, so the same signature verifies on any chain that mirrors the signer registry.

  • value and decimals: a scaled integer, never a float.
  • observedAtMs: when the value was true at the source, in milliseconds. Freshness is measured from here.
  • confidence and sourceCount: the spread across sources, and how many were used.
  • payloadHash: binds anything that is not the primary number, by the hash of its canonical JSON.
  • signerKind: 1 for Lumoracle as notary, 2 for the provider, 3 for an aggregate.

Paste any report into the report checker to see each check pass or fail.

Packs, keys and the floor

Settling a payment on chain costs gas, which Lumoracle pays. A payment smaller than about twenty cents would cost more to settle than it earns, so Lumoracle does not settle it. A feed priced below that per call sells packs instead: one payment for a hundred or a thousand calls, spent afterwards with a wallet signature and no transaction. Packs last 30 days and are not refundable.

A pack belongs to the wallet that paid for it, so spending one means proving that wallet. In a browser, sign in with it: ask GET /v1/sessions/challenge, sign the challenge, post it to POST /v1/sessions, and send the bearer token it returns. On a server, make an API key in the dashboard and send that instead. Both spend the packs of the same wallet, and every answer carries how many calls are left.

terminal
curl -i https://lumoracle.io/gw/v1/feeds/lumoracle/eur-usd-ecb-reference/latest \
  -H "Authorization: Bearer lk_…"
# HTTP/1.1 200 OK
# X-Lumoracle-Pack-Calls-Left: 99

A key spends packs and cannot buy one: it holds no wallet and signs no payment. When its wallet has no call left on that feed the answer is the ordinary 402 quote, and the next pack is bought with the wallet. A key is shown once when it is made, stored only as a hash, and can be revoked at any time.

Errors

There is no error for which you are charged. Payment is settled only after the report exists, and the report is returned only after payment settles.

The SDK adds one code of its own, PACK_EMPTY, raised before it calls when the key you gave it has no pack left and no account to pay from.

CodeHTTPMeaning
BAD_REQUEST400The request was not understood.
PAYMENT_INVALID401The payment authorization was not accepted.
SETTLEMENT_FAILED402The payment could not be settled on chain.
FEED_UNAVAILABLE403This feed is not selling right now.
FEED_NOT_FOUND404This feed does not exist.
RATE_LIMITED429Too many requests. Wait a moment and try again.
BAD_REPORT502The provider returned a report that failed its signature check.
STALE_DATA503The latest report is too old to sell. Try again when the feed recovers.
SERVICE_UNAVAILABLE503Lumoracle cannot take payments right now.