A sovereign, centerless payment-rail substrate. One interface to move value — fiat or crypto — that any product can vendor and self-host.
MIT OR Apache-2.0 · Rust · non-custodial · no token
The platform holds no funds, takes no cut, and no one owns the network.
patala is Sesotho/Setswana for "to pay." Part of the Vulos family
(vula = "open"). PATALA.md is the anchor spec and single source of truth;
this README describes what is actually built, honestly, as it lands.
patala is a library and a sidecar — there is no GUI. Everything below is either a crate, a trait, or a process you run next to your own app.
Not writing Rust? Fifteen language packages
in sdks/ reach the same core two ways — in-process through a
six-function C ABI, or over a patala-sidecar the package
spawns and manages for you.
The core, the rails and the polyglot layer are all in this repo. make check
runs seven gates: fmt-check, lint, test, test-features, doc,
features and site-check. Two of them are the test suites — 324 offline
tests across the nine landed crates in the default workspace build (314 unit
and integration tests plus 10 doctests), and 618 more once every processor
feature is compiled in (cargo test -p patala-fiat --all-features, 570, +
cargo test -p patala-uniffi --features fiat-all, 20, + cargo test -p patala-ffi --features fiat-all, 28). Clippy-clean, fmt-clean; the default build
pulls no chain and no processor.
What that does not mean: no rail here has been run against a live
merchant account from this repo, and only one rail has been run
against a live network at all — patala-stellar, twice, both on
testnet, both 2026-07-30: a single-leg USDC-shaped payment built and
submitted through the real StellarRail::charge API, independently
confirmed by StellarRail::verify reading it back from Horizon (B7), and
separately, a 3-instalment recurring/pre-authorized schedule (B4,
recurring::RecurringPlan) that settled its first instalment immediately,
had its second instalment genuinely rejected by real Horizon
(tx_bad_minseq_age_or_gap) when resubmitted too early and then accepted
once the pacing floor elapsed, and had its third, still-outstanding
instalment permanently invalidated by a real on-chain cancellation
(tx_bad_seq on resubmission) — transaction hashes and ledger sequences for
both in patala-stellar/README.md. Read both narrowly — neither says
anything about mainnet, and neither says anything about atomic multi-party
splits (patala-stellar now has one — StellarRail::charge_split/
verify_split, B1 — tested offline only, never run against a live network),
or about any other rail, which each still say plainly, in their own
READMEs, that they have not been run live and name the exact step to
validate (fund a testnet account, run the #[ignore]d, env-gated live
test). Treat the rails as a tested foundation to validate against
testnet/sandbox, not as production-proven.
The things that genuinely executed end-to-end are the Python binding, the
Go binding, the C ABI and the sidecar — real round-trips over a real
interpreter, real cgo, a real dlopen from C, and a real socket. CI runs
five jobs: the two Rust passes (make check), the Python smoke run, the Go
binding's test suite (CI installs uniffi-bindgen-go at the pinned tag and
uses the C toolchain the runner already has), the C ABI dlopened from C on
ubuntu-latest (make smoke-ffi, twice), and the docs/site gate. The Kotlin
and Swift UniFFI bindings are run by hand — make smoke-kotlin and
make smoke-swift — and have no CI job.
Nineteen documents in docs/, readable here on GitHub or in the
docs viewer at site/docs.html. docs/ is the single source; site/docs/ is
generated from it by node scripts/gen-site-docs.mjs, and
--check fails the build if the two ever drift, so the site cannot ship a
stale page.
Start: What patala is · Quickstart · Choosing a mode
Architecture: The rail interface · One core, every language · The offline default build
Consuming it: Rust, embedded · Python binding · Go binding · Fifteen language packages · The C ABI · The sidecar HTTP API
The rails: Solana & Stellar · Hyperswitch & fiat · Paying a customer back · Splits & shared economics
Operating: Self-host & vendor · Troubleshooting · Status & verification
Not sure where to start? Quickstart gets a charge →
verify round trip running in your language against a rail that needs no
network, and Choosing a mode is the decision that
shapes everything after it.
Payment adapters split into two kinds, and patala treats them completely differently:
| Fiat processors (Stripe, Paystack, Xendit, …) | Crypto rails (Solana, Stellar, …) | |
|---|---|---|
| Shape | REST calls + webhook verification | tx construction + signing + chain RPC |
| Trust | custodial, reversible (chargebacks), KYC, T+2 | non-custodial, final, wallet-based, near-instant |
| Build vs. adopt | adopt — Hyperswitch already ships 100+, Apache-2.0, Rust, self-hostable | build — this is the part nobody provides non-custodially |
The whole value add is (a) the non-custodial crypto rails, and (b) a thin
capability layer that presents both classes behind one honest interface, with
failover, and never blurs which class you're getting. See PATALA.md §2 for
the full reasoning.
patala-core/ trait + capability model + FailoverRail + MockRail + errors + receipt + webhook
Every consumer of patala programs against one trait — PaymentRail — and one
capability descriptor — RailCapabilities. Nothing names a provider-specific
type. The settlement class (CustodialReversible vs NonCustodialFinal)
lives in the type, not a flag, because it changes what you owe the payer.
This is patala-core's own crate-level doctest, word for word — it runs
under cargo test --doc on every make check:
use patala_core::{MockRail, PayRequest, PaymentRail, RailClass};
let rail = MockRail::new("mock", RailClass::NonCustodialFinal, vec!["USDC".into()]);
let req = PayRequest {
amount_minor: 500, // 5.00 USDC — integer minor units, never a float
currency: "USDC".into(),
destination: "wallet-or-processor-token".into(),
reference: "order-1".into(),
};
// `charge` returns the Receipt — the entitlement.
let receipt = rail.charge(&req).await.unwrap();
assert_eq!(receipt.reference, "order-1");
// Gate on `verify` returning `Ok(true)`, never on `charge` merely having
// returned `Ok`: a receipt can be stored and re-checked later, and only
// `verify` re-derives whether it still holds.
assert!(rail.verify(&receipt).await.unwrap());Swap MockRail for a real one — or wrap several in a FailoverRail, which
tries them in order and refuses to silently cross from a NonCustodialFinal
request to a CustodialReversible rail — and nothing else in the code above
changes; that's the point of the seam. See patala-core/README.md for the
FailoverRail example, the full method list, and test instructions.
No code path anywhere in this repo may make patala hold funds. A rail can set
holds_funds: true on its own capabilities — that's the rail's processor
custodying money, e.g. Stripe behind Hyperswitch — but the substrate itself
never does. There is no balance table, no payout queue, no ledger.
| Crate | What it is | Class | Tests | Live-verified? |
|---|---|---|---|---|
patala-core |
trait + capability model + FailoverRail + MockRail + the webhook seam + the destination seam |
— | 38 + 3 doctests | offline by design |
patala-fiat |
20 direct processor adapters + the ISO-4217 currency table + the offline manual rail |
custodial, reversible | 570 (all features) | no — no live merchant account |
patala-solana |
SPL-USDC on Solana, ported from magnetite-seams/src/solana/ |
non-custodial, final | 56 (+1 gated) + 2 doctests | no — testnet step in its README |
patala-stellar |
native USDC on Stellar (SDF's own stellar-xdr/stellar-strkey) |
non-custodial, final | 84 (+3 gated) + 5 doctests | no — testnet step in its README |
patala-hyperswitch |
adapter to a self-hosted Hyperswitch (its whole processor set as one rail) | custodial, reversible | 23 | no — needs a live instance |
patala-uniffi |
the one UniFFI surface, namespace patala → Python, Go, Kotlin and Swift today, wasm later |
— | 11 Rust (20 with fiat-all) + 19 top-level Go binding tests, 34 with the fiat build tag (patala-go/bindingtest) + ✓ ran under Python 3.13 and Go 1.25 |
executed, and now CI-enforced |
patala-py |
the Python wheel over patala-uniffi (cdylib + generated patala.py) |
— | 3 (namespace + re-export) + the CI smoke-python job |
executed, and now CI-enforced |
patala-ffi |
a plain extern "C" cdylib (JSON in/out, uint64 handles) for the eleven languages UniFFI cannot serve — C, C++, Swift, Java, Node/Deno/Bun, Ruby, PHP, .NET, Elixir |
— | 26 Rust (28 with fiat-all) + 58 checks driven through C by ctest/smoke.c |
executed, and now CI-enforced |
patala-sidecar |
loopback HTTP over the core, token-gated, fail-closed | — | 17 (12 HTTP round-trips + 5 unit) | executed |
One honest caveat on that table: the sidecar's rail registry is still
mock-only. The server, its auth, its error mapping and all six endpoints
are real and exercised over a real socket, but default_registry() registers
exactly one rail — "mock". Reaching a Solana, Stellar, Hyperswitch or fiat
rail through the sidecar needs the per-rail registration its
patala-sidecar/src/registry.rs documents and does not yet have. Everything
else in the table is built code with tests behind it.
Fiat coverage is Hyperswitch's coverage, plus twenty direct adapters. Any
processor Hyperswitch supports is a config value — Paystack is supported
(confirmed in Hyperswitch's connector list), so it's free through the adapter.
A processor Hyperswitch lacks — PayFast, for example (confirmed absent) —
gets a direct adapter in patala-fiat against the same PaymentRail trait;
PayFast is one of the twenty that exist today. Nothing is ever locked out.
Every rail beyond the mock is feature-gated and optional; the default build of this repo stays fully offline no matter how many rails exist here.
Every consumer — Rust, Python, Go, or an HTTP client talking to the sidecar —
gets the same seven methods on PaymentRail: id, capabilities, quote,
charge, verify, verify_webhook, and validate_destination.
verify is for when you hold a receipt and want it re-derived; verify_webhook
is the push path, for when the processor calls you and you need to know the
delivery is genuine; validate_destination is the pre-flight path, for
checking a payout address offline before any money moves. All three live on the
trait deliberately — anything beside it is invisible to every consumer that
dispatches through dyn PaymentRail, which leaves them able only to poll.
refund returns Unsupported on every NonCustodialFinal rail — finality is
the whole point of that class — and that is not a gap. Giving the money back
there is a compensating payment: a second, independent charge to an
address the customer supplies, never the address the payment came from,
which is very often an exchange withdrawal address where the funds cannot be
credited back to them. validate_destination is the offline check on that
address; every verdict it returns requires a human to confirm, because patala
does not detect exchange-owned addresses and will not guess. The whole flow,
including the wording to show a customer, is in
docs/compensating-payments.md.
Every adapter is written once, in Rust, in patala-core or a rail crate.
Nothing is reimplemented per language:
- Rust — direct,
patala-coreplus whichever rail crates you enable. patala-uniffi— the one UniFFI surface (namespacepatala), generating both the Python binding packaged bypatala-py/and (viauniffi-bindgen-go) the Go binding inpatala-go/. Real round-trips, real cgo, CI-enforced on both languages.patala-ffi— a plainextern "C"shared library, JSON in and JSON out, for the languages UniFFI cannot generate for. patala being Rust, it puts no runtime in the host process: no GC, no scheduler, no signal handlers, nothing started at load, and an 0.81 MiB offline artifact. A C smoke test dlopens it and counts the process's threads, so that claim is enforced rather than asserted.patala-sidecar— a thin local HTTP server over the core, token-gated and fail-closed. Any language with an HTTP client can drive the substrate with zero FFI; keys live in one hardened process instead of being smeared across every app.
You do not have to wire any of that up by hand. sdks/ holds
a working package for fifteen languages — bun, c, cpp, deno, dotnet, elixir,
go, java, kotlin, node, php, python, ruby, rust, swift — each with an
in-process path and a managed-sidecar path, and each with two runnable examples
doing the same charge → verify round trip against MockRail: offline,
deterministic, no credentials. A payments library whose example moves real
value is not an example.
sdks/README.md is the index, and its "Default" column is
a real recommendation with a per-language reason. Two of those recommendations
are worth calling out here, because they are the reverse of what the same page
says in llmux and openrate — and the reversal was measured, in each case
against a Go library in the same environment as a control:
| patala (Rust core) | a Go c-shared control |
|
|---|---|---|
HotSpot signal handlers replaced (sdks/java/signal-probe.sh) |
0, and 0 with altered flags | 5 replaced, 3 with altered flags |
-Xcheck:jni says |
nothing | Warning: SIGSEGV handler modified! … Consider using jsig library. |
JVM threads, before dlopen → after → after a round trip |
23 → 23 → 23 | — |
A Node worker_threads worker that entered the library |
exits 0 in ~33 ms | never exits — killed at 15 s |
| Node process threads across a round trip | 7 → 7 | 7 → 13 |
| Release library, offline mock-only build | 849,584 bytes | libllmux.dylib 12,823,104 |
So Java and Kotlin default to in-process here: libjsig is the siblings'
whole argument for the sidecar, it is a flag on the java launch command that a
library cannot add to a running process, and patala does not need it. And
patala's Node package ships a working callAsync, which neither sibling
could. --features fiat-all — twenty processor adapters, UniFFI, reqwest and
TLS — brings the library to 6,350,144 bytes.
The costs that are real are stated in every package rather than buried: a
current-thread Tokio runtime per handle, so calls on one handle serialise;
cgo if you choose the Go binding (cackle chose the sidecar for exactly
that reason); rustc stamping the cdylib's LC_ID_DYLIB with an absolute
build-tree path, worked around with install_name_tool; a handle that is not
usefully inherited across fork() if it was in use when the fork happened
(4–8 of 200 against 0 of 200 for one opened in the child); darwin/arm64 as the only target built and
executed, with the .so smoke-tested in CI and no Windows DLL at all; and
no streaming anywhere, in any language, deliberately.
One thing overrides every default in that index: the sidecar's rail registry is mock-only, so in-process is the only path to a real rail today.
→ Fifteen language packages has a run command for every row · The C ABI is the six-function contract eleven of them are built on · Choosing a mode puts all five surfaces side by side.
No code path holds funds; every receipt fails closed if it can't be
verified; a rail that can't do a refund or a webhook check returns
Unsupported rather than faking one. See SECURITY.md for the reporting
process and the full scope — and for what a tagged release publishes: a source
archive plus prebuilt C ABI bundles for linux/amd64 and darwin/arm64, a
SHA256SUMS manifest covering every asset, and a sigstore build-provenance
attestation minted from the workflow's OIDC identity (no long-lived signing
key). scripts/verify.sh is the consumer half and fails closed — there is no
skip path, and a missing manifest is never read as "nothing to check". patala
is still on no package registry, so Rust consumers vendor by path or git.
node scripts/check-contrast-rendered.mjs loads the landing and every docs
chapter in a real browser, at two widths and in both themes, and computes WCAG
AA contrast from the composited pixels — cumulative ancestor opacity,
each background's alpha, then the text colour — rather than from the hex values
in the stylesheet. A gate that reads colour values cannot see a fade: the same
check found a tab strip at opacity:.72 measuring 3.67:1 in a sibling repo
while every token involved was individually fine.
--selftest breaks the page eight ways and requires each break to be refused,
including an opacity fade, an unparseable colour function, prose hidden behind
aria-hidden, and text at opacity:0 that hovering never reveals. Each
mutation must also demonstrably change the rendered measurement first — a
selector that matches is not a declaration that wins — and the unmodified page
must still pass, because a gate that refuses everything refuses every mutation
too.
Any-stablecoin mint generalization, an Algorand rail, and a gateway-discovery
phonebook. See PATALA.md §4. (A direct PayFast rail was on this list; it now
exists, as patala-fiat's payfast adapter.)
The mark in brand/ is the source of truth. Every icon this repo
ships — favicon, PWA and app icons, the mark in the README and on the site — is
rendered from brand/logo.svg rather than redrawn, so there is one approved
drawing and no second copy to drift.
Copy it outward, never edit a derived copy, and never edit brand/ to match
something downstream.
MIT OR Apache-2.0 — © VulOS. No token. No
protocol tax. All seven crates in this workspace declare
license = "MIT OR Apache-2.0" in their Cargo.toml, matching the pair
offered here — a tool resolving licences from crate metadata (cargo deny,
cargo about, an SBOM generator) sees the same grant a human reading this
file does.
![]()
vulos — open by design