@pugshole/lilac-sdk
v0.1.5
Published
Official TypeScript SDK for the Lilac verifiable AI oracle (https://app.base-oracle.cloud). Wraps the REST API, on-chain quote flow, and webhook HMAC verification.
Maintainers
Readme
@pugshole/lilac-sdk
Official TypeScript SDK for the Lilac verifiable AI oracle.
One-liner integration for the Lilac REST API (/v1/ask, /v1/quote, /v1/query/:id, /v1/account/:address) plus a static helper for verifying webhook HMAC signatures. Works in Node >=18 and any modern runtime with a global fetch.
- Endpoint:
https://app.base-oracle.cloud(same for every chain) - Get an API key: https://app.base-oracle.cloud/console
Supported chains
Lilac is live on 6 EVM chains. Same REST endpoint, same USD pricing on every chain — payable in the chain's native token.
| Chain | chainId | Native token | Use CHAIN_IDS.* |
|---|---|---|---|
| Base mainnet | 8453 | ETH | CHAIN_IDS.base |
| Arbitrum One | 42161 | ETH | CHAIN_IDS.arbitrum |
| Optimism | 10 | ETH | CHAIN_IDS.optimism |
| Polygon PoS | 137 | POL | CHAIN_IDS.polygon |
| BNB Smart Chain | 56 | BNB | CHAIN_IDS.bnb |
| Blast | 81457 | ETH | CHAIN_IDS.blast |
For up-to-date Oracle / FeeManager / BillingAccount contract addresses per chain, see /docs.
Install
npm install @pugshole/lilac-sdkviem is an optional peer dependency — only needed if you also want to submit on-chain requests yourself. The SDK itself is dependency-free.
Quickstart
import {LilacClient, CHAIN_IDS} from "@pugshole/lilac-sdk";
const lilac = new LilacClient({
apiKey: process.env.LILAC_API_KEY!, // "ok_..."
chainId: CHAIN_IDS.base, // default. CHAIN_IDS.arbitrum / .optimism / .polygon / .bnb / .blast
});
const {answer, confidence, sources} = await lilac.ask({
question: "Is BTC above $50k right now?",
options: ["yes", "no"],
});
console.log({answer, confidence, sources});Switching chains per call
You can construct one client and override chainId per call — no need for one client per chain:
const lilac = new LilacClient({apiKey: process.env.LILAC_API_KEY!});
// Ask Lilac on Polygon (fee paid in POL)
const a = await lilac.ask({
chainId: CHAIN_IDS.polygon,
question: "Did Team Spirit win PGL Astana 2026?",
options: ["yes", "no"],
});
// Same wallet's account history on BNB Chain
const acct = await lilac.account("0xb15...f9b3", {chainId: CHAIN_IDS.bnb});Examples
1. On-chain wallet-paid request (quote + submit)
The ask() flow is convenient but billed off-chain against your API key. For verifiable, on-chain answers, request a signed Quote and submit it to the Oracle contract from your own wallet:
import {LilacClient} from "@pugshole/lilac-sdk";
import {createWalletClient, http, parseAbi} from "viem";
import {base} from "viem/chains";
import {privateKeyToAccount} from "viem/accounts";
const lilac = new LilacClient({chainId: 8453}); // no API key needed for quote()
const quote = await lilac.quote({
question: "Will ETH close above $3,000 on 2026-06-01 UTC?",
options: ["yes", "no"],
tier: 1,
requester: "0xb15697083b70b075426a3c282Ee11765551bf9b3",
nonce: "1",
});
// quote.feeWei, quote.signature, quote.questionHash, quote.expiry, quote.node ...
const account = privateKeyToAccount(`0x${process.env.PRIVATE_KEY!}`);
const wallet = createWalletClient({account, chain: base, transport: http()});
const oracleAbi = parseAbi([
"function requestData((bytes32 questionHash,uint8 tier,uint256 feeWei,uint256 expiry,address node,bytes signature) quote, string question, string[] options) payable returns (bytes32)",
]);
const txHash = await wallet.writeContract({
// Lilac Oracle on Base mainnet. Per-chain Oracle addresses are listed at
// https://app.base-oracle.cloud/docs#contract-addresses
address: "0xc82aac3acd8804a90c6597889bdaa47c4db51e8c",
abi: oracleAbi,
functionName: "requestData",
args: [quote, "Will ETH close above $3,000 on 2026-06-01 UTC?", ["yes", "no"]],
value: BigInt(quote.feeWei),
});To submit on a different chain, swap chain: base for arbitrum / optimism / polygon / bsc / blast from viem/chains and use that chain's Oracle address.
2. Poll a request to terminal status
import {LilacClient} from "@pugshole/lilac-sdk";
const lilac = new LilacClient();
const requestId = "0x" + "ab".repeat(32);
for (;;) {
const r = await lilac.query(requestId);
if (r.status === "fulfilled" || r.status === "failed" || r.status === "refunded") {
console.log(r);
break;
}
await new Promise((res) => setTimeout(res, 2000));
}3. Webhook signature verification (Express)
import express from "express";
import {LilacClient} from "@pugshole/lilac-sdk";
const app = express();
app.post(
"/lilac/webhook",
express.raw({type: "*/*"}), // IMPORTANT: keep the raw body
(req, res) => {
const ok = LilacClient.verifyWebhookSignature({
payload: (req.body as Buffer).toString("utf8"),
signature: req.header("x-lilac-signature"),
secret: process.env.LILAC_WEBHOOK_SECRET!,
});
if (!ok) return res.status(401).end();
const event = JSON.parse((req.body as Buffer).toString("utf8"));
// event.requestId, event.status, event.answer, event.confidence, event.sources ...
res.status(200).end();
},
);The secret comes from LilacClient.deriveWebhookSecret(rawApiKey) — derive once on key issue, persist, and pass as Buffer.from(hex, "hex") to verifyWebhookSignature.
API
| Method | Endpoint | Auth |
|---|---|---|
| lilac.ask({question, options?, webhookUrl?, chainId?}) | POST /v1/ask | API key |
| lilac.quote({question, options?, requester?, nonce?, chainId?}) | POST /v1/quote | public |
| lilac.query(requestId) | GET /v1/query/:requestId | public |
| lilac.account(address, {chainId?}) | GET /v1/account/:address | public |
| LilacClient.verifyWebhookSignature({payload, signature, secret}) | static | n/a |
| LilacClient.deriveWebhookSecret(rawApiKey) | static | n/a |
All REST methods throw LilacApiError (with .status, .code, .body) on non-2xx responses.
Constructor options
new LilacClient({
apiKey?: string, // "ok_..." — required for ask() only
chainId?: number, // default 8453 (Base mainnet)
baseUrl?: string, // default "https://app.base-oracle.cloud"
fetch?: typeof fetch, // override for tests / custom transport
timeoutMs?: number, // default 30000
});Versioning
Pre-1.0. Breaking changes will bump the minor version while we shake out integration feedback. Pin to a range like ^0.1.0.
License
MIT
