Skip to content
Orvyn

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.

Protocol overview

One request carries three separate mechanisms. They fail independently, and the sections below document each one on its own.

  1. Quote. An unpaid request for a paid resource answers 402 with machine-readable payment requirements.
  2. Settlement. The client signs an EIP-3009 transfer authorization. Orvyn broadcasts it, so the client spends no gas and holds no native token.
  3. 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:

ParameterValue
ChainRobinhood Chain, chain id 4663, CAIP-2 eip155:4663
Payment assetUSDG, 6 decimals, at 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168
Payment protocolx402 v2, scheme exact, settled by EIP-3009 transferWithAuthorization
Asset signing domainEIP-712 {name: "Global Dollar", version: "1"}, the domain a payment authorization is signed under
Registry0x1Fd5CDDBCA88Ccf92507A0E21cd487D9842a1F7c
Verifier0x684482efbC4F169679b067f6A58700Af453D08fa
Platform fee10% 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 validity30 days from purchase, non-refundable
Gateway base URLhttps://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}.

  1. Request the resource with no payment. The answer is 402 Payment Required with a PAYMENT-REQUIRED header holding base64 JSON: accepts[], each entry carrying scheme, network, amount, asset, payTo and maxTimeoutSeconds.
  2. Select an offer and sign the EIP-3009 authorization it describes, a transfer of amount to payTo under the USDG domain. Signing is off chain and costs nothing.
  3. Repeat the request with the payload in PAYMENT-SIGNATURE. The gateway verifies the authorization, settles it on chain, and returns 200 with the report envelope, the transaction hash in PAYMENT-RESPONSE.
terminal
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.

HeaderDirectionCarries
PAYMENT-REQUIREDResponse, 402Base64 JSON payment requirements, x402 v2
PAYMENT-SIGNATURERequestBase64 JSON of the signed EIP-3009 authorization
PAYMENT-RESPONSEResponse, 200Base64 JSON with transaction, network and payer. Present only when this response settled a payment
X-Orvyn-Pack-Calls-LeftResponse, 200Calls remaining in the pack this call was spent from
AuthorizationRequestBearer with a session token or an API key
SIGN-IN-WITH-XRequestBase64 JSON of a CAIP-122 message and its signature

The SDK performs all three steps inside a budget fixed at construction:

buy.ts
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
OptionTypeEffect
gatewayUrlstringRequired. Origin the client talks to
accountLocalAccountSigns payments and sign-ins. Without it the client can search, preview and verify, but not buy
apiKeystringA dashboard key (lk_…). Spends the packs of the wallet that made it and signs nothing. Cannot buy
networkeip155:${number}The only network a payment may be on. Default: Robinhood Chain
assetHexThe only token a payment may be in. Default: the USDG pinned in the package, never the address the gateway offers
maxSpendUsdgbigintHard budget in atomic USDG for the life of the instance. Default 5 USDG
packPreferencePackPreferenceWhat to buy when payment is needed. Default: a single call where the feed sells one, else the smallest pack
rpcUrl, registrystring, HexResolve 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 rpcUrl set, a payTo that 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:

x402.ts
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 pathReturns
GET /v1/configNetwork, USDG address and domain, contract addresses, fee for new providers, configured floor, live settlement gas estimate, pack validity and tiers
GET /v1/feedsFeed 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}/seriesDelayed unsigned series for charting, at most 160 points. range=24h|7d|30d
GET /v1/feeds/{feed}/reliabilitySeven days of heartbeat bars, each ok, late or miss, measured by Orvyn from stored reports
GET /v1/feeds/{feed}/previewThe value as of preview_delay ago, unsigned, with its observedAtMs. Rate limited, for display only
GET /v1/providersEvery 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/statsFeeds selling, providers, paid calls in 30 days, and the date counting started
GET /v1/activityRecent on-chain settlements, newest first. Pack redemptions are off chain and are counted, never listed
POST /v1/verifyBody: a report envelope. Returns each check as pass or fail, the same logic as the SDK and OrvynVerifier
GET /v1/sessions/challengeA fresh Sign-In-With-X challenge: domain, URI, nonce, issued-at, expiry, and the chains it may be signed for
POST /v1/sessionsBody {payload}. Exchanges a signed SIWx payload for a 15 minute session token, with the payer it resolved
GET /v1/packsThe caller packs. Needs a session or an API key
GET /.well-known/x402Discovery document: every paid resource with its offers
GET /llms.txtPlain-text guide for agents: what is sold, how to pay, how to verify
GET /v1/healthComponent status: gateway, database, chain lag, worker heartbeat, relayer balance

