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 · refund

Three 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.