A fully on-chain, privacy-preserving identity attestation system using the Self protocol and Human appchain with true cross-chain architecture powered by Spire's Pylon.
This project includes a unified frontend application:
frontend: A unified frontend that combines the complete flow from attestation to NFT minting with automatic network switching and a single flow from wallet connection → attestation → NFT minting.
- Connect Wallet: Connect your wallet to start the flow
- Generate Signature: Sign a message to prove address ownership
- Scan QR Code: Use Self app to scan passport and generate proof
- Automatic Submission: Self automatically submits proof to ProofOfHuman contract on Celo mainnet
- ProofOfHuman: Binds your wallet address to your passport without revealing passport details
- Switch to Human appchain: The app automatically switches to Human appchain
- Claim NFT: Mint the "I am human" NFT on Human appchain
- Cross-Chain Verification: The HumanNFT contract on Human appchain reads your attestation from Celo via a cross-chain synchronous read call
This system enables:
- Users to Maintain privacy - only cryptographic commitments and ZK proofs are stored on-chain
- App devs to defend against sybil attacks - NFTs can only be claimed once per passport
- Celo to act as a user identity hub - Celo's network effects expand reach for identity-based applications
- True cross-chain composability - Human appchain reads Celo state synchronously via Pylon
graph TD
U[User]:::userLayer
FW[frontend]:::serviceLayer
SA[Self Mobile App]:::externalLayer
subgraph Celo["Celo Mainnet"]
SH[Self Hub Contract]:::contractLayer
PH[ProofOfHuman Contract]:::contractLayer
end
subgraph Pylon["Human Appchain"]
SP[Settlement Port]:::pylonLayer
PROXY[SettlementForwardingProxy]:::pylonLayer
HN[HumanNFT Contract]:::contractLayer
end
U -->|1. Connect & Sign| FW
U -->|2. Scan QR Code| SA
SA -->|3. Submit ZK Proof| SH
SH -->|4. Verify & Store| PH
U -->|5. Click Mint| FW
FW -->|6. Mint Request| HN
HN -->|7. Read Attestation| PROXY
PROXY -->|8. Settlement Read| SP
SP -.->|9. Cross-chain Read| PH
PH -.->|10. Return Data| SP
SP -->|11. Return Result| PROXY
PROXY -->|12. Verify & Mint| HN
%% Styling
classDef userLayer fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#0d47a1
classDef serviceLayer fill:#fff8e1,stroke:#f57f17,stroke-width:2px,color:#e65100
classDef externalLayer fill:#fce4ec,stroke:#c2185b,stroke-width:2px,color:#880e4f
classDef contractLayer fill:#e8f5e8,stroke:#388e3c,stroke-width:2px,color:#1b5e20
classDef pylonLayer fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#4a148c
If you want to use our existing deployment instead of deploying your own contracts:
# Install dependencies
pnpm install
# Set environment variables for existing deployment
export SELF_HUB_ADDRESS=0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF
export SELF_CONFIG_ID=0x7baf7f25b3fe0f6eacb06d67e319140996ad4dd54f00529abf3fea5095f06b72
export SELF_SCOPE=18357982425819932074273780827128310208012362272222002103953286134929761147025
export NEXT_PUBLIC_SELF_SCOPE="self-pylon-demo"
# Celo mainnet configuration
export CELO_RPC_URL=https://forno.celo.org
export CELO_CHAIN_ID=42220
export PROOF_OF_HUMAN_ADDRESS=0x5E05a5CCf9fe3EC0a4b602A56381D685D0f711a8
# Human appchain configuration
export PYLON_RPC_URL=https://pylon.celo-mainnet.spire.dev/v1/chain/2139/rpc
export PYLON_CHAIN_ID=2139
export PYLON_SETTLEMENT_PORT=0x0000000000000000000000000000000000000042
# Celo contract address
# export HUMAN_NFT_ADDRESS=0xE95515970B457130B5D891666e02ABBA49c84448
# Human appchain contract addresses
export SETTLEMENT_FORWARDING_PROXY=0x5234cc99A4197525b8550E17d02b25F0D00D10B9
export HUMAN_NFT_ADDRESS=0xF54a6f384d88afB9c9b48fa9979BBdf445B8eC6D
# Configure frontend
cat > frontend/.env.local << EOF
NEXT_PUBLIC_CELO_RPC_URL=$CELO_RPC_URL
NEXT_PUBLIC_CELO_CHAIN_ID=$CELO_CHAIN_ID
NEXT_PUBLIC_PROOF_OF_HUMAN_ADDRESS=$PROOF_OF_HUMAN_ADDRESS
NEXT_PUBLIC_SELF_SCOPE=$NEXT_PUBLIC_SELF_SCOPE
NEXT_PUBLIC_PYLON_RPC_URL=$PYLON_RPC_URL
NEXT_PUBLIC_PYLON_CHAIN_ID=$PYLON_CHAIN_ID
NEXT_PUBLIC_HUMAN_NFT_ADDRESS=$HUMAN_NFT_ADDRESS
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_walletconnect_project_id
EOF
# Replace 'your_walletconnect_project_id' with your actual WalletConnect project ID
# Get your project ID from https://cloud.walletconnect.com/
# Start frontend
pnpm --filter frontend devNote: The existing deployment uses the cross-chain architecture where attestations are on Celo and claims happen on Human appchain via Pylon.
# Install dependencies
pnpm install
# Generate deployer private key
export SIGNER_PRIVATE_KEY=$(cast wallet new | grep "Private key:" | cut -d: -f2 | tr -d ' ')
# Get the public address from the private key
export SIGNER_ADDRESS=$(cast wallet address --private-key $SIGNER_PRIVATE_KEY)
echo "Private key: $SIGNER_PRIVATE_KEY"
echo "Address: $SIGNER_ADDRESS"
# Ensure account has funds for deployment gas fees# Celo mainnet - where attestations are stored
export CELO_RPC_URL="https://forno.celo.org"
export CELO_CHAIN_ID=42220
# Human appchain - where claims happen
export PYLON_RPC_URL="https://pylon.celo-mainnet.spire.dev/v1/chain/2139/rpc"
export PYLON_CHAIN_ID=2139
# Pylon settlement port - enables cross-chain reads from Celo
export PYLON_SETTLEMENT_PORT="0x0000000000000000000000000000000000000042"
# Self Hub contract (official Self contract on Celo)
export SELF_HUB_ADDRESS=0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BFVerify Configuration:
# Check Celo connectivity
cast block-number --rpc-url $CELO_RPC_URL
# Check Human appchain connectivity and verify chain ID
cast block-number --rpc-url $PYLON_RPC_URL
cast chain-id --rpc-url $PYLON_RPC_URL # Should return: 2139
# Verify settlement port contract exists
cast code $PYLON_SETTLEMENT_PORT --rpc-url $PYLON_RPC_URL
# Verify Pylon is synced with Celo
curl -s https://pylon.celo-mainnet.spire.dev/_status/ready | jqSkip to section 3 if using our existing ProofOfHuman contract.
- Visit Self's configuration interface
- Set verification requirements:
- Minimum age: 18
- Enable OFAC 1 and OFAC 2 sanctions checks
- Submit to get your
configId - Generate a scope seed for frontend
The Self configuration tool will:
- Deploy a new configuration if one doesn't exist for your chosen settings
- Provide you with a
configId(bytes32) andscope(uint256) - Allow you to customize verification parameters through the UI
Note: If you prefer to deploy the configuration from the terminal instead of the UI, you can use the CLI instead.
# After using the Self configuration tool, set the generated values
# Replace these with the actual values from your Self configuration
export SELF_CONFIG_ID=<your_generated_config_id>
export NEXT_PUBLIC_SELF_SCOPE="<your_scope_seed>"
export SELF_SCOPE=<your_generated_scope_value>
# Example of what these might look like:
# export SELF_CONFIG_ID=0x7baf7f25b3fe0f6eacb06d67e319140996ad4dd54f00529abf3fea5095f06b72
# export NEXT_PUBLIC_SELF_SCOPE="my-custom-demo"
# export SELF_SCOPE=6477103330237602230352812949141264605456698243037569300666502678848111318328# Set verification configuration on Self's Hub contract on Celo
cast send $SELF_HUB_ADDRESS \
"setVerificationConfigV2((bool,uint256,bool,uint256[4],bool[3]))" \
"(true,18,false,[0,0,0,0],[true,true,false])" \
--rpc-url $CELO_RPC_URL \
--private-key $SIGNER_PRIVATE_KEY
# Parameter breakdown:
# config.olderThanEnabled: true (enable age verification)
# config.olderThan: 18 (minimum age requirement)
# config.forbiddenCountriesEnabled: false (disable country restrictions)
# config.forbiddenCountriesListPacked: [0,0,0,0] (no forbidden countries)
# config.ofacEnabled: [true,true,false] (enable OFAC 1 & 2, disable OFAC 3)# Deploy the contract on Celo mainnet
pushd contracts
forge script script/DeployHubRoot.s.sol:DeployHubRoot \
--rpc-url $CELO_RPC_URL \
--broadcast \
--private-key $SIGNER_PRIVATE_KEY
popd
# Get the deployed address
latest=$(find contracts/broadcast/DeployHubRoot.s.sol/$CELO_CHAIN_ID -name "run-latest.json" -type f)
lowercase_addr=$(jq -r '.transactions[] | select(.contractName=="ProofOfHuman") | .contractAddress' "$latest")
export PROOF_OF_HUMAN_ADDRESS=$(cast to-check-sum-address $lowercase_addr)
echo "ProofOfHuman deployed on Celo at: $PROOF_OF_HUMAN_ADDRESS"# Set the scope and configId on your ProofOfHuman contract on Celo
cast send $PROOF_OF_HUMAN_ADDRESS \
"setScope(uint256)" \
"$SELF_SCOPE" \
--rpc-url $CELO_RPC_URL \
--private-key $SIGNER_PRIVATE_KEY
cast send $PROOF_OF_HUMAN_ADDRESS \
"setConfigId(bytes32)" \
"$SELF_CONFIG_ID" \
--rpc-url $CELO_RPC_URL \
--private-key $SIGNER_PRIVATE_KEYSkip to section 4 if using our existing HumanNFT contract.
Important: This deployment creates both the SettlementForwardingProxy and HumanNFT on Human appchain. The proxy enables cross-chain reads from Celo via Pylon.
export PYLON_SETTLEMENT_PORT="0x0000000000000000000000000000000000000042"
# Deploy on Human appchain (deploys both SettlementForwardingProxy and HumanNFT)
pushd contracts
forge script script/DeployPylon.s.sol:DeployPylon \
--rpc-url $PYLON_RPC_URL \
--broadcast \
--private-key $SIGNER_PRIVATE_KEY
popd
# Get the deployed addresses
latest=$(find contracts/broadcast/DeployPylon.s.sol/$PYLON_CHAIN_ID -name "run-latest.json" -type f)
# Get SettlementForwardingProxy address
lowercase_proxy=$(jq -r '.transactions[] | select(.contractName=="SettlementForwardingProxy") | .contractAddress' "$latest")
export PROOF_OF_HUMAN_PROXY=$(cast to-check-sum-address $lowercase_proxy)
echo "SettlementForwardingProxy deployed on Human appchain at: $PROOF_OF_HUMAN_PROXY"
# Get HumanNFT address
lowercase_nft=$(jq -r '.transactions[] | select(.contractName=="HumanNFT") | .contractAddress' "$latest")
export HUMAN_NFT_ADDRESS=$(cast to-check-sum-address $lowercase_nft)
echo "HumanNFT deployed on Human appchain at: $HUMAN_NFT_ADDRESS"
echo ""
echo "✅ Cross-chain setup complete"'!'
echo " - Attestations are stored on Celo: ${PROOF_OF_HUMAN_ADDRESS}"
echo " - Claims happen on Human appchain: $HUMAN_NFT_ADDRESS"
echo " - Settlement proxy on Human appchain: $PROOF_OF_HUMAN_PROXY"The frontend requires a frontend/.env.local file with all environment variables before building or running. This file is created below:
# Configure frontend (complete flow in one app)
cat > frontend/.env.local << EOF
NEXT_PUBLIC_CELO_RPC_URL=$CELO_RPC_URL
NEXT_PUBLIC_CELO_CHAIN_ID=$CELO_CHAIN_ID
NEXT_PUBLIC_PROOF_OF_HUMAN_ADDRESS=$PROOF_OF_HUMAN_ADDRESS
NEXT_PUBLIC_SELF_SCOPE=$NEXT_PUBLIC_SELF_SCOPE
NEXT_PUBLIC_PYLON_RPC_URL=$PYLON_RPC_URL
NEXT_PUBLIC_PYLON_CHAIN_ID=$PYLON_CHAIN_ID
NEXT_PUBLIC_HUMAN_NFT_ADDRESS=$HUMAN_NFT_ADDRESS
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_walletconnect_project_id
EOF
# Replace 'your_walletconnect_project_id' with your actual WalletConnect project ID
# Get your project ID from https://cloud.walletconnect.com/
echo "✅ Frontend configured"'!'
echo " - frontend supports both Celo (chain $CELO_CHAIN_ID) and Human appchain (chain $PYLON_CHAIN_ID)"
echo " - Complete flow: attestation → NFT minting"
# Build the frontend
# Note: For deployment using GitHub Pages, use ./scripts/build-frontend.sh
pnpm --filter frontend buildEnsure frontend/.env.local is configured (see section 4) before running. By default, the app runs with an empty basePath (for custom domain deployment). To run locally:
# Start frontend (complete flow in one app)
# Access at: http://localhost:3000/
pnpm --filter frontend devCustom Base Path
To run with a basePath (e.g., for testing repository path deployment):
# Set custom basePath (must start with /)
export NEXT_PUBLIC_BASE_PATH="/self-pylon-demo"
# Start the dev server
# Access at: http://localhost:3000/self-pylon-demo/
pnpm --filter frontend devImportant: When running locally with a basePath, you must access the app at http://localhost:PORT/basePath/ (not at the root). All assets and routes will be prefixed with the basePath. If basePath is empty, access the app at the root URL.
Before building, ensure frontend/.env.local is configured with all required environment variables. You can copy frontend/.env.local.example as a template. The app will not function correctly without these values.
-
Build the static site (default basePath is empty for custom domain deployment):
cd .. ./scripts/build-frontend.shFor Custom Domain (e.g.,
human.spire.dev):GITHUB_PAGES_CUSTOM_DOMAIN="human.spire.dev" ./scripts/build-frontend.sh- The build script will automatically create a
CNAMEfile in thedocs/folder with your custom domain - The default build uses an empty basePath, which is correct for custom domains
- After building, configure your custom domain in GitHub Pages settings
- Set up DNS CNAME record pointing your subdomain to your GitHub Pages domain
- Your site will be available at
https://human.spire.dev/
For Repository Path Deployment (e.g.,
username.github.io/self-pylon-demo/):NEXT_PUBLIC_BASE_PATH="/self-pylon-demo" ./scripts/build-frontend.shThe
NEXT_PUBLIC_BASE_PATHenvironment variable controls the basePath used in production. If not set, it defaults to empty (for custom domain deployment). - The build script will automatically create a
-
Commit and push:
git add docs/ git commit -m "Deploy: Update frontend to GitHub Pages" git push origin main -
Enable GitHub Pages in your repo settings:
- Go to Settings → Pages
- Source: "Deploy from a branch"
- Branch:
main - Folder:
/docs - For custom domain: Enter your custom domain (e.g.,
human.spire.dev) in the "Custom domain" field
Your unified app will be available at:
https://your-custom-domain.com/(if using custom domain with empty basePath)https://yourusername.github.io/self-pylon-demo/(if using repository path with basePath set)
Note:
- The default basePath is empty, which is suitable for custom domain deployment (e.g.,
human.spire.dev) - To use a repository path, set
NEXT_PUBLIC_BASE_PATHto match your repository name - The basePath is configured in
frontend/next.config.mjsand can be overridden with theNEXT_PUBLIC_BASE_PATHenvironment variable
This demo uses Pylon for synchronous cross-chain reads:
- ProofOfHuman on Celo: Stores attestations on Celo mainnet
- SettlementForwardingProxy on Human appchain: Deployed on Human appchain, forwards calls to Settlement Port
- Settlement Port on Human appchain: Fixed address
0x0000000000000000000000000000000000000042- reads state from Celo synchronously via Pylon - HumanNFT on Human appchain: Uses the proxy to verify attestations during minting
From the HumanNFT contract's perspective, it calls a local contract (the proxy), but the data is actually being read from Celo synchronously.
Human Appchain Status: You can verify the Human appchain is operational at https://pylon.celo-mainnet.spire.dev/_status/ready
# ❌ Wrong - lowercase address from Foundry
export PROOF_OF_HUMAN_ADDRESS=0xf3d2672c6321311e4e7606fb081e59a08c43abad
# ✅ Correct - checksummed address for Self
export PROOF_OF_HUMAN_ADDRESS=$(cast to-check-sum-address 0xf3d2672c6321311e4e7606fb081e59a08c43abad)Symptoms: ScopeMismatch: scope in header doesn't match scope in proof
Solution: Ensure you have set the Scope on the ProofOfHuman contract and that the casing matches between the frontend and contract deployment.
If minting fails, check:
- Attestation completed on Celo: Complete the attestation process using frontend first
- Connected to correct network: Ensure your wallet is connected to Human appchain (chain ID 2139)
- Settlement proxy deployed: The SettlementForwardingProxy must be deployed on Human appchain
- Cross-chain read working: Pylon must be able to read from Celo
Debug Steps:
# Check environment variables
echo "HumanNFT Address (Human appchain): $HUMAN_NFT_ADDRESS"
echo "ProofOfHuman Address (Celo): $PROOF_OF_HUMAN_ADDRESS"
echo "Human appchain RPC: $PYLON_RPC_URL"
echo "Celo RPC: $CELO_RPC_URL"
# Verify contracts exist
echo "Checking HumanNFT on Human appchain..."
cast code $HUMAN_NFT_ADDRESS --rpc-url $PYLON_RPC_URL
echo "Checking ProofOfHuman on Celo..."
cast code $PROOF_OF_HUMAN_ADDRESS --rpc-url $CELO_RPC_URL
# Check if address is attested on Celo
cast call $PROOF_OF_HUMAN_ADDRESS \
"addressToNullifier(address)(uint256)" \
<your_address> \
--rpc-url $CELO_RPC_URLSolutions:
- Complete attestation on Celo: Use the frontend to generate and submit proof on Celo before attempting to mint
- Verify network connection: Ensure your wallet is connected to Human appchain (chain ID 2139)
- Check contract deployment: Verify SettlementForwardingProxy and HumanNFT are deployed on Human appchain
- Verify cross-chain configuration: Ensure
PYLON_SETTLEMENT_PORTis set to0x0000000000000000000000000000000000000042and ProofOfHuman address is correctly configured in the proxy
If the cross-chain read fails:
- Settlement Port address: Verify it's set to
0x0000000000000000000000000000000000000042(fixed address on all Human appchains) - Network connectivity: Ensure both Celo and Human appchain RPC endpoints are accessible
- Contract configuration: Verify ProofOfHuman address is correctly set in the SettlementForwardingProxy
- Human appchain status: Check https://pylon.celo-mainnet.spire.dev/_status/ready to verify Human appchain is operational