Two endpoints are paid, and both answer in signed report envelopes:

Method and pathCostReturns
GET /v1/feeds/{feed}/latest1 callThe latest report envelope for the feed
GET /v1/feeds/{feed}/historyA page price in calls, from a pack onlyUp 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:

  1. Authorization: Bearer with a session token from POST /v1/sessions,
  2. Authorization: Bearer with a dashboard API key,
  3. SIGN-IN-WITH-X with a CAIP-122 payload,
  4. PAYMENT-SIGNATURE with 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.

CardVault.sol
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 fieldTypeMeaning
maxAgeSecuint32Maximum age measured from observedAtMs. Zero reverts, so there is no way to read without naming a bound
allowedKindsuint8Bitfield. KIND_MANAGED 0x02, KIND_PROVIDER 0x04, KIND_AGGREGATE 0x08, KIND_ANY their union
minValue, maxValueint256Inclusive bounds on the reported value, in the feed scale

Three entry points, differing only in what they record:

FunctionRecordsUse for
_peekNothingA view, or a check whose result you discard
_readThe last observation accepted, allowing the same one againA contract that only keeps a current value
_readOnceThe last observation accepted, requiring a strictly newer oneA 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_LENGTH bytes, 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 equals report.signerKind,
  • report.decimals equals the decimals the registry holds for the feed,
  • observedAtMs is within maxAgeSec of 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:

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.

mcp.json
{
  "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"
      }
    }
  }
}
ToolPaysDoes
search_feedsNoSearch and filter the catalog
get_feedNoMetadata, schema, pricing, reliability
read_feedYes, inside the budgetBuys or spends a pack, returns value, payload and the verification result
verify_reportNoChecks a report envelope without buying anything
VariableRequiredEffect
ORVYN_GATEWAY_URLRequiredGateway origin. The server refuses to start without it
ORVYN_PRIVATE_KEYOptionalThe 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_KEYOptionalSpends packs bought ahead and can never pay, whatever the agent is told. The tighter grant of the two
ORVYN_MAX_SPEND_USDGOptionalAtomic USDG ceiling for the process lifetime. Default 1 USDG
ORVYN_NETWORK, ORVYN_ASSET, ORVYN_RPC_URL, ORVYN_REGISTRYOptionalOverride 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 modesignerKindMechanism
Orvyn, from your API1You 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 key2You 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
Aggregate3Cross-provider median with quorum and outlier exclusion. Specified, not built. See the roadmap
signer.ts
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.

FieldSolidity typeMeaning
feedIdbytes32The feed this observation belongs to
sequenceuint64Ordering within the feed. For first party signers this is the observation time in milliseconds
observedAtMsuint64When the value was true at the source. Every freshness rule is measured from here, never from publication
publishedAtMsuint64When Orvyn stored it. The gap between the two is what the reliability bars measure
valueint256The observation, scaled. Signed, so a negative rate or spread needs no offset
decimalsuint8The scale. Checked against the registry, so a feed cannot silently change it
confidenceuint256Spread across sources, in the same scale as value. Zero where a single source is definitive
payloadHashbytes32keccak256 of the canonical JSON (RFC 8785) of everything that is not the primary number. Zero where there is no payload
requestHashbytes32Binds request parameters into the signature. Zero until parameterized reads ship
sourceCountuint16How many upstream sources the value was derived from
signerKinduint81 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:

envelope.json
{
  "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:

terminal
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.

terminal
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.

CodeHTTPMeaning
BAD_REQUEST400The request was not understood.
PAYMENT_INVALID401The payment authorization was not accepted.
SETTLEMENT_FAILED402The payment could not be settled on chain.
FEED_UNAVAILABLE403This feed is not selling right now.
FEED_NOT_FOUND404This feed does not exist.
RATE_LIMITED429Too many requests. Wait a moment and try again.
BAD_REPORT502The provider returned a report that failed its signature check.
STALE_DATA503The latest report is too old to sell. Try again when the feed recovers.
SERVICE_UNAVAILABLE503Orvyn 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 one OrvynVault per 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 and POST /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

ItemWhat it adds
Parameterized readsGET /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 paymentx402 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 feedsCross-provider median at signerKind 3, with quorum, outlier exclusion and published confidence
Provider reputationA score derived from measured uptime, latency, deviation from aggregates and takedown history. Not a token
Verification feeAn 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 adapterA 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-chainVerifier and vault factory on a second EVM chain with a mirrored registry, and one 402 offer per network
StreamingSSE or WebSocket delivery for pack holders, instead of one HTTP round trip per read
ToolingPython SDK, CSV export, provider alerts, a status page