Self-hosted
A node runs inside your own Fastify app. Resource functions execute in your process, and their code never leaves your server.
Open source · Peer-to-peer
Stellarium is an open, programmable API marketplace where anyone can host an API and get paid for it.
npm install stellarium-ts fastify requestscriptOverview
A resource is a named object with typed functions, hosted on a node. Nodes share the resources they know about with their peers. Programs call resources by name, and each call is routed to the node that hosts it.
A node runs inside your own Fastify app. Resource functions execute in your process, and their code never leaves your server.
Programs refer to resources by full name, like com.example.Weather. Your node works out which peer hosts each one and forwards the call.
Start a node with the address of any running peer. It copies that peer's peer and resource lists, then announces itself to the network.
How it works
Register functions on your node. They run locally when called.
await node.register({
metadata: {},
path: "com.example",
name: "Weather",
functions: [{
name: "temperature",
parameters: [{ name: "city", type: "string" }],
returnType: "int32",
exec: async (args) => 12,
}],
});Name the resources you need and call them. No URLs or API keys.
request GetTemperature {
const weather: com.example.Weather
const reading = weather.temperature(city: "Oslo")
if (reading.error != null) {
return reading.error.message
}
return reading.value
}Send the request to POST /v1/run on any node.
{ "returnValue": 12 }A failed resource call returns an error value the script can check. It doesn't fail the whole request.
Payments
Add a payment manifest to any function on a resource you host. Callers pay per successful call from their organization's prepaid balance, and you're paid out through Stripe. A valid payment also authorizes the call, so there is no billing system or key management to run.
paymentManifest: {
costMicroUsd: 10000, // $0.01 per call
}Contracts
A contract is a RequestScript program you deploy to any node with POST /v1/contracts. It becomes a resource like any other: anyone can call it, and it can call any resource itself. You don't write TypeScript or run a server, and every call is paid for by whoever made it.
Contracts can keep state between calls. Declare a variable with var state and it's saved each time a call succeeds. Only the contract's own code can change it, so its published source is also the policy for its data.
contract com.example.Assistant(message: string): string {
// One conversation per organization, kept between calls.
var state(caller.org) history: string = ""
const sonnet: sh.stellarium.inference.claude.Sonnet
const prompt = "${history}User: ${message}\nAssistant:"
const reply = sonnet.message(prompt: prompt, maxTokens: 1024)
if (reply.error != null) {
return reply.error.message
}
history = "${prompt} ${reply.value}\n\n"
return reply.value
}This contract is a long-lived assistant. Each call adds the new message to the conversation it has stored, sends the whole history to Claude Sonnet through an inference function, and saves the reply. Every organization gets its own conversation, and the caller pays for the tokens. A call that fails saves nothing, and calls to a stateful contract take turns, so no message is lost.
Keep one shared value for every caller, or a copy per organization with state(caller.org). The caller is verified, so no one can read or spend another organization's count.
Store the last answer from a paid resource and return it until it goes stale, so callers pay for one upstream call instead of many.
Give an agent a contract that remembers what it learned between runs, behind rules anyone can read in the contract's published source.
Events
An organization declares an event type and its scripts emit events of that type. Anyone can deploy an event contract on any node that consumes it, and runs with each event, wherever it was published.
// com.example declares: event com.example.OrderPlaced(orderId: string, total: decimal)
// and its Checkout contract emits one with each order.
contract com.example.OnOrderPlaced consumes com.example.OrderPlaced {
const receipts: com.example.Receipts
receipts.send(orderId: orderId, total: total)
}Events are signed by the node that published them and checked by every node that receives them, so only the type's owner can emit it. A run that fails publishes nothing, and each event contract runs once per event. Whoever deploys an event contract pays for its runs, so subscribing never costs the publisher.
Subscribe to another organization's events with a contract on any node, instead of exposing an endpoint and checking signatures yourself.
An event contract can emit events of its own, so one event can start a chain of up to four steps across nodes.
Mark an event type restricted, and only contracts your own organization deployed receive it.
Structs
A struct names the shape of an object: its fields and the type of each. An organization declares it once, and anyone on the network can use it as a type in contracts, event types and resource functions, so values are checked wherever they're passed, returned or emitted, instead of travelling as an unchecked object.
// com.example declares the shape once, on one of its nodes.
struct com.example.Address@1(street: string, city: string, postcode: string)
// Anyone, on any node, can then use it as a type.
contract com.acme.Ship(to: com.example.Address@1): string {
const carrier: com.acme.Carrier
return carrier.label(city: to.city, postcode: to.postcode)
}Every reference names a version, and versions never change or go away, so com.example.Address@1 means the same fields on every node, forever. Nodes learn structs from their peers through signed announcements, and only the organization that owns a path can declare it. Checking is structural, so any value with the right fields fits.
Declare an Address, an Invoice or a Forecast once, and every organization's contracts, events and resources can speak it.
Give event type fields struct types, so every consumer on every node gets the shape it expects.
Versions are immutable and never deleted. Declare @2 for a new shape, and nothing that names @1 changes.
Get started