Developers
Become an oracle
Anyone can measure SaTT posts. Bring your own data sources, sign what you see with your own key, publish your prices — and get paid for each report that makes it into a final quorum.
Step by step
- 1Generate a dedicated key
A fresh EVM key used only by your oracle (never a treasury key). Its address is your oracle identity.
- 2Register
Sign an OracleRegistration (name, endpoint, networks) and POST /register. Your status is “pending” until a SaTT admin approves it.
- 3Publish your offer
Sign an OracleOffer: networks, metrics, freshness, prices per measurement with volume tiers (USD for dollar pools, token for crypto pools), optional subscription per pool, SLA. New versions apply to new jobs only.
- 4Poll jobs
GET /jobs with three headers: your address, a deadline and your signature of OracleRequest{action: "jobs"}. Each job gives the post, its network, the creator wallet and SaTT’s signed ownership proof.
- 5Measure with your own sources
Official APIs, your scraper, a panel — your choice. Report only what you observed; unknown metrics are 0.
- 6Sign the MetricsReport
Copy adpool, dataHash, platform and jobId from the job, add the five metrics, the time you observed them and a short source label, sign and POST /reports.
- 7Get paid
Reports that count in a final quorum accrue your price. Claim with POST /claims: dollar pools credit your linked satt.com account, crypto pools return an OracleClaim you submit on-chain.
Code samples
import { privateKeyToAccount } from 'viem/accounts';
import { metricsReportTypedData, oracleRequestTypedData } from '@satt/shared';
const API = '/api/oracle-network/v1'; // on https://satt.com's API host
const me = privateKeyToAccount(process.env.ORACLE_KEY as `0x${string}`);
const { chainId } = await (await fetch(API)).json();
const deadline = Math.floor(Date.now() / 1000) + 600;
const headers = {
'x-satt-wallet': me.address,
'x-satt-deadline': String(deadline),
'x-satt-signature': await me.signTypedData(oracleRequestTypedData(chainId, me.address, 'jobs', deadline)),
};
const { jobs } = await (await fetch(`${API}/jobs`, { headers })).json();
for (const job of jobs) {
const m = await measureFromMySources(job.url); // { views, likes, comments, shares, followers }
const report = { oracle: me.address, adpool: job.adpool, dataHash: job.dataHash, platform: job.platform,
...m, observedAt: Math.floor(Date.now() / 1000), source: 'my-oracle-v1', jobId: job.jobId };
const signature = await me.signTypedData(metricsReportTypedData(job.chainId, report));
await fetch(`${API}/reports`, { method: 'POST', headers: { 'content-type': 'application/json' },
body: JSON.stringify({ report, signature }) });
}import time, requests
from eth_account import Account
from eth_account.messages import encode_typed_data
API = "https://<api-host>/api/oracle-network/v1"
me = Account.from_key(open("oracle.key").read().strip())
chain_id = requests.get(API).json()["chainId"]
T = lambda *f: [{"name": n, "type": t} for n, t in f]
TYPES = {
"OracleRequest": T(("oracle", "address"), ("action", "string"), ("deadline", "uint256")),
"MetricsReport": T(("oracle", "address"), ("adpool", "address"), ("dataHash", "bytes32"),
("platform", "string"), ("views", "uint256"), ("likes", "uint256"), ("comments", "uint256"),
("shares", "uint256"), ("followers", "uint256"), ("observedAt", "uint256"),
("source", "string"), ("jobId", "uint256")),
}
def sign(primary, msg, chain=chain_id):
domain = {"name": "SaTT Oracle Network", "version": "1", "chainId": chain}
m = encode_typed_data(domain_data=domain, message_types={primary: TYPES[primary]}, message_data=msg)
return "0x" + me.sign_message(m).signature.hex().removeprefix("0x")
dl = int(time.time()) + 600
auth = sign("OracleRequest", {"oracle": me.address.lower(), "action": "jobs", "deadline": dl})
jobs = requests.get(f"{API}/jobs", headers={"x-satt-wallet": me.address,
"x-satt-deadline": str(dl), "x-satt-signature": auth}).json()["jobs"]
for job in jobs:
m = measure_from_my_sources(job["url"])
report = {"oracle": me.address.lower(), "adpool": job["adpool"], "dataHash": job["dataHash"],
"platform": job["platform"], **m, "observedAt": int(time.time()),
"source": "my-oracle-py", "jobId": job["jobId"]}
requests.post(f"{API}/reports", json={"report": report, "signature": sign("MetricsReport", report, job["chainId"])})Quorum, median, outliers
A value is final once 2 oracles agree. The paid number is the median of the agreeing reports; any report more than 10% away from the median is an outlier — it earns nothing and lowers your agreement rate. Late reports are refused; missed jobs lower your uptime.
Verify the identity proof
Every job carries an OwnershipMatch signed by SaTT’s identity key: wallet, network, handle hash, post hash, method (official sign-in or bio code). Recover the signer and compare it with the identity signer published at GET /api/oracle-network/v1.
Self-signed claims (always verified)
Creators can sign their own figures. It is an accountable public statement that triggers a measurement right away — payment still waits for the quorum and is computed on the lower of declared and verified numbers. Over-declaring beyond the tolerance costs reputation; repeat offenders must post a bond, then are barred.
How oracles are paid
You set your prices. A fee accrues only for reports that count in a final quorum, at the offer version in force when the job was created, with your volume tiers. Dollar pools reserve fees from the pool budget (or charge the advertiser on top); crypto pools pay them from the pool at claim time.
API reference
Versioned REST API under /api/oracle-network/v1 — every endpoint, schema and error code is in the OpenAPI file.
FAQ
What data do I provide?
Public counts of a post — views, likes, comments, shares, and the account’s followers — as you observed them, with the time and your source label.
What are the rate limits?
Oracle calls: 120 requests burst, 2 per second. Public reads: 120 burst, 4 per second. Ownership matches: 30 burst, one every 2 seconds. Over the limit you get HTTP 429 with Retry-After.
How and when am I paid?
Each agreeing report in a final quorum accrues your price. Claim any time: dollar fees are credited to the satt.com account linked to your oracle address; crypto fees come as a signed claim on the pool.
What gets an oracle suspended?
Made-up or copied numbers, systematic outliers, reporting posts you did not measure, sharing your key or abusive polling.
Does SaTT compete with me?
The SaTT Oracle is one quorum member like any other and publishes its own offer. Where SaTT cannot measure a network, pools rely on the marketplace.
Is it on-chain?
Not yet: the network decides what the single payout key may sign. Multi-signer verification in the contract, staking and slashing are on the mainnet roadmap.