Creating your own feed requires an active subscription. Reading existing feeds requires no subscription. This guide covers the producer flow: subscribe, create a job, then execute rounds to keep the feed updated.
Subscribe and create a job
Subscribe to a plan
Subscriptions are paid in USDC on Solana. Fetch the live plan price first and pass it as a safety bound — the transaction aborts if the on-chain price is higher than what the user approved.import { PlanType } from "@molpha-oracle/sdk";
const plan = await sdk.solana.getPlan(PlanType.Basic);
const { pricePaid } = await sdk.solana.subscribe(PlanType.Basic, {
maxPriceUsdc: plan.subscriptionPrice,
});
Plan tiers are Basic, Standard, Professional, Enterprise (stable u8 ordinals 0..3). Each Plan defines subscription_price (monthly, USDC base units), max_jobs, max_signers, and whether private APIs are enabled. Extend later with extend_subscription — it adds one month and tops up the prepaid balance. Create the job
A job commits to a specific off-chain data source and parsing logic. Only the hash of the API config is stored on-chain. createJob initializes both the Job account and its Feed account in one instruction.import { deriveApiConfigHash } from "@molpha-oracle/sdk";
const apiConfig = {
url: "https://api.example.com/price",
responseParser: "$.price",
};
const { jobId } = await sdk.solana.createJob({
apiConfigHash: deriveApiConfigHash(apiConfig),
signaturesRequired: 3,
decimals: 8,
});
Constraints enforced on-chain
- Subscription active (
valid_until in the future) — otherwise SubscriptionNotActive
signatures_required <= plan.max_signers — otherwise SignaturesExceedPlanMax
job_count < plan.max_jobs — otherwise JobLimitReached
job_id == keccak256("MOLPHA_JOB_V1" || owner || api_config_hash) — otherwise InvalidJobId
With raw Anchor
const apiConfigHash = /* 32-byte keccak of your API configuration */;
const jobId = deriveJobId(wallet.publicKey, apiConfigHash);
await program.methods
.createJob(
{ apiConfigHash: Array.from(apiConfigHash), signaturesRequired: 8, decimals: 8 },
Array.from(jobId)
)
.accountsPartial({
owner: wallet.publicKey,
protocolConfig: protocolConfigPda,
plan: planPda,
job: findJobPda(jobId),
subscription: subscriptionPda,
feed: findFeedPda(jobId),
systemProgram: SystemProgram.programId,
})
.rpc();
See the PDA derivation reference for all seeds. add_delegate / remove_delegate authorize up to 5 additional addresses to manage the job; this does not affect how the feed is read.
Execute a round
requestAndSubmit runs a gateway round against the current on-chain registry version, receives a threshold-signed data update, and submits it to Solana — all in one call.
const { result, signature } = await sdk.requestAndSubmit(jobId, { apiConfig });
console.log("Submitted in tx:", signature);
Under the hood this is equivalent to:
const result = await sdk.gateway.requestSignedData({ jobId, apiConfig });
const { signature } = await sdk.solana.submitDataUpdate(result);
The result
The gateway returns a DataUpdateResult — a completed round, ready to submit on-chain. This flat payload maps directly onto every chain’s verifier structs — see verify on EVM, Starknet, and Solana.
Without a signAuthMessage-capable wallet (or a keypair-backed wallet), gateway execution falls back to an all-zero authSig. That path is for development only — production jobs should authenticate gateway execution.
For fast repeated rounds, cache registry version, node set, and job config with sdk.gateway.prepareContext(jobId) so each subsequent round is a single gateway POST. See the Gateway API.