Architecture
One domain, three surfaces — how Transvia is wired from the web app down to the escrow contract.
The system, end to end
UML component diagram — Transvia, from humans to the escrow
[Humans] [Businesses] [Machines — SDK · MCP · x402 agents]
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌─────────────────────┐
│ [Web App] │ │ [Console] │ │ [SDK / MCP / REST] │
│ /send · etc. │ │ /console │ │ 6 MCP tools · JSON │
└──────┬───────┘ └──────┬───────┘ └──────────┬──────────┘
└─────────────────┼─────────────────────┘
▼ «use» HTTP · JSON
┌──────────────────┐
│ [Transvia API] │
└────────┬─────────┘
▼ «use» (interfaces only)
[Quoting] [Remittance] [Settlement] [Compliance Agent]
└─────────────────┼─────────────────────┘
▼ «port»
ChainAdapter ──▶ [Simulated │ Viem adapter] ──▶ Arc · USDC
FxAdapter ──▶ [StableFX RFQ adapter] (chainId 5042002)
Repositories ──▶ [Prisma repositories]
▼ (live rail)
TransviaEscrow.sol — deposit · settle · claim · refundThree audiences, one API, one domain. The web app, the business console, and the machine surfaces (SDK, MCP, x402) all speak to the same services — so an agent's transfer and your transfer follow the identical lifecycle.
The core: domain, ports, adapters
src/core (inside apps/web) is a framework-free TypeScript package.
Nothing inside it imports Next.js, Prisma, or viem by name — dependencies
point inward. The pure domain model lives one level up, in the workspace
package packages/domain (@transvia/domain).
- Domain (
packages/domain/src—@transvia/domain) — the vocabulary: transfer statuses and lifecycle events, the fee model (0.35% / 0.10% / 0.05%), corridor definitions, the policy engine, money units. Pure types and pure functions, no I/O anywhere. - Ports (
src/core/ports) — the seams:ChainAdapter(deposit, settle, claim, refund, x402 verification),FxAdapter(rates, always labeled with their source), and the repository interfaces. Services depend on these interfaces, never on implementations. - Services (
src/core/services) — the use cases: quoting (RFQ quotes, fee split, x402 terms), remittance (send / claim / refund), settlement (the lazy state machine), and the compliance agent (policy-gated sweeps). - Adapters (
src/core/adapters) — the implementations: the simulated and viem-based chain adapters, a simulated StableFX adapter, Prisma repositories. Swapping any of them is a one-line change in the container. - Config (
src/core/config) — the composition root and typed env access. The container is the only place that knows concrete adapters.
The dual adapter
The chain adapter port has two implementations, selected by environment:
- Simulated — the full product loop with honest labeling: fake tx hashes, demo-speed settlement (~6 seconds), and a demo float that is explicitly not money. The default when no chain credentials exist.
- Live (viem) — real escrow calls against Arc Testnet, with USDC balances read from the operator wallet.
Live mode activates when three environment variables are all present:
ARC_RPC_URL + DEPLOYER_PRIVATE_KEY + ESCROW_ADDRESS (plus
DEPLOYER_ADDRESS as the public operator identity). The API always reports
which mode is running — GET /api/health returns mode: "simulated" or
"live" — and the product never pretends simulation is settlement.
The dependency rule
Dependencies point one way only:
UML package diagram — dependency direction (──▶ imports)
┌──────────────────┐ ┌──────────────┐ ┌────────────────────┐
│ ▒ apps │ │ ▒ api │ │ ▒ services │
│ (site) · console │──▶│ route │──▶│ quoting · remit- │
│ docs · mcp-server│ │ handlers │ │ tance · settlement │
└──────────────────┘ └──────────────┘ │ agent engine │
└─────────┬──────────┘
│ uses
┌──────────────────┐ ▼ (interfaces only)
│ ▒ domain (pure) │ ┌──────────────────┐
│ intent · fees · │ │ ▒ ports │
│ money · corridor │ │ Chain · Fx · Repo│
│ · policy │ └────────▲─────────┘
└────────▲─────────┘ │ implements
│ │
└──────────┬──────────┘
▼
┌────────────────────────────┐
│ ▒ adapters │
│ simulated · viem (live) · │
│ StableFX sim · Prisma repos│
└─────────────┬──────────────┘
▼
Arc RPC · USDC · TransviaEscrow.sol
(adapters are wired only by the config container)- Routes (api) parse HTTP and map domain errors to status codes — nothing more.
- Services contain the use cases and know nothing about HTTP or storage.
- Ports are the contracts; adapters implement them.
- Only the container imports adapters and wires them in.
That is why one domain serves the web app, the REST API, and the MCP server without forking behavior — and why the simulated and live rails cannot disagree about the rules. The onchain half of the picture is in Smart Contracts; the agent services are in Automation Agents; machine access is x402 and MCP.