Skip to main content
The Molpha Gateway is the HTTP front door to the Molpha oracle protocol. You send it a job execution request; it fans the request out to the oracle node set over a libp2p P2P network, waits for the nodes to fetch the data and produce an aggregated Schnorr signature, and returns the signed result to you. A single execute call gives you everything needed to settle a value on-chain: the value, the aggregated signature (s, rx, ryParity), the set of signers, and the on-chain commitment address.
The gateway is stateless with respect to results — every execute call triggers a fresh oracle round. There is no caching layer at the gateway; freshness is guaranteed by a per-request timestamp.

Base URL

https://gateway.molpha.io
All endpoints are unversioned at the host level; resource versioning is in the path (/v1/...).

Quick start

1

Discover the node set

Call GET /v1/nodes to learn which oracle nodes exist and their signing keys.
2

Read the job config

Call GET /v1/jobs/{jobId}/config to learn how many signatures the job requires.
3

Sign and execute

Build an authSig over the job ID and a current timestamp, then call POST /v1/jobs/{jobId}/execute to get the signed result.
The TypeScript SDK does all three steps for you — see Using the SDK.

Authentication

The execute endpoint authenticates the caller against the on-chain job owner. You prove ownership by signing a message with the job owner’s ed25519 key (a standard Solana keypair). The message to sign is:
keccak256( jobId_bytes || uint64_be(timestamp) )
  • jobId_bytes — the 32 raw bytes of the job ID.
  • timestamp — the same uint64 Unix-seconds value you put in the request body, big-endian.
The resulting 64-byte ed25519 signature is sent (hex-encoded) as authSig. The gateway recovers the job owner from Solana and rejects the request if the signature does not match.
Because the signed message includes the timestamp, the signature is single-use for a given timestamp. The SDK recomputes authSig on every retry attempt with a fresh timestamp.
import { keccak_256 } from "@noble/hashes/sha3.js";

function authMessage(jobId: string, timestamp: bigint): Uint8Array {
  const jobIdBytes = Buffer.from(jobId.replace(/^0x/, ""), "hex");
  const tsBuf = new Uint8Array(8);
  new DataView(tsBuf.buffer).setBigUint64(0, timestamp, false); // big-endian

  const preimage = new Uint8Array(jobIdBytes.length + 8);
  preimage.set(jobIdBytes, 0);
  preimage.set(tsBuf, jobIdBytes.length);

  return keccak_256(preimage);
}

Endpoints

GET /health

Liveness probe. Returns 200 OK when the service is up. No request parameters.
curl https://gateway.molpha.io/health
StatusMeaning
200Service healthy
503Service degraded

GET /v1/nodes

Returns the configured oracle node set, including each node’s libp2p address and the compressed secp256k1 signing key used for end-to-end encryption.
curl https://gateway.molpha.io/v1/nodes
Response
status
string
Always "ok".
data.nodes
Node[]
Response
{
  "status": "ok",
  "data": {
    "nodes": [
      {
        "index": 0,
        "peerId": "12D3KooW...",
        "address": "/ip4/127.0.0.1/tcp/9000",
        "signingKey": "02a1b2c3..."
      }
    ]
  }
}

GET /v1/jobs//config

Fetches the on-chain selection parameters for a job.
jobId
string
required
32-byte job ID, hex-encoded (64 hex chars). A leading 0x is accepted and stripped.
curl https://gateway.molpha.io/v1/jobs/$JOB_ID/config
Response
status
string
Always "ok".
data.signaturesRequired
number
Number of node signatures required to form a valid aggregated signature for this job.
data.redundancyBuffer
number
Extra nodes selected beyond signaturesRequired to tolerate failures. Group size = signaturesRequired + redundancyBuffer.
data.decimals
number
Fixed-point decimals the job’s value is scaled to.
Response
{
  "status": "ok",
  "data": {
    "signaturesRequired": 3,
    "redundancyBuffer": 1,
    "decimals": 8
  }
}
Errors
StatuserrorCause
400invalid job IDjobId is not 32-byte hex
404job not foundNo job account exists on-chain
500failed to fetch redundancy bufferUpstream Solana read failed

POST /v1/jobs//execute

Triggers an oracle round and returns the signed, aggregated result. This is the core endpoint.
jobId
string
required
32-byte job ID, hex-encoded (64 hex chars). A leading 0x is accepted and stripped.

Request body

