Warning
The following is a proof-of-concept. This open source code is provided for research, experimentation, and developer education only. This code has not been audited, is actively experimental, and may contain bugs, vulnerabilities, or incomplete features. Use at your own risk and obtain legal advice as appropriate - DYOR.
Parity doesn’t deploy the code but may update it based on community feedback.
If you experience problems with any product or service that was built on or deployed from this code, you should contact the third party who deployed the code in its amended form, not Parity.
A prototype funding surface for Polkadot App mobile hosts. It receives an inbound asset, crypto or
fiat-sourced, on an ephemeral account on Asset Hub, converts it into CASH on the People chain,
and hands the result to the host. The same surface runs the other way at #/withdraw: it takes
CASH out of the host's purse and delivers PAS to an Asset Hub address. The prototype consists of
a static Nuxt 4 single-page app plus a background worker, both published to bulletin/DotNS.
The surface (app/, lib/) is what the user sees. It quotes, shows a deposit address or
opens the provider's widget, and tracks the request. The worker (worker/) runs in the
background inside the host. Once a deposit has landed, the surface hands the job to the
worker, which converts it to CASH and teleports it to the People chain in one transaction on
Asset Hub, then claims the CASH through the host's top-up call. The top-up is registered under
the ephemeral account's public key and driven by the host from there; the worker follows its
status until the claim is final, and registers a further top-up for whatever a short claim left
on the account. The worker keeps going after the surface is closed.
The conversion has two tiers, and the PSM is the default. Asset Hub's Peg Stability Module swaps
an approved token for CASH at a given rate less its own fee, so what a purchase buys
is known before it is quoted and cannot move before it settles: the provider delivers that
token and the worker signs one Utility.batch_all that mints the CASH and teleports it to
People. The AssetConversion pool is the fallback, priced at whatever it quotes at the time. It
takes over whenever the PSM cannot serve a request (i.e.: no instance open for the pair, minting
paused, the amount over the instance's debt ceiling or under its minimum) and then the provider
delivers the native token and one XCM swaps and teleports it instead. Which tier a request takes
is decided at quote time, before the provider is told what to send, and recorded on the request
with the fee rate it was quoted; the worker runs the tier it is handed and never chooses one.
On the Polkadot route the token the buyer picks decides: DOT takes the pool, USDT the PSM (the
pool through PAS when the PSM cannot serve), USDC a stable pool leg, one XCM that exchanges USDC
for PAS and PAS for CASH inside the holding and teleports the CASH, every fee paid in the stable,
and dotUSD, the underlying itself, a teleport with no conversion, its fees paid in dotUSD.
@getsome/funding holds the four programs, the routing rule and the tick.
The two talk over host storage. lib/worker-rpc.ts (surface side) and worker/src/rpc.js
(worker side) implement a polled request/response channel: the surface writes a request under
a sequence key, the worker claims it, runs the handler, and writes the response. Job records
live in the same storage, so either side can resume after a restart.
Every request runs through a fresh ephemeral keypair. Nothing is stored for it. The
session asks the EntropyPort for a seed under a label made of the source id and the trade
number, and @getsome/ephemeral derives the sr25519 keypair from that seed.
Inside a host the entropy is deterministic, so the surface, the worker, and a later resume all
re-derive the same account from the same label. In a plain browser a random seed is persisted
instead, which is enough for UI work but cannot be recovered across devices.
The rail delivers funds to the ephemeral's address. The worker re-derives the same key to spend them. When the claim completes, the next request moves to a new trade number and a new account. Rails with a refund leg get a second per-request key on the source chain, derived the same way, so a refund never needs a typed address.
The off-ramp reuses the pieces above in the other direction. The page at #/withdraw derives a
fresh ephemeral key under a wd: label and asks the host to pay the CASH into it, under a payment
id derived from the key. The worker watches that key on People and runs two transactions signed
by it: a swap that buys the PAS the fees need, then one XCM that withdraws everything the key
holds and lands PAS on the destination account on Asset Hub. The key is left empty and reaped.
Arrival is a balance read at the head, like every other read in the engine: the destination's
PAS is read just before the XCM leaves, and the run is done once it has grown by what the Asset
Hub dry run said would land. Hosts serve the current head and nothing older, so nothing follows
block history. @getsome/withdraw holds the program, the sizing and the tick;
worker/src/withdraw-engine.js drives it.
Withdrawals are a second record kind in the same request store as the top-ups, moved by the same observations and the same reconcile.
@getsome/core owns the session state machine and defines the ports it needs. The rail port
(ChainflipRail in packages/core/src/ports.ts) is the contract a funding source has to
meet: reverse-quote a target amount, open a deposit channel to the ephemeral, report status,
probe liquidity, and list its sources. Two packages implement it, and one source needs none:
@getsome/chainflipfor crypto deposits, over the Chainflip SDK. Sources are BTC, ETH, USDC and the other Chainflip assets.@getsome/meldfor card and bank sources, over a Meld adapter service. Meld delivers the route's deposit asset to the ephemeral; from there the flow is identical to a crypto deposit.- Polkadot directly: the buyer sends DOT, dotUSD, USDT or USDC from any wallet to the ephemeral
on Asset Hub.
createManualRailin@getsome/fundingstands in for the rail port, one source id per token, and the deposit screen shows the account as a QR at once.
The core session does not know which rail it is running. The surface picks one per route.
On the UI side the same idea repeats. app/funding/ is a shell that resolves a funding
route (crypto, card, bank) to a route package: a lazily loaded screen, a top-up adapter
that projects that rail's records into the shared history, and an optional status line for
the journey view. A route with no registered package is shown as unavailable. Adding a
funding source means implementing the rail port and registering a route package; the
session, the worker, and the history need no change.
Everything the engine touches outside its own code comes in through a small set of ports:
ChainPort (balances, submit, sweep), StorageAdapter, EntropyPort, and the rail. Three
packages wire them:
@getsome/hostfor running inside a Polkadot App host: storage, entropy, and chain access come from the host API.@getsome/browserfor a plain browser: local storage and a persisted random seed.@getsome/testingfakes for the unit tests. The same fakes drive the mock world the app runs when no host is present, so the whole state machine can be exercised atlocalhost:3000without a chain.
app/ Nuxt 4 SPA: screens, composables, pinia stores, the funding shell
lib/ host integration: chain connections, session wiring, faucet, worker RPC
worker/ the background executable (plain ESM, bundled by esbuild)
packages/ the engine, one workspace package each (see below)
tests/ app-level tests; each package keeps its own unit tests next to its source
public/ static assets copied into the build; public/worker/ receives the worker bundle
brand/ the product icon used in the bulletin manifest
.papi/ polkadot-api chain descriptors, regenerated by `pnpm install`
.github/ build, deploy and PR-preview workflows
| Package | Role |
|---|---|
@getsome/core |
session state machine, flow store, re-entry logic, port types |
@getsome/ephemeral |
seed to keypair derivation, handoff secret encoding, refund keys |
@getsome/funding |
the funding program the worker runs on Asset Hub |
@getsome/withdraw |
the withdrawal legs the worker runs on People, swap then XCM |
@getsome/chainflip |
crypto rail over the Chainflip SDK |
@getsome/meld |
card and bank rail over the Meld adapter |
@getsome/people |
People chain port: CASH balances and the handoff submit |
@getsome/revive |
Asset Hub chain port over polkadot-api, batch and sweep building |
@getsome/host |
port wiring for the Polkadot App host |
@getsome/browser |
port wiring for a plain browser |
@getsome/testing |
fakes for tests and the mock world |
Packages export TypeScript source directly (exports points at src/index.ts); there is no
build step for them. They are private to this workspace and are not published to npm.
pnpm build:worker bundles worker/src/index.js into a single ESM file and copies it to
public/worker/index.js. pnpm build then runs nuxt generate, which copies public/ into
.output/public, so the worker bundle ends up inside the site. Run the worker build first;
public/worker/ is not tracked.
bulletin-deploy.config.ts describes the product for bulletin-deploy: a manifest record on
the base name and one executable record each for the app (.output/public) and the worker
(.output/public/worker).
Node 22 and pnpm 9.12 (the packageManager field pins the exact pnpm version).
pnpm install # also regenerates .papi/ descriptors
pnpm dev # localhost:3000, mock world in a plain browser
pnpm test # unit tests for the packages and the app
pnpm typecheck # app layer (vue-tsc)
pnpm typecheck:packages # the engine (tsc)
pnpm format # prettierA build targets the network packages/core/src/network.json describes: whether it is a testnet,
the native symbol and, for Asset Hub and People, the para id, genesis hash and endpoint, plus
People's pool account. .papi/ holds the metadata the calls are typed from and must name the same
genesis hashes; the build refuses a mismatch. Both describe Paseo Next. Demo builds, and with them
the faucet and Skip, need "testnet": true; the build refuses VITE_FAUCET_SEED otherwise.
Copy .env.example to .env and fill in what you need. Nuxt reads .env, not .env.local.
| Variable | Purpose |
|---|---|
VITE_FAUCET_SEED |
demo faucet account, holding PAS, dotUSD, USDt and USDC on Asset Hub; inlined into the client bundle, use a testnet account |
VITE_DEPLOYER_SEED |
fallback for MNEMONIC in deploy.sh |
VITE_MELD_BASE_URL |
origin of the Meld adapter; unset, the offline fake Meld client runs instead |
VITE_MELD_PRODUCT_ID |
product id the adapter expects in the x-dev-product-id header |
Two tests submit real transactions to the Paseo testnet and are skipped unless enabled:
PROD_PROOF=1 runs tests/prod-proof.test.ts, VERIFY_AMOUNTS=1 runs
tests/verify-amounts.test.ts. VERIFY_STABLE=1 runs tests/verify-stable.test.ts, which
dry-runs the USDC and USDT programs from a rich account on Paseo and spends nothing, and
VERIFY_DOTUSD=1 runs tests/verify-dotusd.test.ts, the same for the dotUSD teleport.
Being a prototype, parts of the tree exist to keep a demo moving and are not what a real
deployment would do. Each is marked TODO(production) at its definition:
- the faucet (
lib/faucet.ts,app/utils/demo.ts), which funds the burner in the route's deposit asset on Skip, and theestimateSource*helpers behind the≈amounts on the deposit screen demoFallbackinapp/stores/offers.ts, which offers every source ungated when Chainflip answers for nothingDEMO_MAX_CASHinapp/stores/session.ts, a 200 CASH cap on a purchase- the demo Chainflip picks, which run the Polkadot direct deposit under the source of the token the route delivers until the Chainflip channel rail lands
./deploy.sh [name.paseo] # default getcash.paseoBuilds the worker and the site and publishes both with bulletin-deploy, which must be
installed globally. The deploying account comes from MNEMONIC, or from VITE_DEPLOYER_SEED
in .env.local or .env.
CI does the same: a push to main deploys getcash.paseo, and every pull request gets a
preview at pr<N>-getcash.paseo. Both are signed with the repository's MNEMONIC secret and
build with VITE_FAUCET_SEED and VITE_MELD_BASE_URL from repository secrets.
Copyright (C) 2026 Parity Technologies. Licensed under the GNU General Public License v3.0 or later; see LICENSE.