Price Oracle

A public, signed AVM price feed for tokenized real property. Free for any consumer to read; subsidized by the property side.

Fabrica Price Oracle

The Fabrica Price Oracle publishes EIP-712 signed price quotes for individual Fabrica property tokens (ERC-1155 NFTs that legally represent real property held in the Fabrica trust). Each quote is bound to a specific token contract, token ID, currency token, and consuming pool address, and carries an explicit validity window.

The oracle is built to be consumed by:

  • MetaStreet v2 pool contracts (and any contract conforming to the IPriceOracle interface) to determine collateral value when originating loans against Fabrica tokens
  • Other DeFi lending protocols that want a verifiable reference value for loans against tokenized real property
  • Aggregators, vault curators, and analytics tools that need a verifiable on-chain or off-chain reference price for tokenized parcels
  • Third-party valuation services that want to compare or benchmark against the Fabrica AVM

The oracle is paid for by token holders (annual subscription, billed on the property side) and is therefore free to consumers. It follows the same posture as a published Zestimate-style reference value, not investment advice. The architecture is intentionally non-exclusive: any party can run a competing oracle that signs quotes for the same tokens, and any pool deployer can wire any oracle they want into their pool's oracleContext.

API endpoint

GET https://api.fabrica.land/meta-street/{networkName}/{poolAddress}/{contractAddress}/{tokenId}/signed-valuation

Path parameters

ParameterTypeDescription
networkNameenumOne of ethereum, sepolia, base-sepolia
poolAddressaddressAddress of the MetaStreet pool the quote is for. Bound into the EIP-712 domain. Pool must be registered in Fabrica's oracle map.
contractAddressaddressFabrica ERC-1155 token contract address
tokenIduint256Fabrica token ID to value

Query parameters

ParameterTypeRequiredDescription
borroweraddressNoBorrower wallet address. If supplied, the oracle applies a credit-default penalty to the price based on the borrower's historical default count across Fabrica pools. Quotes for the same token can therefore differ by borrower; quotes without borrower represent the unscaled token price.

Authentication and rate limits

The endpoint is public and unauthenticated. There is no API key requirement. Reasonable rate limiting is applied at the edge; high-volume integrators (block explorers, vaults, mirrored oracles) should reach out to Fabrica for an integration partnership.

Example

curl 'https://api.fabrica.land/meta-street/ethereum/0x842Ffbf1AD5314503904626122376f71603A3Cf9/0x5cbeb7a0df7ed85d82a472fd56d81ed550f3ea95/12345678/signed-valuation'

Response format

