A decentralized, transparent, and permissionless framework for executing LLM tasks and AI applications.
- Transparency: Everything happens on-chain, if you don't see it on the block-explorer it didn't happen.
- Ease of Use: SDK has an intuitive interface to integrate LLMs into your application, be it AI-driven workflows or autonomous agents.
- Decentralized & Permissionless: Anyone can run an Oracle Node, including you!
- Validations: Validator nodes validate & give score to each result, ensuring high-quality generations.
- Diverse LLM Support: Oracles support 50+ LLMs, you can pick specific models for your request.
- Multi-step Tasks: Oracles can refer to previous tasks, and keep a conversation history on-chain.
See more here!
Dria Oracle SDK is an NPM package, which can be installed with any of the following:
npm i dria-oracle-sdk
yarn add dria-oracle-sdk
pnpm add dria-oracle-sdk
Using Oracle SDK, the following happens when you make a request:
- The request is recorded on-chain & this event is picked up by Oracle nodes.
- Oracles compete to respond with their results & solve a small hash-based proof-of-work.
- Once the requested amount of generations are made, validator oracles give a score on each of them to assess their quality, while also doing proof-of-work themselves.
- Once the requested amount of validations are made, you can read the highest-score generation from the contract.
We use Viem to connect with blockchains. Provide the two Viem clients as input:
import { Oracle, ArweaveStorage } from "dria-oracle-sdk";
import { createPublicClient, createWalletClient, http } from "viem";
// wallet client for "write" operations
const walletClient = createWalletClient({
account: privateKeyToAccount(SECRET_KEY),
transport: http(RPC_URL),
chain,
});
// public client for "read" operations
const publicClient = createPublicClient({
transport: http(RPC_URL),
chain,
});
// arweave storage
const storage = new ArweaveStorage();
// create the oracle sdk
const oracle = new Oracle(
{
public: publicClient,
wallet: walletClient,
},
storage
);
Then, initialize the SDK by connecting to the Coordinator contract at the given address:
await oracle.init(coordinatorAddress);
As you may have noticed, we have written const storage = new ArweaveStorage();
above. This is because we save from gas costs by writing large strings to Arweave and storing its transaction id instead of the string itself within the contract.
If you omit Arweave by not passing this argument to the Oracle
constructor, Arweave messages will not be downloaded automatically, nor large values will not be uploaded.
In a contract, if we see a stringified object such as { arweave: "tx-id-here"}
then this belongs to an Arweave transaction.
Oracle nodes can understand this, and read the actual content from Arweave. Even further, they can write their results to Arweave the same way, from which the SDK understands and downloads the output!
In short, there are two types of operations:
- Read: It is enough to pass in the
ArweaveStorage
instance as shown above to theOracle
constructor.
const storage = new ArweaveStorage();
- Write: To be able to upload to Arweave as the SDK user, you must provide an
ArweaveWallet
type object. The SDK tolerates to some byte-length, i.e. it will not use Arweave if the input is not large enough; this is configurable.
const storage = new ArweaveStorage();
const wallet = JSON.parse(fs.readFileSync("./path/to/wallet.json", "utf-8")) as ArweaveWallet;
const byteLimit = 2048; // default 1024
storage.init(wallet, byteLimit);
Before we make a request, we must make sure that we have enough allowance to the coordinator contract so that it can pay the oracle fees. You can check the allowance, and approve tokens if required with the following snippet:
const allowance = await oracle.allowance();
if (allowance === 0n) {
// you can omit `amount` as well to make an infinite approval
const amount = parseEther("1.0");
const txHash = await oracle.approve(amount);
console.log({ txHash });
}
We are now ready to make a request. Within a request, we simply provide the input as-is, along with the models to be used:
const input = "What is 2+2?";
const models = ["gpt-4o-mini"]; // or just "*" for any model
const txHash = await oracle.request(input, models);
When this transaction is mined, a taskid
will be assigned to it by the contract. A taskId start from 1 and simply increments for each request; however, we cant be sure of its value until our request is mined. For this reason, we follow our request
with this function:
const taskId = await oracle.waitRequest(txHash);
With the request made & its task id known, we can sit back for a while and wait for the generator and validator oracles to finish their jobs. First, generator oracles will work to answer until the requested number of generations are met. After that, the validations will take place and each generation will take a score from each validator. Once the requested number of validations are met as well, the last validator computes the final scores & distributes fees accordingly.
To wait for this programmatically, we can provide the task id and wait until all this is completed with the wait
function:
await oracle.wait(taskId);
When we return from wait
without any errors, we are sure that the request has been completed.
When calling request
, you can provide optional parameters:
protocol
: Protocol is a string that should fitbytes32
of Solidity, which identifies your application. Can be omitted, defaults to SDK protocol.taskParameters
: An object that definesdifficulty
,numGenerations
andnumValidations
.difficulty
defines the proof-of-work difficulty for the oracles, increasing this exponentially rises the oracle fee.numGenerations
is the number of generation you expect.numValidations
is the number of validations you expect for each generation.
Here is an example:
const txHash = await oracle.request(input, models, {
protocol: "my-app/0.1.0",
taskParameters: {
difficulty: 4,
numGenerations: 3,
numValidations: 2,
},
});
To read the best (i.e. highest score) response to a request, we have the read
function:
const response = await oracle.read(taskId);
const { output, metadata } = response;
This function also handles downloading the actual values from Arweave, if the responder nodes have used it to save from gas. Note that you must have passed in the ArweaveStorage
instance to the Oracle
constructor for this to work.
If you are interested in reading all generations, you can use:
const responses = await oracle.getResponses(taskId);
This returns you an array of raw responses. You can fetch the actual values from Arweave using the processResponse
function:
for (const responseRaw of responses) {
const response = await oracle.processResponse(responseRaw);
// console.log ...
}
If you are interested in reading all validations, you can use:
const validations = await oracle.getValidations(taskId);
This returns you an array of raw responses. You can fetch the actual values from Arweave using the processResponse
function:
for (const validationRaw of validations) {
const validation = await oracle.processValidation(validationRaw);
// console.log ...
}
We have several examples that demonstrate making a request, following it up with another request, and view an existing request! You can examine the code there, or try them out yourself.
First, go to examples
directory and install packages there:
cd examples
npm install
Prepare the environment by copying .env.example
as .env
, and fill out your SECRET_KEY
and RPC_URL
.
The examples use Base Sepolia network, so make sure your RPC URL belongs to that network.
Warning
Make sure you have enough ETH and WETH as well, to pay for the fees.
Let's make a request now. Simply call ./request.mjs
with a single argument, that is your prompt.
node ./request.mjs <your-input-here>
# node ./request.mjs "What is 2+2?"
# node ./request.mjs "Who created you?"
# node ./request.mjs "Tell me a joke"
The script will print your request, and the best generation along with validations to the screen. It will also print the task id, which identifies your request for the coordinator contract.
Oracle's can "follow up" on an existing message, similar to how ChatGPT works with context! Check your task id from the previous example, and try to follow it
up with another question using the ./chat.mjs
script:
node ./chat.mjs <your-input-here> <task-id-here>
# node ./chat.mjs "how many words are there in my previous message?" 4
# node ./chat.mjs "what are we talking about in our last conversation?" 123
# node ./chat.mjs "what is the multiplication of the numbers in your previous messages" 1337
You can keep following up the chat messages on and on, as the oracle writes the entire history for each chat message!
You can view the request, its generations and validations for any task id with the ./view.mjs
script:
node ./view.mjs <task-id>
# node ./view.mjs 1
Tests use the live environment, so make sure you have some balance in your wallets. To run them:
pnpm test
Contract | Address |
---|---|
Registry | 408d245a853137e44a2465d5c66061f97582eae9 |
Coordinator | 13f977bde221b470d3ae055cde7e1f84debfe202 |
We are using MIT license.