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.
8.5 KiB
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:
CryptoGateway(inop-rails-crypto) — the only thing driver authors implement. Methods:submit_transfer,query_status, plus accessors (name,chain,token,supports).CryptoAdapter(inop-orchestrator) — wraps anyCryptoGatewayas aRailAdapterthe orchestrator can route through. Status → outcome mapping:Finalized → Success,Rejected → HardDecline,Pending|Confirming → SoftFailure,Transient → SoftFailure.DeterministicCryptoGateway(inop-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:
- Footprint.
ethers-rsalone is ~250 crates. - Signing security. Operators sign with HSMs, KMS, multisig, Fireblocks. Baking in a software keystore would be a footgun.
- 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
Confirmingis reported as such; the operator drives the polling loop viagateway.query_status(...)and decides when depth meets their finality threshold. - Fiat ⇄ stablecoin conversion is operator-side. The intent's
Moneyflows 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.