This project is a bridge between Syscoin UTXO and Syscoin NEVM. It allows Syscoin assets to be moved to Syscoin NEVM and back.
Trustless transfer of SYS back and forth between the Syscoin UTXO and Syscoin NEVM blockchains without middlemen !
- User burns SYS to create SYS on the Syscoin UTXO chain by via
syscoinBurnToAssetAllocationRPC call. - User burns SYSX to create SYS on the Syscoin NEVM chain by via
assetAllocationBurnRPC call and specifying the NEVM address which receives the SYS on NEVM chain. - Once both transactions are mined, the user can now use the transaction data to build a SPV proof
fetchBackendSPVProof. This proof is then send to a Smart Contract on Syscoin NEVM chain. - The Smart Contract verifies the SPV proof and if valid, mints SYS on the Syscoin NEVM chain to the address indicated on the SPV proof.
- User freezes and Burn their SYS by calling on the
SyscoinERC20ManagercontractfreezeBurnERC20function. - Once the transaction is mined, the user can now use the transaction data to mint SYSX asset on UTXO chain by calling
assetAllocationMintRPC call. - Once SYSX is minted, this again can be burned using
assetAllocationBurnto get native SYS on UTXO.
The bridge UI is a ReactJS application that allows users to interact with the bridge. It is a NextJS application that uses Mongodb for storage. This allows users to interact with the bridge without having to install any software.
Each step taken on the Bridge is stored in MongoDB. This allows the user to resume the process at any time.
When FOUNDATION_FUNDED=true, the bridge sponsors destination-side transaction fees directly. Users still sign and pay the source-chain transaction.
sys-to-nevm: the configured NEVM sponsor wallet signs the final NEVMsubmit-proofstransaction, so the destination NEVM address does not need pre-existing gas to receive SYS.nevm-to-sys: the UTXO sponsor signs and broadcasts the SYSX mint. For the following SYSX-to-SYS burn, Pali signs the user-owned SYSX input and the backend signs a reserved sponsor-owned SYS fee input before broadcasting.sys-to-nevmwith existing SYSX: the same split-signature UTXO flow is attempted for the SYSX burn. If the SYSX input already carries native SYS, the bridge uses the normal user-funded Pali path so native change never crosses ownership boundaries.
UTXO sponsorship reserves individual sponsor outputs by txid:vout in MongoDB before signing. Pre-split the UTXO sponsor wallet into multiple outputs to support concurrent users. The backend never gives a sponsor signature to the browser: it verifies the Pali-signed PSBT against the stored unsigned transaction, adds its signature last, and broadcasts atomically.
Sponsorship is idempotent per transfer/action and source transaction inputs, so refreshing or aliasing a transfer cannot repeatedly spend sponsor outputs. If the sponsor is disabled or has no available output, the UI falls back to the existing user-funded flow.
Sponsor signing keys are configured through deployment secrets/env vars. MongoDB stores sponsorship usage and UTXO reservations only; it does not store sponsor private keys.
NEVM sponsorship is server-broadcast: the backend durably stores each signed transaction, broadcasts it in nonce order, and returns only the accepted hash to the browser. Pending raw transactions are replayed by later requests before another nonce is signed.
Treat enabling FOUNDATION_FUNDED=true as an atomic V2 backend cutover:
- Keep funding disabled while all pre-V2/older backend instances are stopped.
- Configure
NEVM_V2_ACTIVATION_BLOCKto the immutable first NEVM block eligible for V2 sponsorship. Funding fails closed if it is missing or invalid. - Reconcile every legacy signed sponsor row that lacks
actionorsourceTxHash. Confirm/broadcast or replace its nonce as appropriate, then archive the row; do not copy it into the V2 sponsor namespace. - Reconcile duplicate historical transfer IDs. Startup checks for duplicates and synchronously creates the unique transfer-ID index before serving public writes.
- Deploy the V2 backend and frontend together to every instance and allow its MongoDB indexes to be created. New transfers receive a per-transfer write capability; pre-cutover rows without one are intentionally read-only through the public API.
- Enable foundation funding only after all instances run the same V2 sponsor protocol.
Startup fails closed when foundation funding is enabled while an unreconciled legacy signed row remains. A rolling deployment with old sponsor-signing instances is unsupported.
- NodeJS 24+ (recommended to use
nvmto install NodeJS) - Yarn (recommended to use
npm install -g yarnto install Yarn)
This project uses Node.js v24. We recommend using nvm to manage Node versions:
# Install nvm (if not already installed)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.4/install.sh | bash
# Restart your terminal or run:
source ~/.bashrc # or ~/.zshrc
# Install and use the correct Node version (reads from .nvmrc)
nvm install
nvm use
# Or manually install Node v24
nvm install 24
nvm use 24yarn installRuns NextJS Dev Server on port 3000
yarn devyarn builddocker build -t syscoin/bridge .The backend deployment treats the Compose environment files as authoritative:
- Tanenbaum:
/home/ubuntu/syscoin-bridge-testnet/.env - Mainnet:
/home/ubuntu/syscoin-bridge-mainnet/.env
The deployment copies application images and Compose configuration but does not
generate, replace, or import these files from Vercel. MONGO_ROOT_USER and
MONGO_ROOT_PASSWORD initialize an empty Mongo volume only. The Docker backend
derives its internal Mongo URI from those existing credentials. The managed
Compose service explicitly ignores MONGODB_URI so a legacy URI with embedded
credentials cannot drift from the initialized database volume; the override
remains available to non-Compose runtimes. Never change initialized root
credentials or delete/recreate a database volume to apply an environment
change. Before validation, deployment reconciles the root settings from the
healthy running Mongo container without logging them. It also recovers the
existing data, configuration, and backup volume names from the running
containers. All three volumes are then attached as explicit external volumes,
so Compose never offers to recreate an existing Mongo data volume.
| Name | Description | Default |
|---|---|---|
MONGO_ROOT_USER |
Existing Mongo root user used by Docker Compose and URI derivation | |
MONGO_ROOT_PASSWORD |
Existing Mongo root password used by Docker Compose and URI derivation | |
MONGO_APP_DB |
Mongo application database | bridge |
MONGODB_URI |
Optional MongoDB URI override for non-Compose runtimes; managed Compose ignores it | |
MONGO_DATA_VOLUME |
Existing external Mongo /data/db volume name |
|
MONGO_CONFIG_VOLUME |
Existing external Mongo /data/configdb volume name |
|
MONGO_BACKUP_VOLUME |
Existing environment-specific external Docker volume for Mongo backups | |
CONFIRM_TRANSACTION_TIMEOUTS |
Transaction confirmation timeout | |
MINIMUM_AMOUNT |
Minimum amount of SYS to transfer | 100 |
ADMIN_API_KEY |
Admin API Key | |
SECRET_COOKIE_PASSWORD |
Secret Cookie Password | |
ADMIN_COOKIE_DOMAIN |
Optional domain for admin session cookie | |
NEVM_RPC_URL |
NEVM RPC URL | |
NEVM_EXPLORER |
NEVM Explorer URL | |
NEVM_API_URL |
NEVM Block Explorer API URL (EVM only) | |
UTXO_RPC_URL |
UTXO RPC URL | |
UTXO_EXPLORER |
UTXO Explorer URL | |
IS_TESTNET |
Is Testnet | |
CHAIN_ID |
Chain ID | |
RELAY_CONTRACT_ADDRESS |
Relay contract address | |
ERC20_MANAGER_CONTRACT_ADDRESS |
ERC20 Manager contract address | |
SYS5_ENABLED |
Enable Sys5 features | true |
PALI_V2_NEVM_ENABLED |
Enable Pali V2 NEVM features | true |
FOUNDATION_FUNDED |
Enable direct NEVM and UTXO transaction fee sponsorship | false |
NEVM_V2_ACTIVATION_BLOCK |
First NEVM block eligible for V2 foundation-funded transactions; required when funding is enabled | |
NEVM_SPONSOR_PRIVATE_KEY |
Private key for the NEVM sponsor wallet used to sign sponsored submit-proofs transactions |
|
UTXO_SPONSOR_ADDRESS |
Syscoin UTXO address used to fund sponsored mint and SYSX burn fees | |
UTXO_SPONSOR_WIF |
WIF private key for the UTXO sponsor address | |
NEXT_PUBLIC_API_BASE_URL |
Base URL the frontend uses for API requests | |
CORS_ALLOWED_ORIGIN |
Allowed frontend origin(s) for API CORS responses (comma-separated, no * for admin cookie auth) |
Note: API URLs are only used for EVM networks. UTXO networks use Blockbook which has a different API structure.
When hosting the frontend separately from the backend services, set NEXT_PUBLIC_API_BASE_URL to the backend origin (for example, https://backend.test.com). All browser and server-side fetches automatically use that base URL when provided, and fall back to the current origin otherwise. The same variable also enables a framework rewrite so hitting /api/* on the frontend domain proxies to the backend. This lets a single build work for both combined and split deployments—just omit the variable when the API routes run alongside the frontend.
Vercel testnet previews automatically proxy /api/* to https://bridge-api.tanenbaum.io when no explicit API base is configured. The testnet backend accepts HTTPS *.vercel.app origins for these preview requests. Mainnet previews are never automatically connected to the production backend, and the mainnet backend continues to require an exact CORS_ALLOWED_ORIGIN match.
Next.js automatically loads .env.local and .env (and env-specific files like .env.production).
So a production build will pick up .env.local if it exists in the project root.


