Skip to content
LogoLogo

Pricing

What a service costs, read from the chain

v1 read prices from the Ocean subgraph. That is gone — Config has no subgraphUri at all any more, and nautilus no longer depends on urql or graphql. Prices now come from contract reads, the same way ocean.js derives them.

For most flows you never need this: access orders whatever is required. Reach for it when you want to show a price before committing to a purchase.

Two steps

Pricing is read in two calls: what scheme the datatoken uses, then what one order costs.

const  = await .('did:ope:1234abcd...')
const  = ()[0].
const  = (, ) as string
 
const  = .()
const  = .()
 
const  = await (, , ) 
const  = await (, , ) 
 
.(.)

PricingInfo

FieldMeaning
schema'fixed', 'free' or 'none'
templateIdDatatoken template: 1 basic, 2 enterprise, 4 confidential EVM
datatokenAddressThe token being priced
exchangeIdThe fixed-rate exchange id, when schema is 'fixed'
baseTokenAddressWhat the exchange charges in
baseTokenDecimalsDecimals of that token
publishMarketFeeThe publisher's market fee

OrderPrice

Every field is a decimal string.

FieldMeaning
totalThe base-token spend — what the exchange takes
baseTokenAmountThe same amount, named for what it is
opcFeeOcean Protocol Community fee
publishMarketFeeThe publishing market's cut, in its own token
consumeMarketFeeWhat your market collects, if you charge anything
consumeMarketThe { address, fee } this quote was priced with

total already contains every swap-side fee, so never sum the fields yourself.

order() approves everything the order pulls from you: the purchase, the publish-market fee and the provider fee. An ERC20 approval replaces the previous one rather than adding to it, so amounts that share a token and a spender get one allowance for their sum, and an allowance you already hold is not approved again. Every check (pricing, exchange, fee collectors, the provider fee) runs before the first approval.

Charging a consume-market fee

Your cut is a fraction of the swap, not an amount — '0.01' is 1% — and it needs the account that collects it. The exchange pays its swap fee to whichever address the order names, so a fee without a collector is either lost or paid to somebody else.

const  = await (, , , {
  : '0xYourMarketCollector', 
  : '0.01' // 1% of the swap
})
 
// `price` carries the fee, so the order charges what was quoted, to the address it was
// quoted for.
await ({ , , , , : 0, ,  })

Do you already hold an order?

hasReusableOrder answers from what the node reports in initialize: validOrder, when the node found a previous order still inside the service's timeout.

if (()) {
  // Already paid, still inside the timeout — no new order needed.
}

nautilus reuses an order whenever the node reports one in validOrder: as it stands when no new provider fee is due, or extended with reuseOrder when one is. What initialize returns depends on the endpoint:

ocean-nodeAccess initializeinitializeCompute
3.0 – 4.2{ providerFee, datatoken, nonce, computeAddress }, with no validOrdervalidOrder per input, when the request carries a transferTxId that is still valid

Downloads: the order on chain