The response is a JSON object with a quote (the unsigned price quote) and a signature (an EIP-712 ECDSA signature over the quote, produced by Fabrica's oracle signing key).

{
  "quote": {
    "token": "0x5cbeb7a0df7ed85d82a472fd56d81ed550f3ea95",
    "tokenId": "12345678",
    "currency": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "price": "75000000000",
    "timestamp": 1745520000,
    "duration": 86400
  },
  "signature": "0x1c2b...90ab"
}

Quote fields

FieldTypeDescription
tokenaddress (string)Fabrica ERC-1155 token contract address
tokenIduint256 (string)Fabrica token ID
currencyaddress (string)Currency the price is denominated in (USDC for the canonical Fabrica oracle: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 on mainnet)
priceuint256 (string)Price per unit of the token's total supply, scaled to USDC's 6 decimals. See "Price scaling" below.
timestampuint64 (number)Unix timestamp (seconds) at which the quote starts being valid. The publisher backs this off by 10 minutes from real wall-clock time to absorb chain-time skew on consumers.
durationuint64 (number)Validity window in seconds. The quote is valid from timestamp to timestamp + duration. After expiry, on-chain verification reverts with InvalidTimestamp.

Signature

FieldTypeDescription
signaturebytes (hex string), optional65-byte ECDSA signature over the EIP-712 hash of the quote. Omitted if the token has not yet been minted. Always present for valued tokens.

Price scaling

The price field is the unit price per token share, not the price of the whole property. Fabrica tokens are ERC-1155 with arbitrary totalSupply; the property's full appraised value is divided by totalSupply to produce a unit price, then scaled to the currency's decimals (6 for USDC). Consumers that want the property-level value should multiply price by the token's totalSupply().

This matches MetaStreet's SimpleSignedPriceOracle.price() semantics: the on-chain function aggregates quotes across multiple tokenIds and returns an average per-unit price, weighted by tokenIdQuantities.

Zero-price quotes

If the oracle's eligibility rule fails (proof of title invalid, fees not in good standing, token not validated by Fabrica, etc.) the returned price is "0". A zero price is still a valid signed quote; on-chain verification rejects it with InvalidQuote because the contract requires quote.price != 0. A zero quote in the response is an honest signal that Fabrica is not currently publishing a price for the token; integrators should surface this as "not currently priced" rather than as an error.

Unsigned quotes

If the token has been configured but not yet minted, the response returns the quote with no signature field. These quotes cannot be used on-chain and exist only as a forecast for UI surfaces.


EIP-712 signing scheme

The oracle signs each quote as an EIP-712 typed structure using the following type hash:

bytes32 constant QUOTE_TYPEHASH = keccak256(
    "Quote(address token,uint256 tokenId,address currency,uint256 price,uint64 timestamp,uint64 duration)"
);

Domain separator

The EIP-712 domain is configured per pool and pinned to the on-chain SimpleSignedPriceOracle contract that the consuming pool uses:

Domain fieldSource
nameoracle.domain from Fabrica's per-pool config
versionoracle.verifyingContractVersion (must match the on-chain IMPLEMENTATION_VERSION() returned by the deployed SimpleSignedPriceOracle)
chainIdThe chain ID of the network in the request path
verifyingContractThe deployed SimpleSignedPriceOracle contract address for that pool

Canonical Fabrica deployments

NetworkPool addressSimpleSignedPriceOracle (verifyingContract)Domain nameversionTTL (seconds)
Ethereum mainnet0x842Ffbf1AD5314503904626122376f71603A3Cf90xA23887308d75154C50Cbb7F8bbD5DC9EA40c38AcAll Fabrica Properties1.2600
Sepolia (default pool)0x00E5cb8833fdaCFc6a97faA0B54384adE19bE61A0x2E5113D939ff6c2F5a3FEe3AB961e112AB6B9B37Fabrica v3 Property Prices1.2600
Sepolia (CA-only test pool)0x62c2940Cc4aB105cfdA49Bdabc3b7A9741Ba9c9e0xE9A6120a032F411DA749D95e51D07BC4D039Ee6A(empty string)1.2600

Multiple pools per network are supported. Each pool can pin its own SimpleSignedPriceOracle contract and its own eligibility rule (the Sepolia CA-only pool, for example, additionally restricts to California-state tokens). The publisher signs with the domain values that match the requested pool.

To recover the signer or verify a quote off-chain, integrators must use the same domain values that the consuming pool's oracle uses. These are addressable on-chain via the oracle contract's eip712Domain() method (OpenZeppelin's standard EIP-5267 accessor). Read it once and cache it rather than hardcoding from this table, since deployments can be updated.

Signer address

The Fabrica oracle signs with a key whose public address is registered in the consuming SimpleSignedPriceOracle for that collateral token. Query the active signer via priceOracleSigner(collateralToken) on the oracle contract. Do not hardcode the signer address from documentation, as keys can be rotated by the oracle's owner.


Verifying a quote off-chain

TypeScript (viem)

import { recoverTypedDataAddress } from 'viem'

const valuation = await fetch(
  `https://api.fabrica.land/meta-street/ethereum/${poolAddress}/${tokenContract}/${tokenId}/signed-valuation`
).then((r) => r.json())

const recoveredSigner = await recoverTypedDataAddress({
  domain: {
    name: 'All Fabrica Properties', // read from oracle's eip712Domain() in production
    version: '1.2',
    chainId: 1,
    verifyingContract: '0xA23887308d75154C50Cbb7F8bbD5DC9EA40c38Ac', // Ethereum mainnet
  },
  types: {
    Quote: [
      { name: 'token', type: 'address' },
      { name: 'tokenId', type: 'uint256' },
      { name: 'currency', type: 'address' },
      { name: 'price', type: 'uint256' },
      { name: 'timestamp', type: 'uint64' },
      { name: 'duration', type: 'uint64' },
    ],
  },
  primaryType: 'Quote',
  message: {
    token: valuation.quote.token,
    tokenId: BigInt(valuation.quote.tokenId),
    currency: valuation.quote.currency,
    price: BigInt(valuation.quote.price),
    timestamp: BigInt(valuation.quote.timestamp),
    duration: BigInt(valuation.quote.duration),
  },
  signature: valuation.signature,
})

// Compare against the registered signer
const expectedSigner = await publicClient.readContract({
  address: priceOracleAddress,
  abi: priceOracleAbi,
  functionName: 'priceOracleSigner',
  args: [tokenContract],
})

if (recoveredSigner.toLowerCase() !== expectedSigner.toLowerCase()) {
  throw new Error('Invalid signer')
}

Solidity

The on-chain verification path is implemented in SimpleSignedPriceOracle._verifyQuote. To verify in your own contract, hash the typed data per EIP-712 and recover with ECDSA.recover:

import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";

bytes32 constant QUOTE_TYPEHASH = keccak256(
    "Quote(address token,uint256 tokenId,address currency,uint256 price,uint64 timestamp,uint64 duration)"
);

function verifyQuote(
    address token,
    uint256 tokenId,
    address currency,
    uint256 price,
    uint64 timestamp,
    uint64 duration,
    bytes calldata signature,
    address expectedSigner
) external view {
    bytes32 structHash = keccak256(abi.encode(
        QUOTE_TYPEHASH,
        token,
        tokenId,
        currency,
        price,
        timestamp,
        duration
    ));

    address recovered = ECDSA.recover(_hashTypedDataV4(structHash), signature);
    require(recovered == expectedSigner, "Invalid signer");
    require(timestamp <= block.timestamp, "Quote not yet valid");
    require(timestamp + duration >= block.timestamp, "Quote expired");
    require(price > 0, "Zero price");
}

On-chain integration via SimpleSignedPriceOracle

The reference on-chain consumer is MetaStreet's SimpleSignedPriceOracle (fabrica-contracts/src/meta-street/SimpleSignedPriceOracle.sol). It conforms to the generic IPriceOracle interface, which any pool can implement against:

interface IPriceOracle {
    function price(
        address collateralToken,
        address currencyToken,
        uint256[] memory tokenIds,
        uint256[] memory tokenIdQuantities,
        bytes calldata oracleContext
    ) external view returns (uint256 price);
}

The pool calls priceOracle.price(...) during loan origination, passing an oracleContext that the consumer pool obtained from the borrower (typically supplied alongside borrow(...) calldata).

Encoding oracleContext

For SimpleSignedPriceOracle, oracleContext is the ABI-encoded array of SignedQuote structs, one per tokenId in the same order as tokenIds:

struct Quote {
    address token;
    uint256 tokenId;
    address currency;
    uint256 price;
    uint64 timestamp;
    uint64 duration;
}

struct SignedQuote {
    Quote quote;
    bytes signature;
}

// oracleContext = abi.encode(SignedQuote[])

In TypeScript, with the @metastreet/sdk-v2 QuoteHelper:

import { QuoteHelper } from '@metastreet/sdk-v2'
import { encodePacked, fromHex } from 'viem'

const signedQuote = {
  quote: {
    token: valuation.quote.token,
    tokenId: BigInt(valuation.quote.tokenId),
    currency: valuation.quote.currency,
    price: BigInt(valuation.quote.price),
    timestamp: BigInt(valuation.quote.timestamp),
    duration: BigInt(valuation.quote.duration),
  },
  signature: valuation.signature,
}

const encodedQuote = QuoteHelper.encodeQuotes([signedQuote])

// Wrap in MetaStreet's borrow-options envelope (tag 5 = oracle context)
const numBytes = fromHex(encodedQuote, 'bytes').length
const options = encodePacked(['uint16', 'uint16', 'bytes'], [5, numBytes, encodedQuote])

The options bytes are then passed as the final argument to the pool's borrow(...) function.

Aggregation behavior

SimpleSignedPriceOracle.price(...) aggregates across multiple tokenIds:

totalOraclePrice = sum_i (signedQuotes[i].quote.price * collateralTokenQuantities[i])
count = sum_i (collateralTokenQuantities[i])
return totalOraclePrice / count

This is the average per-unit price across all collateral pieces, weighted by quantity. For a single ERC-1155 with quantity = 1 it is just the per-unit price from the quote.


How the oracle prices a token (architecture)

The price returned by the Fabrica AVM is computed from a stack of independent inputs and safety filters before signing. Integrators do not need to understand the internals to consume the oracle; the architecture is summarized here so consumers can reason about failure modes.

Inputs

InputSourceRole
Algorithmic land valuationThird-party AVM signalPrimary market-value estimate for raw land
Confidence score on the algorithmic estimateThird-party AVM signalMultiplier in [0, 1] applied to the estimate
Fabrica AVM estimated valueFabrica's in-house automated valuation modelIndependent valuation signal for cross-validation, trained on closed deed transactions with geospatial enrichment (slope, flood zones, road access, zoning, comparable sales)
Fabrica AVM confidence scoreFabrica's in-house modelMultiplier in [0, 1] applied to the Fabrica AVM estimate
County assessor parcel valuePublic county tax assessor recordsFloor-of-last-resort when no algorithmic valuation is available
Token validation statusFabrica validator contractToken must be validated by a Fabrica validator
Provenance check resultsFabrica scoring serviceProof of title, fees in good standing, and other token-status signals
Borrower credit historyFabrica activity ledgerDefault count across Fabrica pools, applied only when the borrower query param is supplied

Eligibility gate

A per-pool rule evaluated server-side determines whether a token is currently eligible to receive a non-zero signed price. The default Fabrica rule requires that the token be validated by a Fabrica validator and (for non-premint tokens) that proof-of-title checks pass and that the property is in good standing on its annual oracle subscription. If the rule evaluates falsy, the signed price is 0. The rule is configurable per pool, so pool deployers can tighten or loosen the gate without changing the oracle code.

Safe-appraisal logic (MIN across AVM sources)

The oracle takes the minimum across independent AVM signals (each weighted by its confidence) before signing. Consumers see only the resulting MIN value, signed once. Independent AVMs that converge are more trustworthy; large divergence is a signal of data quality issues, and the conservative side protects pool depositors from oracle drift. Significant divergence is logged for audit.

This MIN-of-N safety logic is the architecturally preferred mode. The roadmap extends it from server-side aggregation across two sources to on-chain aggregation across N independent publishers; see "Roadmap" below.

Adjustments applied before signing

The signed price reflects several conservative haircuts applied in this order:

  1. Confidence-weighted estimate per source: estimate * confidence
  2. MIN across AVM sources (when both signals are available)
  3. Per-unit normalization: divide by totalSupply so the quote is a unit price
  4. Low-value adjustment: properties below $25,000 receive an additional quadratic discount; high-value properties pass through unchanged. This is a heuristic for the historically lower liquidity of low-value parcels.
  5. Liquidation discount: properties with at least one prior on-chain liquidation receive a 0.25× multiplier (75% haircut) to reflect realized auction outcomes.
  6. Credit default penalty (only when the borrower query param is supplied): 1 default → 0.5× multiplier; 2+ defaults → 0.05× multiplier; 0 defaults → no change.

These adjustments are part of the published oracle's safety design.


Coverage and limitations

Coverage

DimensionCurrent
Supported chainsEthereum mainnet, Sepolia, Base Sepolia
Token contractsThe Fabrica ERC-1155 token deployed on each network (mainnet: 0x5cbeb7a0df7ed85d82a472fd56d81ed550f3ea95)
Token universeAll Fabrica-validated tokens with at least minted supply or valid premint configuration
Property typesCurrently raw land. Tokens with detected residential structures are flagged but not excluded by the default rule.
JurisdictionsAll 50 US states. International parcels are out of scope.
CurrencyUSDC only

Refresh cadence and TTL

The publisher generates a fresh signed quote on every API call. Underlying valuation inputs refresh on independent cadences (third-party AVM signals refresh on a nightly schedule; Fabrica's in-house AVM recomputes on token mint, on validation events, and on a periodic batch schedule; county assessor data refreshes on token mint and on parcel-data refresh events).

The signed quote's duration field defines the on-chain validity window. The default is set per pool (typically a few minutes to an hour) plus 10 minutes of slack. Integrators should fetch a fresh quote immediately before any on-chain action that consumes it.

Data licensing

The oracle's price is derived from a mix of public and licensed third-party data sources. Consumers receive only the derived signed price; redistribution of underlying raw inputs is not granted by consuming this endpoint. Fabrica's own AVM glue code is targeted for open-source release, allowing third parties to build a comparable model from open inputs.

Fabrica makes no warranty of accuracy on derived values. The oracle is a published reference price, not investment advice, not a binding appraisal, not a fairness opinion.

What can go wrong

  • Stale quotes: a quote outside its validity window will revert on-chain (InvalidTimestamp). Refetch.
  • Wrong domain: if the consuming pool's oracle uses a different EIP-712 domain than the publisher signed against, signature recovery returns the wrong address and on-chain verification reverts (InvalidSigner). Make sure your pool's priceOracle registration matches the network's canonical Fabrica oracle deployment.
  • Zero price: rule-failure tokens return a signed price = 0 quote. On-chain consumers reject with InvalidQuote. Off-chain consumers should treat zero as "not currently priced."
  • Unminted tokens: the response omits signature. The quote is a forecast and cannot be used on-chain.

Pricing for token holders

The Fabrica AVM oracle is paid for on the property side, not the consumer side. Annual oracle subscription is billed to the token holder via Fabrica's existing property-side billing rail.

PlanPricingWhat it includes
Standard0.25% to 1% of property value per year, paid in stablecoinContinuous AVM publication, signed quote endpoint, provenance score endpoint, metadata refresh on demand

This mirrors the economic model of property tax assessors, title insurance, and Zillow-for-agents: the asset side pays for the data service, and consumers (lenders, buyers, analytics tools, pool deployers) consume it free of charge. As long as a token's annual subscription is current, the oracle publishes; otherwise the rule fails and the signed price is zero.

Final pricing and billing UX are subject to change before formal launch.


Provenance score

Alongside the price oracle, Fabrica is rolling out a provenance score that publishes how tightly a token is connected to its underlying real-world title. The provenance score is a separate signal from the price: a token can be highly valued and still have weak provenance, or vice versa.

The provenance score is currently embedded in the price oracle's eligibility rule. A standalone provenance score endpoint with its own EIP-712 signed payload is on the roadmap; this section will be updated when it ships.

Until then, integrators that need provenance signals beyond binary eligibility can read the per-check booleans via the Fabrica scoring API.


Roadmap

The current oracle is a single-publisher single-signer architecture. The strategic goal is progressive decentralization of this surface:

  1. Server-side MIN across two AVM signals (live): The publisher's safe-appraisal logic takes the minimum across two independent AVM signals (each weighted by confidence) before signing. The signed quote already reflects two-source agreement.
  2. Multi-publisher quote aggregation (planned): The on-chain SimpleSignedPriceOracle already supports rotating the signer per collateral token via setSigner(...). The architectural goal is a variant oracle contract that requires N signatures from N independent publishers and takes MIN (or median, configurable) on-chain. Any pool deployer can then consume any combination of publishers without trusting a single party.
  3. Open-source AVM recipe (planned): The Fabrica AVM glue code is targeted for open-source release. Once published, any party can stand up a competing oracle that signs comparable prices for the same tokens, and pool deployers can wire whichever oracle they prefer.
  4. Provenance score as a standalone oracle (planned): Lift the per-check provenance signals into a separately signed payload with its own endpoint and EIP-712 typehash, so consumers can read price and provenance independently.

These are forward-looking commitments. Timelines depend on the broader product roadmap and are not contractual.


License and terms

The published oracle data is provided under the following terms:

  • No warranty. The oracle's price is a derived estimate from third-party data sources. Fabrica makes no representation as to accuracy, completeness, or fitness for any particular purpose.
  • Not financial advice, not an appraisal. Consumers must conduct their own due diligence. The oracle is a reference price, not a binding valuation, not a fairness opinion, not regulatory or legal advice.
  • Underlying data is a mix of public and licensed third-party sources. Consumers may use the signed price quote in their own protocols and applications. Redistribution of underlying raw inputs is not granted by consuming this endpoint.
  • No guarantee of continuous availability. Fabrica intends to operate this oracle as durable public infrastructure but reserves the right to rotate signers, deprecate endpoints with reasonable notice, or pause publication for specific tokens (e.g., on revocation of validation status).
  • Sanctions screening. The oracle endpoint applies sanctions screening at the request layer where required by applicable law. Quotes are not signed for tokens or borrower addresses on sanctions lists.

For commercial integration partnerships, custom pricing, or higher rate limits, contact [email protected].


Reference

Source files

Related documentation

Contact