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
| Field | Meaning |
|---|---|
schema | 'fixed', 'free' or 'none' |
templateId | Datatoken template: 1 basic, 2 enterprise, 4 confidential EVM |
datatokenAddress | The token being priced |
exchangeId | The fixed-rate exchange id, when schema is 'fixed' |
baseTokenAddress | What the exchange charges in |
baseTokenDecimals | Decimals of that token |
publishMarketFee | The publisher's market fee |
OrderPrice
Every field is a decimal string.
| Field | Meaning |
|---|---|
total | The base-token spend — what the exchange takes |
baseTokenAmount | The same amount, named for what it is |
opcFee | Ocean Protocol Community fee |
publishMarketFee | The publishing market's cut, in its own token |
consumeMarketFee | What your market collects, if you charge anything |
consumeMarket | The { 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-node | Access initialize | initializeCompute |
|---|---|---|
| 3.0 – 4.2 | { providerFee, datatoken, nonce, computeAddress }, with no validOrder | validOrder 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
OrderStartedof 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 theOrderStartedblock (a reuse does not restart it).0never 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:
validUntilis0, or the time since the transaction's block is at mostvalidUntil. The node signsvalidUntilas 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(orinitializeCompute) 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'sserviceEndpoint, 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:
initializeComputenames 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
| v1 | v2 |
|---|---|
getAccessDetails() | getPricingInfo() |
AccessDetails | PricingInfo |
OrderPriceAndFees | OrderPrice |
AssetWithAccessDetails | — read pricing separately from the asset |
| subgraph queries | contract 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 exchangeconfig.ts in examples/ holds ready-made pricing configs per network,
with the payment-token addresses already filled in.