The access initialize reports no validOrder, so access looks your previous orders up on chain: the datatoken's OrderStarted events with you as consumer, and its OrderReused events with you as caller. It checks each transaction, newest first, from its receipt exactly as the node checks the transferTxId of a download:

  • when the transaction holds an OrderReused, the first one is followed to the order it names (nautilus also requires you to be that reuse's caller);
  • in the order's transaction, the first OrderStarted of this datatoken with you as consumer or payer is taken, and refused when it is for another service index;
  • the order is inside the service's timeout, counted from the OrderStarted block (a reuse does not restart it). 0 never expires, and neither does a missing timeout, as at the node;
  • the transaction itself carries a provider fee signed by the service's node for this service that the node keeps: validUntil is 0, or the time since the transaction's block is at most validUntil. The node signs validUntil as an absolute time but compares it to that elapsed time, so in practice it keeps every fee it signed.

With all four, the download uses that transaction and nothing is sent, so no provider fee is paid and no consent is asked, even when the node quotes one. With the first three only, the order is extended with reuseOrder and the fee the node quotes now. Otherwise a new order is placed. Either way reusedOrder and transferTxId say which.

An order is reused only with at least 10 minutes of its timeout left, as it stands or extended: the node checks it again at download, and a reuse does not restart the timeout, so an order about to expire is ordered anew rather than extended for a download that would fail. A service whose timeout is 10 minutes or less is ordered anew every time. Orders mined in the last two blocks are not reused either, since a reorg could still drop them.

The events are read newest first, 2 000 blocks per eth_getLogs call (narrowed to 1 000, 500, 200, 100 blocks and below while the RPC refuses the range, and kept at the size that works; a rate limit is not taken for a refusal), back to the first block inside the service's timeout and at most 100 000 blocks, in as many rounds of reads as those blocks need at 100 blocks per round (at least 100) and for at most 5 minutes (a call the RPC does not answer in time ends the lookup then), checking at most 50 candidates. On an RPC that serves 100 blocks per call, a 24-hour window on OP Sepolia (2-second blocks) takes about 450 rounds. "Not found" means the whole window was read. When a bound ends the read first (a timeout longer than 100 000 blocks, a timeout of 0, an RPC that serves fewer than 100 blocks per call or answers too slowly), with no order found, the lookup is incomplete: an older order may still be in force.

When the lookup is incomplete, or the RPC fails (a rate limit, no answer, an error), "not found" cannot be told apart from "not checked". On a service that costs anything nautilus then does not buy: it throws an OrderCheckError before paying, with nothing ordered, and the call can be retried. A free service is ordered with a warning, and orderOnLookupFailure: true orders a paid one anyway, also with a warning. For a paid service whose orders never expire (timeout: 0) on a chain longer than 100 000 blocks, a first purchase therefore needs orderOnLookupFailure: true.

What the node charges on top

The price above is read from chain. Two more amounts are not: the node chooses them.

  • The provider fee, which every order carries: the node's initialize (or initializeCompute) answer names its token, its amount and who receives it (providerFeeAddress), and signs them. For a download, that node is the one in the service's serviceEndpoint, which the publisher chooses. The signature check nautilus runs only proves the node signed the fee, so that the order does not revert. It says nothing about whether the amount is reasonable.
  • The escrow payment of a paid compute job: initializeCompute names the escrow contract and the amount to lock.

So nautilus pays neither without your consent. A non-zero provider fee needs maxProviderFee (per token, in the token's smallest unit) or a confirmProviderFees callback that returns true; an escrow payment needs maxEscrowPayment or confirmEscrowPayment, and is only ever paid into one escrow contract: the chain's EnterpriseEscrow in Ocean's address data by default (its Escrow where it lists no EnterpriseEscrow), or the one you set as config.escrow. The node's choice never counts (see Escrow contract). Otherwise access and compute throw a ProviderFeeNotAllowedError or EscrowPaymentNotAllowedError before any approval, purchase, deposit or order. Zero fees need nothing, and neither does a zero escrow payment, even one that names no escrow contract.

Consent is asked only for the provider fee that will be paid. access decides first whether to reuse a previous order: one used as it stands pays no fee, so nothing is asked even when the node quotes a fee; one that is extended, or a new order, asks for the fee the node quotes now.

Set in Nautilus.create, these options are defaults in pairs: a call that sets maxProviderFee or confirmProviderFees uses its own pair and neither default, and the same holds for maxEscrowPayment / confirmEscrowPayment. A tighter ceiling on one call therefore never falls back to a permissive default callback.

const  = await .(, {
  : { : , : ('0.5', 18) }, 
  // asked only for fees above the ceiling, or in another token
  : async () =>
    .(() => . <= ('1', 18)) 
})
 
try {
  await .({ : 'did:ope:1234abcd...' })
} catch () {
  if ( instanceof )
    .('the node asked for', .) // nothing was spent
  else throw 
}

The approvals nautilus sends are exact: the provider fee is approved to the datatoken for the fee amount (together with the purchase and the publish-market fee where they share a token and spender), and the escrow contract only for what is deposited.

Coming from v1

v1v2
getAccessDetails()getPricingInfo()
AccessDetailsPricingInfo
OrderPriceAndFeesOrderPrice
AssetWithAccessDetails— read pricing separately from the asset
subgraph queriescontract reads

Try it

npm start -- access:price did:ope:…      # what an asset costs right now
npm start -- edit:price did:ope:… 2.95   # reprice a fixed-rate exchange

config.ts in examples/ holds ready-made pricing configs per network, with the payment-token addresses already filled in.