open-standards/openpay/docs/25-crypto-rail-progress.md
Transaction Science 70ecd6caa0 Initial import — three open standards under one shelf
Transaction Science open standards monorepo. Each subdirectory is a
self-contained reference protocol with its own README and licence.

- openpay/    Apache-2.0  payment-acceptance stack (Rust, 26 crates)
- smart-byte/ CC-BY-4.0   content-addressed value carrier (spec)
- eoc/        CC-BY-4.0   energy-optimised compute (spec)

The wire format and the right to fork are public. The optional hosted
services are the offer; the protocols are not.
2026-05-23 07:58:09 -04:00

8.5 KiB
Raw Permalink Blame History

Phase 25 — Crypto rail: stablecoin settlement at sub-cent cost

Status: Draft v0.25 Date: 2026-05-22

Why

The mission has always been "bring margin back to the vendors." Card networks charge 100–300 basis points per transaction. A USDC transfer on Base or Solana costs single-cents or fractions of a cent. For a $1M/month vendor that's the difference between $30,000 and $5 in fees. That is the entire point.

Phase 25 adds the crypto / stablecoin rail to OpenPay. First of three sequenced phases (25 → 26 → 27): crypto first because it's the most direct path to the margin win, then persistent backends (deployable), then subscriptions (feature surface).

What shipped

# Item Where
1 RailKind::Crypto variant in op-core op-core/src/rails.rs
2 PaymentMethod::Crypto(CryptoAddress) variant in op-core op-core/src/method.rs
3 CryptoAddress { chain, address } type with chain-agnostic constructor op-core/src/method.rs
4 op-rails-crypto crate — CryptoGateway trait, CryptoTransferReq / CryptoDecision / CryptoStatus, StableToken curated catalog crates/op-rails-crypto/
5 op-orchestrator::CryptoAdapter — wraps any CryptoGateway as a RailAdapter op-orchestrator/src/adapters/crypto.rs
6 PolicyRouter::with_crypto_drivers + rail_order carve-out (Crypto is method-exclusive) op-orchestrator/src/router.rs
7 DeterministicCryptoGateway in op-driver-sdk — per-key overrides, amount rules, transport-error mode, history op-driver-sdk/src/crypto.rs
8 conformance::run_crypto harness — CryptoMissingTxHashOnFinalized, CryptoCrossChainAccepted checks op-driver-sdk/src/conformance.rs
9 Orchestrator integration: crypto-routed approve + reject flows, all-three-rails conformance op-driver-sdk/tests/orchestrator_integration.rs
10 RailKind codec in op-graph rail_telemetry covers Crypto op-graph/src/rail_telemetry.rs

Workspace at the end of Phase 25:

Check Result
cargo build --workspace --all-targets 0 errors, 0 warnings
cargo test --workspace 957 passing, 0 failing (+27 vs Phase 24)
cargo clippy --workspace --all-targets 0 warnings

Test-count delta: op-rails-crypto +7 (token / status), op-orchestrator adapters/crypto +7, op-driver-sdk +11 (8 mock + 3 conformance), e2e +2.

Architecture

                                          on-chain
                                              ▲
   PaymentIntent ──► PolicyRouter ──► CryptoAdapter ──► CryptoGateway ──► chain client
        │                                              (driver-owned)        │
        │                                                                    ▼
        └──Idempotency key flows through unchanged──►       signer / RPC / Fireblocks

Three trait layers, mirroring the existing card / A2A pattern:

  1. CryptoGateway (in op-rails-crypto) — the only thing driver authors implement. Methods: submit_transfer, query_status, plus accessors (name, chain, token, supports).
  2. CryptoAdapter (in op-orchestrator) — wraps any CryptoGateway as a RailAdapter the orchestrator can route through. Status → outcome mapping: Finalized → Success, Rejected → HardDecline, Pending|Confirming → SoftFailure, Transient → SoftFailure.
  3. DeterministicCryptoGateway (in op-driver-sdk) — programmable mock for operator tests + driver-author reference.

Why no chain SDK dependency

The reference stack does NOT pull in solana-sdk or ethers-rs. Three reasons:

  1. Footprint. ethers-rs alone is ~250 crates.
  2. Signing security. Operators sign with HSMs, KMS, multisig, Fireblocks. Baking in a software keystore would be a footgun.
  3. Chain evolution. New chains and L2s appear quarterly; the reference stack stays neutral by exposing only the trait surface.

