Index Circle StableFX quotes and settlements on Arc with Turbo, and publish FX rates and exposure controls with Compose.
The first wave of stablecoins was dollar-only. The next is multi-currency. Circle’s Arc ships with Circle StableFX, an institutional-grade on-chain FX engine - RFQ price discovery and 24/7 payment-versus-payment settlement across USDC, EURC, and regional partner stablecoins.On-chain FX gives a treasury something it has never had: every quote and every fill, for every currency pair, as a queryable event stream. This guide turns that stream into FX analytics, a published rate oracle, and exposure controls.
StableFX settlements are onchain events. Decode them from Arc’s raw_logs with the StableFX ABI, extracting the currency pair, the amounts on each leg, and the counterparties.
stablefx-settlements.yaml
name: stablefx-settlementsresource_size: ssources: arc_logs: type: dataset dataset_name: arc_testnet.raw_logs version: 1.0.0 start_at: latest filter: address = lower('0xSTABLEFX_SETTLEMENT')transforms: decoded: type: sql primary_key: id sql: | SELECT id, _gs_log_decode( _gs_fetch_abi('https://raw.githubusercontent.com/your-org/abis/main/stablefx.json', 'raw'), topics, data ) AS decoded, block_timestamp, transaction_hash, _gs_op FROM arc_logs fx_settlements: type: sql primary_key: id sql: | SELECT id, decoded.event_params[1] AS base_currency, -- e.g. USDC decoded.event_params[2] AS quote_currency, -- e.g. EURC CAST(decoded.event_params[3] AS DECIMAL(38, 0)) AS base_amount, CAST(decoded.event_params[4] AS DECIMAL(38, 0)) AS quote_amount, decoded.event_params[5] AS maker, decoded.event_params[6] AS taker, to_timestamp(block_timestamp) AS settled_at, transaction_hash, _gs_op FROM decoded WHERE decoded IS NOT NULL AND decoded.event_signature = 'Settlement'sinks: fx_warehouse: type: clickhouse from: fx_settlements table: fx_settlements secret_name: MY_CLICKHOUSE primary_key: id
StableFX is new, and its published interface is the source of truth for event names and field order. Fetch the ABI with _gs_fetch_abi and confirm the Settlement event signature and event_params positions against Circle’s StableFX contracts before relying on the extraction above.
With settlements in your warehouse, the desk’s core metrics are SQL:
-- Volume-weighted rate and spread per pair, last hourSELECT base_currency, quote_currency, sum(quote_amount) / sum(base_amount) AS vwap_rate, count() AS fills, sum(base_amount) AS base_volumeFROM fx_settlementsWHERE settled_at > now() - INTERVAL 1 HOURGROUP BY base_currency, quote_currency;
From the same table you get corridor flows (net USD→EUR movement), realized spread versus a reference rate, and maker/taker league tables. Maintain a live last-rate per pair with the PostgreSQL aggregate sink.
Other contracts and apps need the rate you just computed. A scheduled Compose task reads the VWAP and publishes it on-chain - the same mechanism as the NAV oracle, pointed at an FX pair.
src/tasks/publish-fx-rate.ts
import type { TaskContext, Chain } from "compose";// Publish to the same network your StableFX pipeline reads from (Arc). Arc// isn't a built-in Edge RPC chain yet, so define it as a custom chain; swap for// evm.chains.<name> once it's available on Edge. Note Arc pays gas in USDC.const ARC_CHAIN: Chain = { id: 0, // Arc testnet chain ID name: "Arc Testnet", testnet: true, nativeCurrency: { name: "USD Coin", symbol: "USDC", decimals: 6 }, rpcUrls: { default: { http: ["https://<arc-testnet-rpc>"] }, public: { http: ["https://<arc-testnet-rpc>"] }, }, blockExplorers: { default: { name: "Explorer", url: "https://<explorer>" } },};export async function main(context: TaskContext) { const { fetch, evm } = context; const rate = await fetch<{ pair: string; vwap: number; asOf: string }>( "https://fx.example.com/vwap?pair=USDC-EURC", ); const wallet = await evm.wallet({ name: "fx-oracle", sponsorGas: true }); await wallet.writeContract( ARC_CHAIN, "0xFX_ORACLE", "updateRate(bytes32,uint256,uint64)", [ "0x555344432d45555243", // "USDC-EURC" BigInt(Math.round(rate.vwap * 1e18)), BigInt(Math.floor(new Date(rate.asOf).getTime() / 1000)), ], ); return { pair: rate.pair, vwap: rate.vwap };}
FX desks live and die by position limits. Because you have every fill, net exposure per currency is a running sum - and a Compose task can watch it and act (alert, hedge, or halt quoting) when a limit is breached. Compose’s durable execution guarantees the control fires, and every decision is traced for the desk’s risk review - a live version of the circuit-breaker pattern.