Skip to main content
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.

What you’ll build

Step 1: Index quotes and settlements

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-settlements
resource_size: s

sources:
  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.

Step 2: Compute FX analytics

With settlements in your warehouse, the desk’s core metrics are SQL:
-- Volume-weighted rate and spread per pair, last hour
SELECT
  base_currency,
  quote_currency,
  sum(quote_amount) / sum(base_amount)              AS vwap_rate,
  count()                                            AS fills,
  sum(base_amount)                                   AS base_volume
FROM fx_settlements
WHERE settled_at > now() - INTERVAL 1 HOUR
GROUP 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.

Step 3: Publish an FX rate oracle

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 };
}

Step 4: Monitor exposure and enforce limits

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.

Business outcomes

  • Every quote and fill is data - true VWAP, realized spreads, and corridor flows, not a dealer’s end-of-day summary.
  • Multi-currency treasury - real-time positions across USD, EUR, and regional stablecoins in one warehouse.
  • A rate you can publish and defend - on-chain FX oracle sourced from actual settlements, with an audit trail.
  • Automated limit enforcement - exposure breaches trigger controls in seconds.

Resources

Can’t find what you’re looking for? Reach out to us at support@goldsky.com for help.