useIncentiveOffer
A price for the exact Fast Update incentive you are about to offer, computed from the contract's own rate — a quote and nothing more, because a read hook that could spend is a hook a component could spend from by rendering.
import { useIncentiveOffer } from '@flarekit-dev/react'
useIncentiveOffer quotes one offer to the FastUpdateIncentiveManager: what a
given widening of the update range would cost, alongside the range and sample
size the contract reports right now.
The price is a function of the offer. Bisected live on Coston2, a
rangeIncrease of getRange() / 100 is rejected at 366210937499999998 wei
and accepted at 366210937499999999 — about 0.3662 C2FLR, the same minimum on
mainnet. So changing the increase changes the price, and this re-reads rather
than scaling a cached number.
This hook only quotes. Submitting is a signed, value-moving action and belongs to the caller's own wallet flow.
Live#
The readout is the hook's actual return value, running the real quoteIncentive
over the six incentive-manager values recorded on Coston2 on 2026-08-05.
createMockFtsoReader answers no incentive-manager call — it mocks what was
observed on the feed contracts and refuses everything else — so the demo replays
that recording rather than inventing one.
The number to look at is offerAmountWei. It is computed here from
rangeIncreasePrice and getPrecision, not carried in the recording, and it
lands on 366210937499999999 — the amount the live contract accepted in a dry
run and rejected one wei below.
// reads on mountRead from the running hook against the mock kit, on this render.
import type { RoundReader } from '@flarekit-dev/core'
import { useIncentiveOffer } from '@flarekit-dev/react'
import { IncentiveComposer } from '@flarekit-dev/react-ui'
export function Incentive({ reader, payer, onOffer }) {
// The quote is for this exact widening. Changing it changes the price, so a
// different offer is a different read rather than a scaled cached number.
const { data, loading, error, refresh } = useIncentiveOffer({
reader,
chainId: 114,
rangeIncrease: 2433889152438200450873670154321n,
})
if (error) return <p>The incentive manager could not be quoted: {error}</p>
return (
<IncentiveComposer
quote={data}
loading={loading}
payer={payer}
nativeAsset="C2FLR"
now={Date.now()}
onRequote={refresh}
onSubmit={onOffer}
/>
)
}Usage#
import { useIncentiveOffer } from '@flarekit-dev/react'
import { IncentiveComposer } from '@flarekit-dev/react-ui'
export function Incentive({ reader, payer, onOffer }) {
const { data, loading, error, refresh } = useIncentiveOffer({
reader,
chainId: 114,
rangeIncrease: 2433889152438200450873670154321n,
})
if (error) return <p>The incentive manager could not be quoted: {error}</p>
return (
<IncentiveComposer
quote={data}
loading={loading}
payer={payer}
nativeAsset="C2FLR"
now={Date.now()}
onRequote={refresh}
onSubmit={onOffer}
/>
)
}Before anything is signed, dry-run the offer with verifyOfferAmount from
@flarekit-dev/core. The price formula is fitted to four live measurements rather
than derived from Solidity this project does not vendor, which makes it exactly
the kind of number that stays right until a contract upgrade makes it quietly
wrong. Simulating the real call turns that risk into a caught refusal: the node
executes offerIncentive with the quoted value and no signature, spending
nothing. A failure there is a refusal to offer — nothing was submitted, so
nothing failed.
Parameters#
| Prop | Type | Default | Description |
|---|---|---|---|
| readerrequired | RoundReader | — | Reads the FastUpdateIncentiveManager: getRange, getExpectedSampleSize, getIncentiveDuration, rangeIncreasePrice, rangeIncreaseLimit and getPrecision, in one batch. |
| chainIdrequired | number | — | Which deployment. The manager address comes from @flarekit-dev/contracts. |
| rangeIncreaserequired | bigint | — | The exact widening being priced. The quote is meaningless without it, and a different increase is a different read. |
| rangeLimit | bigint | — | Defaults to the contract’s own rangeIncreaseLimit. It travels into the offer, so it is quoted rather than assumed. |
Return type#
ObservedRead<IncentiveQuote> — the
useObservedRead shape over a plain quote rather
than an Observation, because a quote is arithmetic over six contract reads
rather than a single claim about a value.
| Prop | Type | Default | Description |
|---|---|---|---|
| data | IncentiveQuote | undefined | — | undefined until the quote lands. Never a partial quote: all six reads resolve together or the read fails. |
| loading | boolean | — | True only while no quote has ever arrived. |
| error | string | undefined | — | The manager could not be quoted. Never a fabricated price, and it does not clear a quote already held. |
| refresh | () => void | — | Re-quote. The range moves under a quote, which is what makes a quote go stale rather than wrong. |
IncentiveQuote carries rangeIncrease and rangeLimit — the exact offer this
price is for — offerAmountWei, what must be sent as value for this offer and
no other, plus currentRange, expectedSampleSize, durationSeconds and the
fixed-point precision the price divides by, so a surface can show its own
arithmetic instead of asking to be trusted.
States#
- loading — no quote yet.
- quoted — a price for this exact offer, valid until the range moves.
- stale — nothing here marks it; a quote ages against the chain, and
refreshis how a surface re-asks before someone signs. - error — the manager could not be read, or
precisioncame back zero, in which case the offer cannot be priced and nothing is sent. - above the limit —
assertOfferWithinLimitrefuses an increase larger than the contract's ownrangeIncreaseLimit, before a wallet asks anyone to sign something that would revert.
Mock to live#
import { chainFor } from '@flarekit-dev/contracts'
import { createPublicClient, http } from 'viem'
const reader = createPublicClient({ transport: http(chainFor(114).rpcUrl) })There is no FTSO mock for the incentive manager, and that is deliberate: the mock answers what was measured on the feed contracts and refuses the rest, so an unobserved call is an error rather than a plausible number.
What it will not do#
It will not submit anything, hold a key, or take a signer. Quoting is a read; offering moves value and stays in the caller's wallet flow.
It will not price against getRange() — the obvious reading, which overstates
the amount by about 5,500x and would ask somebody on mainnet to sign away
roughly 2000 FLR for a 0.37 FLR purchase. It will not scale a cached price to a
different offer, and it will not report a widening as bought in both dimensions:
sampleSizeIncrease was 0 in the real mainnet offer this was measured
against.
Confirming what an offer did is a separate read, and it is pinned to the offer's
own block, because the widening decays — a later read showing no effect is a
decayed incentive, never a failed one. When that read reports confirmed: false
it means the measured change could not be attributed to this offer, which is an
unknown rather than a failure.