timestamp
number
required
Unix time in seconds. Must satisfy |now - timestamp| ≤ maxAge. Also part of the signed authSig message. With maxAge: 0 only the exact current second is accepted.
registryVersion
number
Node-registry version the round should run against. Folded into the deterministic round ID.
maxAge
number
Allowed clock skew, in seconds, between timestamp and the gateway’s clock.
authSig
string
required
ed25519 signature (64 bytes, hex; 0x accepted) over keccak256(jobId_bytes || uint64_be(timestamp)). Verified against the on-chain job owner. See Authentication.
apiConfig
object
required
Describes the data source the nodes should fetch.
encKeyBundle
object | null
Optional end-to-end encryption envelope. Omit or send null for plaintext API configs. When present, the gateway forwards each node only its own envelope entry. See End-to-end encryption.
curl -X POST https://gateway.molpha.io/v1/jobs/$JOB_ID/execute \
  -H "Content-Type: application/json" \
  -d '{
    "timestamp": 1718650000,
    "registryVersion": 1,
    "maxAge": 60,
    "authSig": "0x<128-hex-char-signature>",
    "apiConfig": {
      "url": "https://api.example.com/price",
      "method": "GET",
      "headers": {},
      "responseParser": "$.data.price",
      "valueTransform": "multiply:1e6"
    },
    "encKeyBundle": null
  }'

Response

status
string
Always "completed" on success.
data
object
Response
{
  "status": "completed",
  "data": {
    "jobId": "1a2b3c...",
    "value": "65432.10",
    "valuePacked": "0x...",
    "timestamp": 1718650000,
    "registryVersion": 1,
    "signaturesRequired": 3,
    "configHash": "0x...",
    "signersBitmap": "0x07",
    "s": "0x...",
    "rx": "0x...",
    "ryParity": 0,
    "commitmentAddr": "0x...",
    "fresh": true
  }
}

Errors

All errors use the envelope { "error": "<message>" }.
Statuserror (examples)Cause
400jobId is requiredEmpty path parameter
400malformed request bodyBody is not valid JSON
400timestamp: is requiredtimestamp missing or zero
400timestamp: must be within the allowed time rangeClock skew exceeds maxAge
400jobId: must be 32-byte hexBad job ID format
400authSig: must be 64 bytes, got NSignature wrong length
400authSig: signature does not match job ownerSignature does not verify against the on-chain owner
400apiConfig.url: scheme must be http or httpsInvalid source URL
400apiConfig.method: must be a valid HTTP methodUnsupported method
503upstream timeoutNo aggregated signature within node.agg_wait_seconds
503(node-supplied message)A node reported an error for the round
500internal server errorUnexpected dispatch/build failure
A 503 typically means too few nodes responded in time. Retry with a fresh timestamp — this also re-rolls the deterministic node selection, which often resolves transient failures. The SDK does this automatically (maxRetries, default 15).

End-to-end encryption

When an API config contains secrets (API keys, tokens), you can encrypt it so that only the nodes selected for the round can read it — the gateway never sees the plaintext. The flow:
  1. Locally compute which nodes will be selected for (jobId, registryVersion, timestamp) using the deterministic selection algorithm.
  2. Resolve secret placeholders (e.g. {{secret.appid}}) in the API config.
  3. Encrypt the resolved config with a fresh symmetric key, then wrap that symmetric key for each selected node via ECDH against its signingKey.
  4. Send the result as encKeyBundle, keyed by each node’s index.
The SDK’s encrypt option handles all of this. The envelopes map must contain exactly the selected node indices, since selection is timestamp-dependent.
Node selection depends on timestamp. If you retry with a new timestamp, you must recompute the selection and re-encrypt for the new node set. The SDK does this on each attempt.

Using the SDK

The TypeScript SDK wraps node discovery, job config, deterministic selection, encryption, auth signing, and retries.
import { MolphaGateway, createSignerFromKeypairFile } from "@molpha/gateway-sdk";

const gw = new MolphaGateway("https://gateway.molpha.io");

// Sign as the job owner using a Solana keypair JSON file
const signer = createSignerFromKeypairFile("~/.config/solana/id.json");

const result = await gw.execute({
  jobId: "1a2b3c...",          // 64-char hex, no 0x
  registryVersion: 1,
  apiConfig: {
    url: "https://api.example.com/price",
    method: "GET",
    responseParser: "$.data.price",
    valueTransform: "multiply:1e6",
  },
  signer,
  maxAge: 60,
  maxRetries: 15,
});

console.log(result.value, result.s, result.rx, result.commitmentAddr);
The SDK retries with a fresh timestamp on each failed attempt, recomputing both authSig and the node selection.

Protocol internals (reference)

You don’t need these to use the API, but they explain how a round works under the hood.
  • Transport. The gateway sends each selected node a binary RoundRequest over a libp2p stream on protocol /molpha/1.0.0.
  • Aggregation. Nodes publish the aggregated signature as JSON on the GossipSub topic molpha/agg-sig/v1. The gateway matches the message by the deterministic round ID.
  • Deterministic round ID. Computed as keccak256( keccak256("MOLPHA_PULL_ROUND_V1") || jobId || uint32_be(registryVersion) || uint64_be(timestamp) ), so the same inputs always identify the same round.
  • Round ID ≠ randomness. Because the round ID is deterministic in timestamp, freshness and selection both rotate purely by changing the timestamp.