Deploy a bridgeable ERC20 token on your Rayls Privacy Node and transfer it to a public chain.
Each team gets a dedicated Privacy Node — a sovereign, gasless EVM chain where only you can see what's deployed. Your mission: take an asset from invisible to tradeable, with AI agents governing every step of the reveal.
The five-phase flow:
- Design Privately — Deploy rich, complex assets on your Privacy Node. Full metadata, access rules, private balances. Nobody sees anything yet.
- AI Attestation — An AI agent inspects your asset and produces a signed proof of existence, posted to the public chain without exposing private details.
- Governance & Approval — Submit for institutional disclosure review. An AI compliance agent evaluates the asset and produces a structured recommendation.
- Bridge & Simplify — Once approved, your complex private asset crosses to the Public L1 in a simplified, tradeable form. You decide what gets revealed.
- List & Trade — Deploy a marketplace on the Public L1. Your once-invisible asset is now publicly discoverable — with full AI-attested provenance.
This starter repo covers the core building block: deploying a bridgeable token on your Privacy Node and teleporting it to the public chain (phases 1 and 4). You'll extend it with AI agents, governance logic, and a marketplace to complete your project.
See Architecture Overview for how the bridge and contracts work under the hood.
- Foundry installed (
forge,cast) - Node.js 18+ installed (for
npm) curlandjqfor API calls- Backend URL + API keys (provided by hackathon organizers)
git clone <this-repo>
cd rayls-hackathon-starter
forge install
npm install
cp .env.example .envEdit .env with the values provided by hackathon organizers:
PRIVACY_NODE_RPC_URL=<provided>
DEPLOYMENT_PROXY_REGISTRY=<provided>
PUBLIC_CHAIN_RPC_URL=<provided>
PUBLIC_CHAIN_ID=<provided>
BACKEND_URL=<provided>
USER_AUTH_KEY=<provided>
OPERATOR_AUTH_KEY=<provided>
DEPLOYER_PRIVATE_KEY=<your Foundry wallet key — generate with: cast wallet new>
Then load it:
source .envRe-run
source .envwhenever you update the file.
Create your identity and get wallet addresses for both chains.
curl -X POST "$BACKEND_URL/api/user/onboarding" \
-H "Authorization: Bearer $USER_AUTH_KEY" \
-H "Content-Type: application/json" \
-d '{"external_user_id": "your-unique-id"}' | jqReplace
"your-unique-id"with any unique string (e.g. your team name). Use the same ID in Step 2.
Response:
{
"public_chain_address": "0xAAAA...",
"private_chain_address": "0xBBBB...",
"public_chain_private_key": "0x...",
"private_chain_private_key": "0x...",
"status": 0,
"created_at": "..."
}Save all values from this response — you'll need them in Step 3.
Approve your address pair so you can bridge tokens.
curl -X PATCH "$BACKEND_URL/api/operator/onboarding/status" \
-H "Authorization: Bearer $OPERATOR_AUTH_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_user_id": "your-unique-id",
"public_address": "<public_chain_address>",
"private_address": "<private_chain_address>",
"new_status": 1
}'A successful response returns HTTP 200 with an empty body.
Update .env with the values from Step 1's response, then reload:
REGISTERED_PRIVATE_KEY=<private_chain_private_key from response>
MINT_RECIPIENT=<private_chain_address from response>
TRANSFER_TO=<public_chain_address from response>
TOKEN_NAME="My Token"
TOKEN_SYMBOL="MTK"
source .envBefore deploying: Make sure
DEPLOYER_PRIVATE_KEYis set in your.env. Generate one withcast wallet newand copy the private key. Also make sureTOKEN_NAMEandTOKEN_SYMBOLare unique — if a token with the same symbol was already deployed, the transaction will revert.
forge script script/Deploy.s.sol --rpc-url $PRIVACY_NODE_RPC_URL --broadcast --legacyThe script discovers infrastructure addresses from the on-chain registry and deploys your token. Copy the deployed address and set it in your .env:
TOKEN_ADDRESS=<deployed address from output>
Then reload:
source .envRegister the deployed token with the governance system.
curl -X POST "$BACKEND_URL/api/user/tokens" \
-H "Authorization: Bearer $USER_AUTH_KEY" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"$TOKEN_NAME\",
\"symbol\": \"$TOKEN_SYMBOL\",
\"address\": \"$TOKEN_ADDRESS\",
\"uri\": \"\",
\"standard\": 1
}" | jq
standard:1= ERC20,2= ERC721,3= ERC1155
Activate the token. This triggers the relayer to deploy a mirror contract on the public chain.
curl -X PATCH "$BACKEND_URL/api/operator/tokens/status" \
-H "Authorization: Bearer $OPERATOR_AUTH_KEY" \
-H "Content-Type: application/json" \
-d "{\"address\": \"$TOKEN_ADDRESS\", \"status\": 1}"A successful response returns HTTP 200 with an empty body.
After approval, the relayer automatically:
- Deploys a
PublicChainERC20mirror on the public chain - Maps the private → public token addresses
- Authorizes the mirror as a sender
This takes ~30-60 seconds. You can verify by running CheckBalance — if it prints a mirror address instead of reverting, the mirror is deployed:
forge script script/CheckBalance.s.sol --rpc-url $PRIVACY_NODE_RPC_URLMint tokens to your registered private-chain address (the one from Step 1).
forge script script/Mint.s.sol --rpc-url $PRIVACY_NODE_RPC_URL --broadcast --legacyThis calls mint() (onlyOwner) on your token, sending tokens to MINT_RECIPIENT.
Bridge tokens from the Privacy Node to the public chain.
forge script script/Transfer.s.sol --rpc-url $PRIVACY_NODE_RPC_URL --broadcast --legacyThis calls teleportToPublicChain() which:
- Locks tokens on the Privacy Node
- Dispatches a cross-chain message
- The relayer picks it up and submits to the public chain
- The mirror contract mints tokens for you on the public chain
Note: This script signs with
REGISTERED_PRIVATE_KEY(your registered private-chain address), notDEPLOYER_PRIVATE_KEY. TheonlyRegisteredUsersmodifier requiresmsg.senderto be an approved user.
Check your balance on the public chain mirror contract. Your tokens should appear within a minute after the transfer.
forge script script/CheckBalance.s.sol --rpc-url $PRIVACY_NODE_RPC_URLThis script automatically discovers the mirror contract address from the on-chain governance registry, then queries the public chain for your balance.
The same flow works for NFTs and multi-tokens. Use the matching scripts and set standard accordingly when registering via API.
| Standard | Contract | Deploy Script | Mint Script | Transfer Script | API standard |
|---|---|---|---|---|---|
| ERC20 | HackathonToken.sol |
Deploy.s.sol |
Mint.s.sol |
Transfer.s.sol |
1 |
| ERC721 | HackathonNFT.sol |
DeployNFT.s.sol |
MintNFT.s.sol |
TransferNFT.s.sol |
2 |
| ERC1155 | HackathonMultiToken.sol |
DeployMultiToken.s.sol |
MintMultiToken.s.sol |
TransferMultiToken.s.sol |
3 |
Configure the matching env vars (see .env.example for NFT_* and MULTI_TOKEN_* sections), then follow Steps 4-10 using the corresponding scripts.
| Key | Used By | Purpose |
|---|---|---|
DEPLOYER_PRIVATE_KEY |
Deploy.s.sol, Mint.s.sol | Your Foundry wallet. Owns the token contract. Can mint/burn. |
REGISTERED_PRIVATE_KEY |
Transfer.s.sol | Your registered private-chain address (from onboarding). Required by onlyRegisteredUsers modifier. |
Choose one of the three challenges:
Tokenize a real-world asset (bond, invoice, fund share) privately on your Privacy Node. An AI compliance agent reviews it before it goes public. Bridge a clean receipt token to a marketplace.
- AI Role: Governance compliance reviewer
- Key contracts: Your token + a governance contract that accepts/rejects AI recommendations
Mint a high-value digital asset with fully private metadata. An AI oracle attests its existence on-chain. The asset reveals itself to buyers only after purchase.
- AI Role: Cross-chain attestation oracle
- Key contracts: Your token + an attestation contract on the public chain
Build an AI agent that runs an entire institutional treasury: detecting assets, attesting them, submitting governance, bridging, and listing — autonomously. Humans watch; the agent operates.
- AI Role: Full autonomous orchestration
- Key contracts: Your token + the full five-phase pipeline scripted end-to-end
The agent/ directory contains a minimal TypeScript example showing how to connect an AI to on-chain interactions. It demonstrates one pattern in three steps:
- Read — connects to the public chain via ethers.js and reads a bridged token's name, symbol, and supply
- Analyze — sends the token data to an LLM and gets back a structured verdict (approved/rejected, score, reasoning)
- Write — posts the AI's verdict as an on-chain attestation by calling
Attestation.sol
cd agent && npm install && cp .env.example .env
# Fill in your values, then:
npm startSupports Google Gemini (free tier — no credit card), Anthropic (Claude), OpenAI, and OpenRouter. Set AI_PROVIDER in agent/.env.
OpenRouter gives access to 300+ models through a single API key — including several free models ideal for hackathons:
| Model | OPENROUTER_MODEL value |
|---|---|
| Meta Llama 3.3 70B (free) | meta-llama/llama-3.3-70b-instruct:free |
| DeepSeek R1 (free) | deepseek/deepseek-r1:free |
| Mistral 7B (free) | mistralai/mistral-7b-instruct:free |
| Gemma 3 27B (free) | google/gemma-3-27b-it:free |
Get your free API key at openrouter.ai/keys, then set in agent/.env:
AI_PROVIDER=openrouter
OPENROUTER_API_KEY=sk-or-...
OPENROUTER_MODEL=meta-llama/llama-3.3-70b-instruct:free
This is one pattern — adapt the read/analyze/write steps to your challenge track. The repo also includes Attestation.sol and DeployPublic.s.sol as optional building blocks.
The repo includes Marketplace.sol — an escrow contract you deploy on the public chain to sell your bridged tokens. You list tokens, set prices, and anyone can buy with USDR.
Your public chain address needs gas (USDR) on the public chain to deploy the marketplace and create listings. The Privacy Node is gasless, but the public chain is not. Ask organizers for a funded wallet or use a faucet.
You also need the public chain private key from your onboarding response (Step 1). Save it — you'll need it for all public chain interactions.
DEPLOYER_PRIVATE_KEY=<your public_chain_private_key> \
forge script script/DeployMarketplace.s.sol --rpc-url $PUBLIC_CHAIN_RPC_URL --broadcast --legacyNote: this uses your public chain key (not the Privacy Node deployer key), because the marketplace lives on the public chain and you need gas there.
Save the address:
MARKETPLACE_ADDRESS=<from output>
After bridging tokens to the public chain (Steps 1-10), you can list them on your marketplace.
1. Approve the marketplace to spend your tokens:
cast send <PUBLIC_MIRROR_ADDRESS> \
"approve(address,uint256)" \
<MARKETPLACE_ADDRESS> <AMOUNT> \
--rpc-url $PUBLIC_CHAIN_RPC_URL \
--private-key <PUBLIC_CHAIN_PRIVATE_KEY> \
--legacy2. List — tokens are transferred into the marketplace escrow:
# ERC20: assetType=0, tokenId=0
cast send <MARKETPLACE_ADDRESS> \
"list(address,uint8,uint256,uint256,uint256)" \
<PUBLIC_MIRROR_ADDRESS> 0 0 <AMOUNT> <PRICE> \
--rpc-url $PUBLIC_CHAIN_RPC_URL \
--private-key <PUBLIC_CHAIN_PRIVATE_KEY> \
--legacy# ERC721: assetType=1, amount is ignored
cast send <MARKETPLACE_ADDRESS> \
"list(address,uint8,uint256,uint256,uint256)" \
<PUBLIC_MIRROR_ADDRESS> 1 <TOKEN_ID> 1 <PRICE> \
--rpc-url $PUBLIC_CHAIN_RPC_URL \
--private-key <PUBLIC_CHAIN_PRIVATE_KEY> \
--legacyAnyone can buy a listed asset by sending USDR:
cast send <MARKETPLACE_ADDRESS> \
"buy(uint256)" <LISTING_ID> \
--value <PRICE> \
--rpc-url $PUBLIC_CHAIN_RPC_URL \
--private-key <BUYER_PRIVATE_KEY> \
--legacy# Update price
cast send <MARKETPLACE_ADDRESS> "update(uint256,uint256)" <LISTING_ID> <NEW_PRICE_WEI> \
--rpc-url $PUBLIC_CHAIN_RPC_URL --private-key <PUBLIC_CHAIN_PRIVATE_KEY> --legacy
# Delist (returns tokens to you)
cast send <MARKETPLACE_ADDRESS> "delist(uint256)" <LISTING_ID> \
--rpc-url $PUBLIC_CHAIN_RPC_URL --private-key <PUBLIC_CHAIN_PRIVATE_KEY> --legacy
# View a listing
cast call <MARKETPLACE_ADDRESS> "getListing(uint256)" <LISTING_ID> --rpc-url $PUBLIC_CHAIN_RPC_URL
# View all active listings
cast call <MARKETPLACE_ADDRESS> "getActiveListings()" --rpc-url $PUBLIC_CHAIN_RPC_URLEdit src/HackathonToken.sol — there are commented-out examples for:
- Custom decimals — e.g. 6 decimals for stablecoins
- Receive validation — add custom checks on incoming public chain transfers
- AccessControl — role-based mint/burn permissions
To return tokens from the public chain to your Privacy Node, call teleportToPrivacyNode() on the mirror contract. You'll need the public chain RPC and your public chain private key (from the onboarding response).
ERC20:
cast send <PUBLIC_MIRROR_ADDRESS> \
"teleportToPrivacyNode(address,uint256,uint256)" \
<YOUR_PRIVATE_CHAIN_ADDRESS> <AMOUNT> <PRIVACY_NODE_CHAIN_ID> \
--rpc-url $PUBLIC_CHAIN_RPC_URL \
--private-key <PUBLIC_CHAIN_PRIVATE_KEY> \
--legacyERC721:
cast send <PUBLIC_MIRROR_ADDRESS> \
"teleportToPrivacyNode(address,uint256,uint256)" \
<YOUR_PRIVATE_CHAIN_ADDRESS> <TOKEN_ID> <PRIVACY_NODE_CHAIN_ID> \
--rpc-url $PUBLIC_CHAIN_RPC_URL \
--private-key <PUBLIC_CHAIN_PRIVATE_KEY> \
--legacyERC1155:
cast send <PUBLIC_MIRROR_ADDRESS> \
"teleportToPrivacyNode(address,uint256,uint256,uint256)" \
<YOUR_PRIVATE_CHAIN_ADDRESS> <TOKEN_ID> <AMOUNT> <PRIVACY_NODE_CHAIN_ID> \
--rpc-url $PUBLIC_CHAIN_RPC_URL \
--private-key <PUBLIC_CHAIN_PRIVATE_KEY> \
--legacyThe mirror address is shown in the CheckBalance.s.sol output, or query it via the governance API.
| Error | Cause | Fix |
|---|---|---|
User not registered |
Your address isn't approved in RNUserGovernanceV1 | Complete Steps 1-2 |
Token not found or reverts on transfer |
Token not registered/activated | Complete Steps 5-6 and wait for mirror deployment |
Ownable: caller is not the owner |
Wrong key used for mint | Use DEPLOYER_PRIVATE_KEY for Mint.s.sol |
| Transfer succeeds but no tokens on public chain | Relayer hasn't processed yet | Wait ~60 seconds, then check again |
Failed to get EIP-1559 fees |
Rayls Privacy Nodes are gasless and don't support EIP-1559 | Add --legacy flag to your forge script command (already included in all commands above) |
| Token approval returns HTTP 500 | Backend needs a moment after token registration | Retry the same curl command after 5 seconds |
Mirror address returns 0x0000...0000 |
Relayer hasn't deployed the mirror yet | Wait 30-60 seconds after token approval |
| Token register says "not a valid deployed contract" | TOKEN_ADDRESS not updated in .env after deploy |
Set TOKEN_ADDRESS=<address from Step 4 output> and run source .env |
| Redeploy reverts with "execution reverted" | Token with the same symbol already registered | Change TOKEN_SYMBOL in .env to a unique value and redeploy |
Inspect a failed transaction:
cast run <TX_HASH> --rpc-url $PRIVACY_NODE_RPC_URLCheck if the mirror contract is deployed:
# Get the TokenGovernance address
TOKEN_GOV=$(cast call $DEPLOYMENT_PROXY_REGISTRY "getContract(string)(address)" "RNTokenGovernance" --rpc-url $PRIVACY_NODE_RPC_URL)
# Query the public mirror address
cast call $TOKEN_GOV "getPublicAddressByPrivateAddress(address)(address)" $TOKEN_ADDRESS --rpc-url $PRIVACY_NODE_RPC_URLCheck your balances:
# Balance on Privacy Node
cast call $TOKEN_ADDRESS "balanceOf(address)(uint256)" <YOUR_ADDRESS> --rpc-url $PRIVACY_NODE_RPC_URL
# Locked amount (tokens currently bridged to public)
cast call $TOKEN_ADDRESS "getLockedAmount(address)(uint256)" <YOUR_ADDRESS> --rpc-url $PRIVACY_NODE_RPC_URL- Architecture Overview — how the bridge works
- API Reference — complete backend endpoint documentation