Operators wire their preferred client behind a CryptoGateway impl — a RpcClient for Solana, an EthersProvider for EVM, Fireblocks for custody, whatever fits their security posture.

StableToken catalog

Curated (chain, contract, decimals, symbol) constants for the common deployments:

Constructor Chain Symbol Decimals
StableToken::UsdcSolana solana USDC 6
StableToken::UsdcBase base USDC 6
StableToken::UsdcEthereum ethereum USDC 6
StableToken::UsdcPolygon polygon USDC 6
StableToken::UsdcArbitrum arbitrum USDC 6
StableToken::EurcBase base EURC 6
StableToken::PyusdEthereum ethereum PYUSD 6

Drivers must re-validate contract addresses against the issuer's official docs before broadcasting — token contracts have been upgraded / re-deployed in the past, and minting a transfer to a stale contract burns funds.

CryptoStatus taxonomy

Pending      // submitted, not yet in a block
Confirming   // in a block, below operator confirmation threshold
Finalized    // meets operator finality threshold — settled
Rejected     // chain refused (revert, sim failure, invalid sig)
Transient    // RPC/network — may or may not have broadcast

funds_moved() is true only for Finalized. Operator-acceptable finality is driver-side (1 confirmation on Solana, 12 on Ethereum mainnet, varies on L2s).

Routing changes

PolicyRouter::rail_order short-circuits: any PaymentMethod::Crypto(_) routes exclusively to crypto drivers. No fallback to card or A2A — those rails physically can't fulfill a crypto destination, so the orchestrator doesn't try.

let router = PolicyRouter::new(vec![], vec![])
    .with_crypto_drivers(vec!["usdc-base".to_owned(), "usdc-solana".to_owned()]);

The chain-level routing (which (chain, token) driver) is the operator's choice — they register one adapter per pair and rely on supports() to filter by destination chain.

Conformance additions

let gateway = MyChainGateway::sandbox(rpc_url, signer);
let report = op_driver_sdk::conformance::run_crypto(&gateway);
assert!(report.is_clean(), "{:?}", report.failures);

New failure modes:

  • CryptoMissingTxHashOnFinalized — finalized without a tx_hash; callers have no handle for reconciliation.
  • CryptoCrossChainAccepted { gateway_chain, address_chain } — driver accepted a destination on a chain it doesn't service. A Solana driver shouldn't broadcast EVM addresses.

Honest concerns (carry-forward)

  • No on-chain signing. That's operator-side by design (see "Why no chain SDK dependency" above).
  • No bridging. Cross-chain transfers (USDC@Base → USDC@Solana) are out of scope. Operators handle multi-chain by registering one driver per (chain, token) and routing on destination chain.
  • Confirmation polling is operator-side. A transfer that comes back Confirming is reported as such; the operator drives the polling loop via gateway.query_status(...) and decides when depth meets their finality threshold.
  • Fiat ⇄ stablecoin conversion is operator-side. The intent's Money flows through unchanged. Operators quoting in USD and settling in USDC (1:1 by definition) handle the 2-decimal vs 6-decimal scaling in their quote layer.
  • Memo support varies by chain. Solana has the memo program; EVM has no standard memo. Drivers either emit an indexed event log or skip memos entirely.
  • Reorg handling is operator-side. If a chain reorgs and a previously-finalized tx un-confirms, the operator's polling loop is the right layer to detect it and post a compensating ledger entry.
  • No L2-specific routing logic. Drivers register per (chain, token); cost-optimization across L2s (route Base when fees are low, Arbitrum when not) is a higher-layer concern operators build on top.

Test totals

op-rails-crypto             7  (token catalog 3, status predicates 4)
op-orchestrator/crypto      7  (adapter status mapping + idempotency + chain check)
op-driver-sdk crypto mock   8  (default / overrides / amount / transport / xchain / etc.)
op-driver-sdk conformance   3  (deterministic passes, missing tx_hash, cross-chain accept)
e2e integration             2  (crypto routed through Orchestrator, rejection path)
                                                              ----
                                                              +27 net

cargo test --workspace: 957 passing, 0 failing. cargo build --workspace --all-targets: 0 warnings. cargo clippy --workspace --all-targets: 0 warnings.