Skip to content

The patala mark: a cowrie shell

patala

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.

Status: foundational — built and unit-tested; one rail has one live testnet result

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.

Documentation

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

The idea

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.

Diagram: patala sits beside the money path, never in it. It quotes, charges and verifies; value itself moves directly, either wallet-to-wallet on a crypto rail or payer-to-processor-to-payee on a fiat rail.

The seam

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.

Non-custodial, always

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.

What's built

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.

One trait, both directions

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.

Paying a customer back

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.

The polyglot layer — one adapter, four ways in

Every adapter is written once, in Rust, in patala-core or a rail crate. Nothing is reimplemented per language:

Diagram: four consumers — Rust, Python, Go, HTTP — all reach every rail through the one PaymentRail trait, never directly.

  1. Rust — direct, patala-core plus whichever rail crates you enable.
  2. patala-uniffi — the one UniFFI surface (namespace patala), generating both the Python binding packaged by patala-py/ and (via uniffi-bindgen-go) the Go binding in patala-go/. Real round-trips, real cgo, CI-enforced on both languages.
  3. patala-ffi — a plain extern "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.
  4. 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.

Fifteen languages, two ways

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

Security

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.

The site's contrast is measured, not reviewed

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.

Deferred (designed for, not built)

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

Brand

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.

License

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
vulos — open by design

About

Sovereign, centerless payment-rail substrate — one honest interface over fiat and crypto rails, non-custodial by default, no token, no center. Adopt Hyperswitch for fiat, build the crypto rails.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages