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.2 KiB
Phase 24 — Driver SDK: operators can author their own rails
Status: Draft v0.24 Date: 2026-05-22
Why
OpenPay's whole architecture is built around pluggable rail
adapters (RailAdapter, CardAcquirer, A2aAcquirer). What it
didn't have until Phase 24 was the author-side scaffolding —
the deterministic mocks operators use to bring up a deployment
without live credentials, and the conformance harness that lets
driver authors verify their adapter respects the trait's
behavioral contract beyond what the type system enforces.
Final phase of the sequenced trio (22 → 23 → 24) that closes out the operator-facing surface: settlement (Phase 22), HTTP API (Phase 23), driver SDK (Phase 24). After this an operator with real PSP credentials can write a driver, run conformance, register it with the orchestrator, and deploy.
What shipped
| # | Item | Where |
|---|---|---|
| 1 | op-driver-sdk crate — lib + author guide module docs |
crates/op-driver-sdk/src/lib.rs |
| 2 | DeterministicCardAcquirer — programmable card acquirer: per-key overrides, amount-threshold rules, transport-error mode, request history |
card.rs |
| 3 | DeterministicA2aGateway — symmetric programmable A2A gateway with UETR overrides |
a2a.rs |
| 4 | ConformanceFailure taxonomy + ConformanceReport aggregate |
conformance.rs |
| 5 | conformance::run_card — battery of contract checks for any CardAcquirer |
conformance.rs |
| 6 | conformance::run_a2a — equivalent for A2aAcquirer |
conformance.rs |
| 7 | conformance::run_card_with_panic_probe — stronger variant via catch_unwind |
conformance.rs |
| 8 | Orchestrator integration tests — deterministic drivers flowing through a real Orchestrator via CardAdapter / A2aAdapter |
tests/orchestrator_integration.rs |
| 9 | Unit tests proving the harness catches malformed drivers (lying supports(), duplicate PSP ids, empty name, missing UETR) |
conformance.rs::tests |
Workspace at the end of Phase 24:
| Check | Result |
|---|---|
cargo build --workspace --all-targets |
0 errors, 0 warnings |
cargo test --workspace |
930 passing, 0 failing (+24 vs Phase 23) |
cargo clippy --workspace --all-targets |
0 warnings |
Test-count delta: 19 unit (card 8, a2a 5, conformance 6) + 5 e2e integration = 24.
The driver-author flow
1. impl CardAcquirer for MyPspClient { ... }
2. Write at least one happy-path test against your real PSP
sandbox.
3. Run op_driver_sdk::conformance::run_card(&my_client)?
to catch structural bugs.
4. Wrap your acquirer in op_orchestrator::CardAdapter and register
with the orchestrator.
In code:
let driver = MyPspClient::sandbox(api_key);
let report = op_driver_sdk::conformance::run_card(&driver);
assert!(report.is_clean(), "driver failed: {:?}", report.failures);
let card = Arc::new(CardAdapter::new("my-psp", Arc::new(driver)));
orchestrator.register_adapter(card);
What the conformance harness covers
| Check | Failure | What it catches |
|---|---|---|
name() non-empty |
EmptyName |
Telemetry / routing keys off name(); empty breaks operator dashboards |
supports() ↔ authorize() consistency |
SupportsLies |
A misclassifying driver causes the orchestrator to route to a rail that then 4xx's every request |
psp_payment_id non-empty on success |
EmptyPspPaymentId |
Capture / refund / void all require it |
psp_payment_id unique across calls |
DuplicatePspPaymentId |
Reused ids corrupt later capture / refund routing |
authorized_amount or error_code present on success |
AuthorizeResponseEmpty |
Callers have nothing to act on otherwise |
A2A Settled/Accepted echoes UETR |
A2aMissingUetrOnAccept |
Settlement notifications match back to UETR; missing UETR breaks reconciliation |
catch_unwind on transport-error paths |
PanicOnTransportError |
A panicking driver brings down the orchestrator thread |
DeterministicCardAcquirer surface
DeterministicCardAcquirer::new()
.with_default_status(AuthStatus::Settled)
.with_key_override("k-decline", AuthStatus::HardDecline, Some("nsf".into()))
.with_amount_ge(
Money::from_minor(1_000_000, Currency::USD),
AuthStatus::RequiresCustomerAction,
None,
)
.with_transport_error("connect timeout") // exclusive — short-circuits everything
;
// After running:
acq.auth_history(); // every AuthRequest the driver saw
acq.capture_history(); // every CaptureRequest
acq.refund_history();
acq.void_history();
Same shape on DeterministicA2aGateway — UETR overrides, amount
rules, transport-error mode, transfer_history() /
query_history() for assertions.
Why a runtime harness instead of a trait constraint
Rust's type system enforces the shape of the trait but not its behavior. "The idempotency key flows through unchanged" and "transport errors don't panic" are properties only a runtime probe can verify. A doc-only convention is brittle; the runnable harness gives driver authors an immediate pass/fail signal that the OpenPay project can keep in sync with the trait semantics as they evolve.
End-to-end integration
The integration test suite at
tests/orchestrator_integration.rs
proves the deterministic drivers flow through a real Orchestrator
via CardAdapter / A2aAdapter with PolicyRouter. Operators
copy these tests as the starting point for their own driver
acceptance suite.
Honest concerns (carry-forward)
- Idempotency-key propagation isn't probed. The harness documents the requirement (the key the caller sends must equal the key the PSP sees) but can't actively check it without driver cooperation — there's no way to inspect a third-party PSP's view of the request. Driver authors should write a driver-specific test asserting this.
- No async harness. All conformance probes are sync, matching
the current sync acquirer traits. When the traits go async we
add the same checks with
tokio::test. - No fuzz / property-based checks. The harness is a fixed battery. Adding proptest-driven invariants ("for any input the driver responds in finite time / never panics") would be a natural follow-up.
run_cardruns against the real driver. Calling it from CI will hit the PSP sandbox if the driver does network I/O. Operators wrap the driver in their own throttle if they want to run conformance on every PR.- No HTTP request recording. Drivers that talk over HTTP would
benefit from a record/replay layer (capture sandbox responses,
replay deterministically in tests). That's a separate crate.
Most operators handle this with
wiremockormockito.
Test totals
op-driver-sdk 24 tests
card mock 8 (default / overrides / amount / transport / history / supports)
a2a mock 5 (default / override / amount / transport / name)
conformance unit 6 (harness self-test + failure detection)
orchestrator integration 5 (card happy, card decline, a2a happy, a2a amount, conformance both)
----
+24 net
cargo test --workspace: 930 passing, 0 failing.
cargo build --workspace --all-targets: 0 warnings.
cargo clippy --workspace --all-targets: 0 warnings.
What's next
With Phases 22 → 23 → 24 complete, OpenPay now has:
- A double-entry ledger with bi-temporal time-travel (Phases 12, 17)
- A graph-backed audit / fraud query layer (Phases 14, 18, 19, 21)
- A pluggable orchestrator with routing signals (Phases 11, 18, 19, 20)
- Reconciliation with ISO 20022 sources (Phases 15, 20)
- Refund + dispute workflows (Phase 21)
- Settlement + payout file generation (Phase 22)
- A deployable HTTP API server (Phase 23)
- A driver-author SDK with conformance harness (Phase 24)
The remaining gap to "ship and run in production" is operator- side: a real merchant directory, a real fraud scorer model, real PSP integrations using the SDK. None of those belong in the reference stack — they're per-operator concerns the architecture deliberately leaves pluggable. OpenPay is now SOTA-deployable.