Docs
Orvyn is a per-request market for signed observations. A client pays in USDG over x402, an open payment protocol layered on HTTP status 402, and receives a report signed under EIP-712. Any contract can verify that report against the on-chain signer registry, so a paid answer does not have to be a trusted one.
The packages on this page are not on npm yet
/v1, and works with curl or any x402 v2 client, as the first section shows.Protocol overview
One request carries three separate mechanisms. They fail independently, and the sections below document each one on its own.
- Quote. An unpaid request for a paid resource answers
402with machine-readable payment requirements. - Settlement. The client signs an EIP-3009 transfer authorization. Orvyn broadcasts it, so the client spends no gas and holds no native token.
- Verification. The response body carries an EIP-712 signed report. Verification is independent of payment and of this gateway.
The parameters this deployment enforces, read from the gateway rather than written into this page:
| Parameter | Value |
|---|---|
| Chain | Robinhood Chain, chain id 4663, CAIP-2 eip155:4663 |
| Payment asset | USDG, 6 decimals, at 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 |
| Payment protocol | x402 v2, scheme exact, settled by EIP-3009 transferWithAuthorization |
| Asset signing domain | EIP-712 {name: "Global Dollar", version: "1"}, the domain a payment authorization is signed under |
| Registry | 0x1Fd5CDDBCA88Ccf92507A0E21cd487D9842a1F7c |
| Verifier | 0x684482efbC4F169679b067f6A58700Af453D08fa |
| Platform fee | 10% for a provider registering now, taken at withdrawal and locked per provider at registration |
| Settlement floor | $0.20 per call, derived from a measured settlement cost of $0.014283 divided by the fee rate. Below it a feed sells packs, never single calls |
| Pack validity | 30 days from purchase, non-refundable |
| Gateway base URL | https://orvyn.me/gw, all paths under /v1 |
Two conventions hold across the whole API. Any integer that can exceed 253 travels as a decimal string in JSON, which covers values, amounts, sequences and millisecond timestamps. Amounts are atomic USDG at 6 decimals, so "2000" is $0.002.
Buy a report over HTTP
A feed is addressed either as {handle}/{slug} or as its 32 byte feedId. Both resolve to the same record, and both are accepted wherever a path below says {feed}.
- Request the resource with no payment. The answer is
402 Payment Requiredwith aPAYMENT-REQUIREDheader holding base64 JSON:accepts[], each entry carrying scheme, network, amount, asset,payToandmaxTimeoutSeconds. - Select an offer and sign the EIP-3009 authorization it describes, a transfer of
amounttopayTounder the USDG domain. Signing is off chain and costs nothing. - Repeat the request with the payload in
PAYMENT-SIGNATURE. The gateway verifies the authorization, settles it on chain, and returns200with the report envelope, the transaction hash inPAYMENT-RESPONSE.
curl -i https://orvyn.me/gw/v1/feeds/orvyn/eur-usd-ecb-reference/latest # HTTP/1.1 402 Payment Required # PAYMENT-REQUIRED: eyJ4NDAyVmVyc2lvbiI6Mi… curl https://orvyn.me/gw/v1/feeds/orvyn/eur-usd-ecb-reference/latest \ -H "PAYMENT-SIGNATURE: <base64 payment payload>" # HTTP/1.1 200 OK # PAYMENT-RESPONSE: eyJzdWNjZXNzIjp0cnVlLC…
The headers that carry the exchange. Every request header below is on the CORS allow list and every response header on the expose list, so a browser can both send and read them.
| Header | Direction | Carries |
|---|---|---|
| PAYMENT-REQUIRED | Response, 402 | Base64 JSON payment requirements, x402 v2 |
| PAYMENT-SIGNATURE | Request | Base64 JSON of the signed EIP-3009 authorization |
| PAYMENT-RESPONSE | Response, 200 | Base64 JSON with transaction, network and payer. Present only when this response settled a payment |
| X-Orvyn-Pack-Calls-Left | Response, 200 | Calls remaining in the pack this call was spent from |
| Authorization | Request | Bearer with a session token or an API key |
| SIGN-IN-WITH-X | Request | Base64 JSON of a CAIP-122 message and its signature |
The SDK performs all three steps inside a budget fixed at construction:
import {Orvyn} from '@orvyn/sdk'
const orvyn = new Orvyn({
gatewayUrl: 'https://orvyn.me/gw',
account, // any viem account holding USDG
maxSpendUsdg: 5_000_000n, // 5 USDG, atomic, for the life of this instance
rpcUrl, // optional: resolve payTo and the signer on chain
})
const report = await orvyn.read('orvyn/eur-usd-ecb-reference')
report.value // bigint, scaled by report.decimals
report.text() // exact decimal text, no float in the path
report.encoded // pass with report.signature into a contract call| Option | Type | Effect |
|---|---|---|
| gatewayUrl | string | Required. Origin the client talks to |
| account | LocalAccount | Signs payments and sign-ins. Without it the client can search, preview and verify, but not buy |
| apiKey | string | A dashboard key (lk_…). Spends the packs of the wallet that made it and signs nothing. Cannot buy |
| network | eip155:${number} | The only network a payment may be on. Default: Robinhood Chain |
| asset | Hex | The only token a payment may be in. Default: the USDG pinned in the package, never the address the gateway offers |
| maxSpendUsdg | bigint | Hard budget in atomic USDG for the life of the instance. Default 5 USDG |
| packPreference | PackPreference | What to buy when payment is needed. Default: a single call where the feed sells one, else the smallest pack |
| rpcUrl, registry | string, Hex | Resolve the vault and the signer set on chain instead of accepting the gateway answer |
Before it signs anything, the SDK rejects:
- an offer on any network but the configured one,
- any asset but the USDG pinned in the package,
- an amount that is not
pricePerCall × calls, - any payment that would take the instance past
maxSpendUsdg, - with
rpcUrlset, apayTothat is not the CREATE2 vault the registry derives for that feed’s provider.
The first four cost nothing and run offline. The fifth is one RPC call and is the one worth carrying into your own client: a vault address is a pure function of the provider id and the factory, so any other recipient is an answer Orvyn did not produce. A read also signs in first, one free signature, so a pack bought by an earlier process is spent before anything is bought again.
A stock x402 client works as well. Its spend controls only carry the assets it ships with, so USDG has to be allowed explicitly or every offer is rejected before it is shown:
spendControls: {
allowedAssets: [{
network: 'eip155:4663',
asset: '0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168',
maxAmountPerPayment: '2000000',
}],
}If you settle into Orvyn vaults with your own facilitator, note that USDG does not revert on a replayed EIP-3009 nonce. It succeeds, moves nothing and emits AuthorizationAlreadyUsed. Read authorizationState(from, nonce) before sending, and confirm by the Transfer log rather than by receipt status alone.
Endpoint reference
Metadata, discovery and verification are free and need no wallet:
| Method and path | Returns |
|---|---|
| GET /v1/config | Network, USDG address and domain, contract addresses, fee for new providers, configured floor, live settlement gas estimate, pack validity and tiers |
| GET /v1/feeds | Feed summaries. Filters: q, category, provider, kind, maxPriceUsdg, sort. include=preview adds the delayed public value |
| GET /v1/feeds/{feed} | Full metadata: schema, methodology, sources with their URLs, rights declaration, heartbeat, pricing, signers, reliability |
| GET /v1/feeds/{feed}/series | Delayed unsigned series for charting, at most 160 points. range=24h|7d|30d |
| GET /v1/feeds/{feed}/reliability | Seven days of heartbeat bars, each ok, late or miss, measured by Orvyn from stored reports |
| GET /v1/feeds/{feed}/preview | The value as of preview_delay ago, unsigned, with its observedAtMs. Rate limited, for display only |
| GET /v1/providers | Every provider with feed count, paid calls in 30 days and measured uptime |
| GET /v1/providers/{provider} | Profile, feeds, vault address, fee rate, pending payout change |
| GET /v1/stats | Feeds selling, providers, paid calls in 30 days, and the date counting started |
| GET /v1/activity | Recent on-chain settlements, newest first. Pack redemptions are off chain and are counted, never listed |
| POST /v1/verify | Body: a report envelope. Returns each check as pass or fail, the same logic as the SDK and OrvynVerifier |
| GET /v1/sessions/challenge | A fresh Sign-In-With-X challenge: domain, URI, nonce, issued-at, expiry, and the chains it may be signed for |
| POST /v1/sessions | Body {payload}. Exchanges a signed SIWx payload for a 15 minute session token, with the payer it resolved |
| GET /v1/packs | The caller packs. Needs a session or an API key |
| GET /.well-known/x402 | Discovery document: every paid resource with its offers |
| GET /llms.txt | Plain-text guide for agents: what is sold, how to pay, how to verify |
| GET /v1/health | Component status: gateway, database, chain lag, worker heartbeat, relayer balance |
Two endpoints are paid, and both answer in signed report envelopes:
| Method and path | Cost | Returns |
|---|---|---|
| GET /v1/feeds/{feed}/latest | 1 call | The latest report envelope for the feed |
| GET /v1/feeds/{feed}/history | A page price in calls, from a pack only | Up to 500 envelopes between two Unix millisecond times (from, to, limit), oldest first, inside the 48 hour retention. An empty range costs nothing. A page is worth a few calls, under the settlement floor for almost every feed, so it is never settled on its own: without a pack the answer is a 402 offering packs |
A paid endpoint accepts credentials in this order, and stops at the first one present:
Authorization: Bearerwith a session token fromPOST /v1/sessions,Authorization: Bearerwith a dashboard API key,SIGN-IN-WITH-Xwith a CAIP-122 payload,PAYMENT-SIGNATUREwith an x402 v2 payload answering a previous 402.
With none of them, or with a pack that has no calls left, the answer is 402.
Verify in a contract
Pass encoded and signature into your function and let OrvynConsumer apply the policy. Verification reads the registry and nothing else: it does not call the gateway, and it holds for a report bought by anyone, at any time inside its age bound.
import {OrvynConsumer} from "@orvyn/contracts/src/OrvynConsumer.sol";
contract CardVault is OrvynConsumer {
bytes32 constant CARD_FEED = 0x…;
constructor(IOrvynVerifier v) OrvynConsumer(v) {}
function settle(bytes calldata report, bytes calldata sig) external {
int256 cents = _readOnce(CARD_FEED, report, sig, Policy({
maxAgeSec: 3_600,
allowedKinds: KIND_PROVIDER, // first party only
minValue: 1,
maxValue: 10_000_000
}));
}
}| Policy field | Type | Meaning |
|---|---|---|
| maxAgeSec | uint32 | Maximum age measured from observedAtMs. Zero reverts, so there is no way to read without naming a bound |
| allowedKinds | uint8 | Bitfield. KIND_MANAGED 0x02, KIND_PROVIDER 0x04, KIND_AGGREGATE 0x08, KIND_ANY their union |
| minValue, maxValue | int256 | Inclusive bounds on the reported value, in the feed scale |
Three entry points, differing only in what they record:
| Function | Records | Use for |
|---|---|---|
| _peek | Nothing | A view, or a check whose result you discard |
| _read | The last observation accepted, allowing the same one again | A contract that only keeps a current value |
| _readOnce | The last observation accepted, requiring a strictly newer one | A function that pays out, so one purchased report drives it once |
The verifier reverts unless every one of these holds, and each has its own error selector:
- the encoded report is exactly
ENCODED_REPORT_LENGTHbytes, so one signed report has one encoding, - the signature is 65 bytes with a low s value and recovers to a non-zero address,
- that address is a signer registered for this
feedId, and its registered kind equalsreport.signerKind, report.decimalsequals the decimals the registry holds for the feed,observedAtMsis withinmaxAgeSecof the block time and no more than 5 seconds ahead of it.
OrvynConsumer adds the feed identity check, the kind filter, the value bounds and the ordering rule on top. The same verification is one call from a terminal:
cast call 0x684482efbC4F169679b067f6A58700Af453D08fa \ "verifyEncoded(bytes,bytes,uint32)" <encoded> <signature> 3600 \ --rpc-url <rpc>
Agent integration
The MCP server exposes four tools over stdio. Only read_feed can spend, and the budget is enforced inside the process before anything is signed, so nothing the agent reads can raise it.
{
"mcpServers": {
"orvyn": {
"command": "npx",
"args": ["-y", "@orvyn/mcp"],
"env": {
"ORVYN_GATEWAY_URL": "https://orvyn.me/gw",
"ORVYN_PRIVATE_KEY": "<a wallet made for this agent>",
"ORVYN_MAX_SPEND_USDG": "1000000"
}
}
}
}| Tool | Pays | Does |
|---|---|---|
| search_feeds | No | Search and filter the catalog |
| get_feed | No | Metadata, schema, pricing, reliability |
| read_feed | Yes, inside the budget | Buys or spends a pack, returns value, payload and the verification result |
| verify_report | No | Checks a report envelope without buying anything |
| Variable | Required | Effect |
|---|---|---|
| ORVYN_GATEWAY_URL | Required | Gateway origin. The server refuses to start without it |
| ORVYN_PRIVATE_KEY | Optional | The wallet that pays. Without it the server searches, describes and verifies, and refuses to buy. Give the agent a wallet of its own, funded with what you accept losing |
| ORVYN_API_KEY | Optional | Spends packs bought ahead and can never pay, whatever the agent is told. The tighter grant of the two |
| ORVYN_MAX_SPEND_USDG | Optional | Atomic USDG ceiling for the process lifetime. Default 1 USDG |
| ORVYN_NETWORK, ORVYN_ASSET, ORVYN_RPC_URL, ORVYN_REGISTRY | Optional | Override the pinned network and asset, and resolve vaults on chain |
An agent framework that already speaks x402 needs none of this. Every offer carries Bazaar metadata with input and output schemas, and /llms.txt and /.well-known/x402 are the discovery surface.
Publish a feed
Open the dashboard and register as a provider, one transaction, then create a feed, one transaction each. Registration derives your vault address deterministically with CREATE2 and locks your fee rate at the value in force that day. The vault contract itself is deployed on first use.
| Signing mode | signerKind | Mechanism |
|---|---|---|
| Orvyn, from your API | 1 | You supply an HTTPS endpoint and a JSON path. The worker fetches on your heartbeat and signs what it read, as a labelled notary. Every surface that shows the report carries that label |
| You, with your own key | 2 | You run the signer SDK on your own infrastructure. Reports are first party. The worker polls your signer once per heartbeat and re-checks every report against the registry before selling it |
| Aggregate | 3 | Cross-provider median with quorum and outlier exclusion. Specified, not built. See the roadmap |
import {createSigner} from '@orvyn/signer'
export const handler = createSigner({
feedId: '0x…', // from the wizard
decimals: 2,
requester: '0x…', // Orvyn's requester key, from the wizard
privateKey: process.env.ORVYN_SIGNER_KEY,
async observe() {
const sale = await db.medianSale('psa9-charizard')
return {value: BigInt(sale.cents), observedAtMs: sale.at, payload: {sales: sale.count}}
},
})The handler is a standard fetch handler, so it runs on Node 18 and later, Vercel, Cloudflare Workers, Deno and Bun. It serves GET /orvyn/latest and GET /orvyn/health, and refuses any request without a valid X-Orvyn-Request header signed by requester. Set that field: without it the endpoint answers whoever finds it, and what it hands out is the signed report you sell. The sequence number is the observation time in milliseconds, so the handler holds no state. When observe() throws, no report is stored and the feed stops selling until it recovers, rather than selling a value that is past its heartbeat.
A provider cannot register its own key under kind 1 or kind 3. The registry accepts those only with an on-chain attestation from the Orvyn attester key, so the label on a report is a claim the chain backs rather than one the provider made.
Payments move from the buyer’s wallet into your vault in a single transfer. Orvyn never holds them, and no operator key can move a vault balance anywhere except to your payout address and the fixed fee share. The platform fee is 10% of a withdrawal, at the rate locked when you registered. Orvyn’s policy for the fees it receives is 80% to buy back its token and burn it and 20% to operations. That split is a treasury policy carried out on chain, not something the contracts enforce, and it begins once the token exists.
Report format
A report is eleven fields signed under the EIP-712 domain {name: "Lumoracle", version: "1"}. The domain carries no chainId and no verifyingContract, deliberately: a report states a fact about the world, so the same signature verifies on any chain that mirrors the signer registry. The name is Orvyn’s earlier one, which the contracts were deployed under, and it does not change.
| Field | Solidity type | Meaning |
|---|---|---|
| feedId | bytes32 | The feed this observation belongs to |
| sequence | uint64 | Ordering within the feed. For first party signers this is the observation time in milliseconds |
| observedAtMs | uint64 | When the value was true at the source. Every freshness rule is measured from here, never from publication |
| publishedAtMs | uint64 | When Orvyn stored it. The gap between the two is what the reliability bars measure |
| value | int256 | The observation, scaled. Signed, so a negative rate or spread needs no offset |
| decimals | uint8 | The scale. Checked against the registry, so a feed cannot silently change it |
| confidence | uint256 | Spread across sources, in the same scale as value. Zero where a single source is definitive |
| payloadHash | bytes32 | keccak256 of the canonical JSON (RFC 8785) of everything that is not the primary number. Zero where there is no payload |
| requestHash | bytes32 | Binds request parameters into the signature. Zero until parameterized reads ship |
| sourceCount | uint16 | How many upstream sources the value was derived from |
| signerKind | uint8 | 1 managed, 2 provider, 3 aggregate. Must equal the kind the registry holds for the signer |
The envelope around it is what both the SDK and a contract consume:
{
"report": {
"feedId": "0x…", "sequence": "1790016052000",
"observedAtMs": "1790016052000", "publishedAtMs": "1790016052412",
"value": "10432", "decimals": 2, "confidence": "0",
"payloadHash": "0x…", "requestHash": "0x00…00",
"sourceCount": 3, "signerKind": 2
},
"signature": "0x…", // 65 bytes, EIP-712 over the fields above
"signer": "0x…", // recovers from the signature, checked against the registry
"payload": { "sales": 12 }, // canonical JSON of this hashes to payloadHash
"encoded": "0x…", // abi.encode of the report, what a contract takes
"feed": { "slug": "…", "provider": "…", "schema": "…" }
}Paste any envelope into the report checker to see each check pass or fail, or post it to POST /v1/verify for the same result as JSON.
Packs, sessions, keys
Settling a payment on chain costs gas, which Orvyn pays out of the platform fee. The break-even is the settlement cost divided by the fee rate, which is where the floor of $0.20 per call comes from. The gateway raises it per provider when live gas divided by that provider fee rate is higher. A feed priced below the floor never settles a single call: it sells packs, one payment for N calls, spent afterwards with a wallet signature and no transaction.
A pack belongs to the wallet that paid for it, so spending one means proving that wallet. Packs last 30 days and are not refundable, because the money is already in the provider vault. A provider cannot archive a feed while unexpired packs exist against it.
In a browser, open a session:
curl https://orvyn.me/gw/v1/sessions/challenge
# {"info":{"domain":"…","uri":"…","version":"1","nonce":"…",
# "issuedAt":"…","expirationTime":"…","statement":"…"},
# "supportedChains":[{"chainId":"eip155:…","type":"eip191"}]}
curl -X POST https://orvyn.me/gw/v1/sessions \
-H "Content-Type: application/json" \
-d '{"payload": <the SIWx payload, object or base64 string>}'
# {"token":"…","payer":"0x…","expiresInSec":900}On a server, make an API key in the dashboard instead. Both spend the packs of the same wallet, and every answer carries how many calls are left.
curl -i https://orvyn.me/gw/v1/feeds/orvyn/eur-usd-ecb-reference/latest \ -H "Authorization: Bearer lk_…" # HTTP/1.1 200 OK # X-Orvyn-Pack-Calls-Left: 99
A key is lk_ followed by 43 characters, shown once, stored only as its SHA-256. It spends and cannot buy: it holds no wallet and signs no payment, so a leaked key costs at most the calls left and never moves USDG. When its wallet has no call left on a feed the answer is the ordinary 402 quote, and the next pack is bought with the wallet. A wallet holds up to 10 keys and can revoke any of them at any time. Keys are created and revoked only by the web app behind its own sign-in; the gateway exposes no route that creates one.
Error codes
Errors are {"error": {"code": "…", "message": "…", "retryAfterMs": 5000}}, with the code the stable part. There is no error for which a buyer is charged: settlement is attempted only after a sellable report exists, and the report is returned only after settlement confirms.
| Code | HTTP | Meaning |
|---|---|---|
| BAD_REQUEST | 400 | The request was not understood. |
| PAYMENT_INVALID | 401 | The payment authorization was not accepted. |
| SETTLEMENT_FAILED | 402 | The payment could not be settled on chain. |
| FEED_UNAVAILABLE | 403 | This feed is not selling right now. |
| FEED_NOT_FOUND | 404 | This feed does not exist. |
| RATE_LIMITED | 429 | Too many requests. Wait a moment and try again. |
| BAD_REPORT | 502 | The provider returned a report that failed its signature check. |
| STALE_DATA | 503 | The latest report is too old to sell. Try again when the feed recovers. |
| SERVICE_UNAVAILABLE | 503 | Orvyn cannot take payments right now. |
Two further codes exist in the wire type and are never sent by the gateway. PACK_EMPTY is raised by the SDK before it calls, when the key you gave it has no pack left and no account to pay from. PROVIDER_TIMEOUT is reserved: a first party signer that stops answering leaves the last report standing, and the feed turns STALE_DATA once that report passes twice its heartbeat. Handle both, as any client should handle a code it does not know.
Roadmap
What is listed as shipped is deployed and running on the network this build talks to. What is planned is specified and unscheduled, and none of it is a commitment with a date attached.
Shipped
- Contracts deployed and verified on Robinhood Chain:
OrvynRegistry,OrvynVerifier, and oneOrvynVaultper provider at a CREATE2 address, deployed lazily on first use. - Test evidence: 76 Foundry tests at 100% line, branch and function coverage of the four contracts, five invariants over 256,000 calls, and a fork test of real USDG settlement into a vault.
- x402 v2 gateway with its own facilitator in process, EIP-3009 settlement, packs, SIWx sessions and API keys.
- Managed connectors and first party signer intake, heartbeat polling, and reliability measured from stored reports rather than claimed.
- Consumer SDK, signer SDK, MCP server and Solidity consumer library, built and tested, publication pending.
- On-chain verification through
verifyEncoded, plus the same checks off chain at /verify andPOST /v1/verify.
In progress
- Publishing the four packages to npm under their final scope, which is what freezes the names in these snippets.
- Widening the catalog to feeds whose upstream terms permit resale, each with its terms quoted in the feed methodology.
- First party providers running their own signers, which is the mode the protocol is built around.
Planned
| Item | What it adds |
|---|---|
| Parameterized reads | GET /v1/feeds/{feed}/query, a provider-signed answer to a question with parameters, with requestHash bound into the signature. Needs a per-request round trip to the provider signer, which the current pull model does not make |
| Sub-floor per-call payment | x402 upto through Permit2, or a deferred voucher escrow where the buyer deposits once, signs cumulative vouchers and the provider redeems in batches. Also the answer to pack refunds |
| Aggregate feeds | Cross-provider median at signerKind 3, with quorum, outlier exclusion and published confidence |
| Provider reputation | A score derived from measured uptime, latency, deviation from aggregates and takedown history. Not a token |
| Verification fee | An optional per-feed fee charged by the verifier and paid to the vault, which is the answer to free riding on a resold report |
| Push adapter | A permissionless contract that stores the latest verified report and exposes AggregatorV3Interface, so consumers written for the Chainlink shape can read an Orvyn feed unchanged |
| Multi-chain | Verifier and vault factory on a second EVM chain with a mirrored registry, and one 402 offer per network |
| Streaming | SSE or WebSocket delivery for pack holders, instead of one HTTP round trip per read |
| Tooling | Python SDK, CSV export, provider alerts, a status page |
The contracts carry no third-party audit