A Privacy Node is a sovereign, gasless EVM blockchain where only authorized participants can see transactions and balances. A Public Chain is a standard blockchain where anyone can view activity. Rayls lets you issue and manage assets privately, then selectively bridge them to a public chain when you're ready to disclose.
This is the full hackathon journey — from invisible to tradeable:
Design Privately → AI Attestation → Governance & Approval → Bridge & Simplify → List & Trade
(Privacy Node) (AI agent) (AI + human review) (Rayls bridge) (Public L1)
| Phase | What Happens | Where |
|---|---|---|
| 1. Design Privately | Deploy tokens with rich metadata and access rules | Privacy Node |
| 2. AI Attestation | AI agent inspects the asset and posts a signed proof of existence | Public Chain |
| 3. Governance | AI compliance agent reviews; human approves disclosure | Privacy Node |
| 4. Bridge | teleportToPublicChain() locks privately, mints publicly |
Both chains |
| 5. List & Trade | Deploy a marketplace; bridged asset becomes tradeable | Public Chain |
This starter repo covers phases 1 and 4 — deploying a token and bridging it. You'll build the AI, governance, and marketplace layers on top.
The Rayls bridge connects a Privacy Node (where your token lives) to a Public Chain (where tokens become publicly visible). An off-chain relayer watches for events and shuttles messages between the two chains.
┌───────────────────────┐ ┌───────────────────────┐
│ PRIVACY NODE │ │ PUBLIC CHAIN │
│ │ │ │
│ Your Token │ ┌──────────────┐ │ Mirror Token │
│ (RaylsErc20Handler) │◄──►│ RELAYER │◄────►│ (PublicChainERC20) │
│ │ │ (Go svc) │ │ [auto-deployed] │
│ RNEndpointV1 │ └──────────────┘ │ PublicRNEndpointV1 │
│ RNTokenGovernanceV1 │ │ RNMessageExecutorV1 │
│ RNUserGovernanceV1 │ │ │
└───────────────────────┘ └───────────────────────┘
When you call teleportToPublicChain():
- Tokens are locked on the Privacy Node (transferred to the contract owner)
- A cross-chain message is dispatched (via
RNEndpointV1) - The relayer picks up the
MessageDispatchedevent - The relayer submits the message to the public chain
- The mirror contract mints new tokens for the recipient
Privacy Node Relayer Public Chain
│ │ │
│ teleportToPublicChain() │ │
│ ── lock tokens ──► │ │
│ ── dispatch message ──► │ │
│ │ picks up event │
│ │ ── submits message ──────►│
│ │ │ mint tokens
│ │ │ ──► recipient
When someone calls teleportToPrivacyNode() on the public chain mirror:
- Tokens are burned on the public chain
- Message dispatched → relayer picks up → submits to Privacy Node
- Previously locked tokens are unlocked and transferred to the recipient
Every cross-chain message includes a pre-generated revert payload. If the destination execution fails:
- The relayer detects the failure
- Submits the revert payload back to the source chain
- Locked tokens are unlocked (or burned tokens are minted back)
Tokens are never lost.
Write Contract ──► Deploy ──► Register ──► Approve ──► Mirror Deployed ──► Bridgeable
(you) (you) (you/API) (you/API) (relayer, auto) (ready!)
- Write: Inherit from
RaylsErc20Handler - Deploy: Deploy to the Privacy Node with Foundry
- Register: Call the backend API to add your token to
RNTokenGovernanceV1(status: INACTIVE) - Approve: Call the backend API to activate it (status: ACTIVE, emits
TokenActivated) - Mirror Deployed: The relayer detects
TokenActivated, deploys aPublicChainERC20on the public chain, and maps the addresses - Bridgeable:
teleportToPublicChain()now works end-to-end
You'll notice HackathonToken's constructor calls the internal _mint() rather than the public mint():
constructor(...) RaylsErc20Handler(...) {
_mint(msg.sender, 1_000_000 * 10 ** 18);
}This is intentional. The public mint() inherited from RaylsErc20Handler does two things:
- Mints tokens to the recipient
- Calls
_submitTokenUpdate(), which sends a cross-chain message to the commit chain'sTokenRegistryto report the supply change
At construction time, the token does not have a resourceId yet — that is assigned later when the token is registered and approved via the governance API. Without a resourceId, the cross-chain message would fail.
By using the internal _mint(), we skip the cross-chain notification and just update the local balance. Once the token is registered and has a resourceId, subsequent calls to mint() (the public function) will correctly notify the TokenRegistry.
Rule of thumb: Use _mint() in the constructor, use mint() everywhere else.
| Contract | Purpose |
|---|---|
RaylsErc20Handler |
Base class for your token. Provides teleport, mint, burn, lock/unlock. |
RNEndpointV1 |
Message gateway on the Privacy Node. Routes cross-chain messages. |
RNUserGovernanceV1 |
User registry. Controls who can call teleportToPublicChain. |
RNTokenGovernanceV1 |
Token registry. Maps private token addresses to public mirrors. |
PublicChainERC20 |
Auto-deployed mirror on the public chain. Burn/mint model. |
DeploymentProxyRegistryV1 |
On-chain registry to discover all infrastructure contract addresses. |
onlyRegisteredUsers: Only approved users inRNUserGovernanceV1can callteleportToPublicChain(). Passaddress(0)as userGovernance to disable this check.onlyOwner: Only the token owner (deployer) can callmint()andburn().receiveMethod: Only the trusted message executor can call receive functions (prevents unauthorized minting).
Your token inherits from RaylsErc20Handler. Here are the key functions you can call or override — no need to dig through the source:
| Function | Access | Description |
|---|---|---|
teleportToPublicChain(address to, uint256 value, uint256 chainId) |
onlyRegisteredUsers |
Lock tokens on Privacy Node, mint on Public Chain |
receiveTeleportFromPublicChain(address to, uint256 value) |
receiveMethod |
Unlock tokens when returning from Public Chain. Override this to add custom validation on incoming transfers. |
revertTeleportToPublicChain(address to, uint256 value) |
receiveMethod |
Called automatically by the relayer if the public-side mint fails. Unlocks tokens. |
| Function | Access | Description |
|---|---|---|
teleport(address to, uint256 value, uint256 chainId) |
public | Send tokens to another Privacy Node via the commit chain |
teleportAtomic(address to, uint256 value, uint256 chainId) |
public | Atomic cross-chain transfer with 240-second timeout. All-or-nothing. |
receiveTeleport(address to, uint256 value) |
receiveMethod |
Receive tokens from another Privacy Node. Override this for custom logic. |
| Function | Access | Description |
|---|---|---|
mint(address to, uint256 value) |
onlyOwner |
Mint tokens and notify the commit chain's TokenRegistry |
burn(address from, uint256 value) |
onlyOwner |
Burn tokens and notify the commit chain's TokenRegistry |
getLockedAmount(address account) |
view | Check how many tokens are currently locked (bridged to public chain) |
GetERCStandard() |
view | Returns the token standard enum (ERC20) |
For NFT-based projects (e.g., Confidential NFT Reveal challenge). Inherit from RaylsErc721Handler.
| Function | Access | Description |
|---|---|---|
teleportToPublicChain(address to, uint256 tokenId, uint256 chainId) |
onlyRegisteredUsers |
Lock NFT on Privacy Node, mint on Public Chain |
receiveTeleportFromPublicChain(address to, uint256 tokenId) |
receiveMethod |
Unlock NFT returning from Public Chain. Overridable. |
teleport(address to, uint256 tokenId, uint256 chainId) |
public | Send NFT to another Privacy Node |
teleportAtomic(address to, uint256 tokenId, uint256 chainId) |
public | Atomic cross-chain NFT transfer with timeout |
| Function | Access | Description |
|---|---|---|
mint(address to, uint256 tokenId) |
onlyOwner |
Mint NFT with specific token ID |
burn(uint256 tokenId) |
onlyOwner |
Burn NFT by token ID |
isTokenLocked(address owner, uint256 tokenId) |
view | Check if a specific NFT is locked (bridged) |
For multi-token projects. Inherit from RaylsErc1155Handler.
| Function | Access | Description |
|---|---|---|
teleportToPublicChain(address to, uint256 id, uint256 amount, uint256 chainId, bytes data) |
onlyRegisteredUsers |
Lock tokens on Privacy Node, mint on Public Chain |
receiveTeleportFromPublicChain(address to, uint256 id, uint256 amount) |
receiveMethod |
Unlock tokens returning from Public Chain. Overridable. |
teleport(address to, uint256 id, uint256 amount, uint256 chainId, bytes data) |
public | Send tokens to another Privacy Node |
teleportAtomic(address to, uint256 id, uint256 amount, uint256 chainId, bytes data) |
public | Atomic cross-chain transfer with timeout |
| Function | Access | Description |
|---|---|---|
mint(address to, uint256 id, uint256 amount, bytes data) |
onlyOwner |
Mint tokens with specific ID and amount |
burn(address from, uint256 id, uint256 amount) |
onlyOwner |
Burn tokens by ID and amount |
getLockedAmount(address account, uint256 id) |
view | Check locked amount for a specific token ID |