---
title: "Swaps & FX"
description: "The rule for this family: A rate is a perishable fact. It never appears without a timestamp, and it is never transacted on without a lock. Specs for 9 React components: SwapWidget, RateLine, RateLock, ConversionBreakdown and 5 more."
canonical: "https://lopifi.com/docs/catalog/swaps-fx"
---

# Swaps & FX

> The rule for this family: A rate is a perishable fact. It never appears without a timestamp, and it is never transacted on without a lock. Specs for 9 React components: SwapWidget, RateLine, RateLock, ConversionBreakdown and 5 more.

Source: https://lopifi.com/docs/catalog/swaps-fx
Part of: Lopifi — Planned: US GAAP books on autopilot, checked by a CPA (https://lopifi.com)

---

# Swaps & FX

The rule for this family:

> **A rate is a perishable fact.** It never appears without a timestamp, and it is never transacted on without a lock.

---

## 38. `SwapWidget`

> The signature component: from and to, with the flip.

```
┌─ 24 ───────────────────────────────────┐
│  You send                              │  label
│  [1,000.00]              [USD ↓]       │  figure-lg · currency select
│  Balance $4,120.00              [Max]  │  caption
│                                        │
│         [ ↑↓ ]  flip button            │  centered, overlapping the divider
│                                        │
│  Recipient gets                        │
│  [918.42]                [EUR ↓]       │
│                                        │
│  1 USD = 0.91842 EUR · 12s  [locked]   │  RateLine
│  ├─ ConversionBreakdown ─────────────┤ │
└────────────────────────────────────────┘
```

### The flip
The one place `spring.playful` is used.

```
icon      rotateY? no — rotate(180deg), spring.playful { duration: 0.45, bounce: 0.22 }
fields    cross-translate: top ↕ bottom, transform only, same spring
values    swap INSTANTLY. The amounts do not interpolate. (Money Law 1)
```

The fields move; the numbers don't animate. Motion explains *what happened structurally* and never renders a false value.

Under reduced motion: instant swap, no rotation.

### Rules
- **Editing either field recomputes the other**, with a 140ms blur-masked crossfade on the computed value, never a count-up.
- **The computed field looks derived**: `--text-secondary` until it's focused.
- Debounce rate fetches at 300ms; **never show a stale computed value as current.** During refetch, dim the computed value to 0.6.
- Insufficient balance is an inline error under the send field, not a disabled button.
- Fee and rate are always visible without expanding anything.
- **A pair with no quote has no rate.** Never a 1:1 fallback: the receive side stays empty, the breakdown is withheld, and the line says "No quote for USD to JPY."
- **The fee is in the send currency.** `fee` takes one `MoneyValue` per currency the user can send from; after a flip, a send currency with no fee listed is unquoted rather than charged another currency's number. Omitted, no fee is charged.
- **The balance check runs only when the send side is in the balance's currency.** After a flip into another currency the balance and Max stand down. `defaultFrom`, `defaultTo` and `defaultAmount` set the opening state.

---

## 39. `RateLine`

```tsx
<RateLine from="USD" to="EUR" rate={0.91842} asOf={t} />
// 1 USD = 0.91842 EUR · 12s ago
```

5 significant figures, `tnum`. **The timestamp is not optional.** The component has no variant without it.

Live variant flashes on update ([value flash](https://lopifi.com/md/docs/motion/catalog#value-flash)), throttled to one flash per 400ms.

## 40. `RateLock`
A locked rate with a countdown. The countdown is a `linear` **rule-line progress bar**, matching the statement grammar, not a rounded progress pill.

On expiry: the rate refreshes, the new rate crossfades in, and the confirm action is **disabled until the user acknowledges the new rate**. Executing at a rate the user didn't see is the worst failure this family can have.

Warning state at 20% remaining: amber plus a text countdown, never color alone.

## 41. `ConversionBreakdown`

> Wise's honesty pattern as a component, with every line visible.

```
Amount to convert                    1,000.00 USD
Our fee                                 −4.50 USD   ← itemized, named
Amount we'll convert                   995.50 USD
                                      ──────────    ← single rule
Rate  1 USD = 0.91842 EUR  · 09:12 UTC
Recipient gets                         914.29 EUR
                                      ══════════    ← double rule
```

- **Every fee named and itemized.** Never a lump "Fees".
- The rate line carries its timestamp.
- Uses `Rule`, the same grammar as the statements, so the library reads as one system.
- Percentage-of-total splits use largest-remainder allocation, so the parts sum to the total exactly.

## 42. `CurrencyPairChart`
Rate history with a period selector. The y-axis **does not start at zero**. It's a rate, not a quantity, and forcing zero destroys the resolution. This is the one sanctioned exception to the zero-baseline rule, so the axis is labeled clearly.

Draws once on mount. Instant on period change.

## 43. `RateTicker`
Live multi-pair ticker. The value flash is the point. Pauses off-screen and on tab hide. **Never a marquee**: a scrolling ticker is continuous large-surface motion that can't be read or paused.

## 44. `SlippageControl`
Tolerance setting that **always states the worst case in absolute money**: "You'll receive at least 910.15 EUR". A percentage alone isn't enough to decide on.

## 45. `SwapRoute`
Multi-hop path (USD → USDC → EUR). Rendered as a horizontal flow, each hop with its own rate and fee. A rare legitimate use of the [isometric flow-diagram treatment](https://lopifi.com/md/docs/foundations/imagery-and-isometric), where motion explains the mechanism.

## 46. `FxExposure`
Position by currency with unrealized gain/loss. Uses `polarity="standard"` per position, and keeps realized and unrealized separate. They're different facts, and mixing them misstates the position.
