A Farcaster Frames-enabled NFT marketplace built on a Polkadot parachain.
This project demonstrates how to integrate Farcaster Frames with a Polkadot parachain for social-driven NFT minting.
This project showcases a complete NFT marketplace built on a Polkadot parachain with Farcaster Frames v2 Mini App integration. The marketplace allows:
- 🖼️ Artists to create and manage NFT collections
- 🔄 Users to mint NFTs directly from Farcaster Frames
- 🔒 Social-gated minting that requires users to like and recast the original cast
- 🌐 Seamless integration between Farcaster's social layer and Polkadot's blockchain infrastructure
The project consists of:
- 💿 Parachain Node: A Polkadot SDK-based parachain with custom pallets for NFT functionality
- 🧮 Runtime: Core blockchain logic including the NFT pallet
- 🎨 Frontend: Next.js application with Farcaster Frames Mini app
- 🔌 Farcaster Integration: API routes and utilities for verifying user engagement
We have used the polkadot-sdk-parachain-template for this demo app. This section provides instructions for building and running the parachain locally.
- Rust
- openssl
- cmake
- llvm
- protobuf
# Build the node with release optimizations
cargo build --release
The build process takes some time. When complete, the binary will be available at ./target/release/parachain-template-node
.
You can start a development chain with:
./target/release/parachain-template-node --dev
This command:
- Runs a temporary node in development mode
- Purges the development chain state on exit
- Uses Alice and Bob as pre-funded development accounts
- Has no external connections to other networks
Additional useful flags:
--alice
: Includes the pre-defined Alice key in the node's keystore--tmp
: Runs a temporary node (state is not saved to disk)-d <path>
: Specifies the directory for storing chain data
Once your node is running, you can interact with it using:
-
Polkadot.js Apps UI
(Pre-configured for a local node on default ports) -
Command-line tools (using
curl
to make RPC calls)
When making changes to the runtime:
- Edit the runtime code
- Rebuild the runtime:
cargo build --release -p parachain-template-runtime
- Restart your node to apply the changes
For testing as a parachain:
- Build the Polkadot relay chain:
cargo build --release
- Follow the Cumulus Workshop for connecting your parachain to a local relay chain
The project supports integration with both Polkadot.js Extension and WalletConnect for seamless user authentication and transaction signing. However, the web extenssion support is not available in Farcaster Frames mini app, so user need to use the Walletconnect only to connect to wallets like Subwallet or Nova Wallet. Make sure you add your node rpc URL as custom network in these wallets.
This project uses the @polkadot-onboard
packages to provide a unified wallet connection experience. The setup is handled by the PolkadotProvider
component.
Check out frontend/src/components/PolkadotProvider.tsx
for full Polkadot <-> Walletconnect integration.
frontend/src/components/PolkadotProvider.tsx
// frontend/src/components/PolkadotProvider.tsx
export default function Provider({ children }: { children: React.ReactNode }) {
// Configure injected wallets (browser extensions)
// Configure WalletConnect
const walletConnectParams: WalletConnectConfiguration = {
projectId: process.env.NEXT_PUBLIC_APPKIT_PROJECT_ID!,
metadata: {
name: "Polkadot Demo",
description: "Polkadot Demo",
url: "",
icons: ["Wallet_Connect.svg"],
},
chainIds: [
"polkadot:e143f23803ac50e8f6f8e62695d1ce9e", // Rococo
"polkadot:91b171bb158e2d3848fa23a9f1c25182", // Polkadot
`polkadot:${process.env.NEXT_PUBLIC_LOCAL_NODE_CAPID!}`, // Local node
],
optionalChainIds: [
"polkadot:67f9723393ef76214df0118c34bbbd3d", // Westend
"polkadot:7c34d42fc815d392057c78b49f2755c7", // Kusama
],
onSessionDelete: () => {
// do something when session is removed
},
};
const walletConnectProvider = new WalletConnectProvider(
walletConnectParams,
process.env.NEXT_PUBLIC_APP_NAME!
);
// Combine all wallet providers
const walletAggregator = new WalletAggregator([
walletConnectProvider,
// Other Wallet Providers
// ...
]);
return (
<PolkadotWalletsContextProvider walletAggregator={walletAggregator}>
{children}
</PolkadotWalletsContextProvider>
);
}
Ensure these environment variables are set in your .env.local
file:
NEXT_PUBLIC_APP_NAME
: YourAppNameNEXT_PUBLIC_APPKIT_PROJECT_ID
: YourWalletConnectProjectIDNEXT_PUBLIC_LOCAL_NODE_CAPID
: YourLocalNodeChainId
To get a WalletConnect Project ID, register your application at WalletConnect Cloud.
The chainIds
array in the WalletConnect configuration specifies which networks your application supports. The format follows the CAIP-2 standard:
polkadot:{chain-id-in-hex}
Common Polkadot ecosystem chain IDs:
- Polkadot:
polkadot:91b171bb158e2d3848fa23a9f1c25182
- Kusama:
polkadot:7c34d42fc815d392057c78b49f2755c7
- Rococo:
polkadot:e143f23803ac50e8f6f8e62695d1ce9e
- Westend:
polkadot:67f9723393ef76214df0118c34bbbd3d
For your local node, you'll need to get the genesis hash and convert it to the CAIP format.
Farcaster Frames allows web applications to be embedded within the Farcaster client (like Warpcast), enabling interactive experiences directly in the social feed. This project demonstrates how to connect Farcaster Frames to a Polkadot parachain for NFT minting.
The integration starts in the frontend's layout component (frontend/src/app/layout.tsx
), where we define the Frame metadata:
frontend/src/app/layout.tsx
<head>
<meta
name="fc:frame"
content={JSON.stringify({
version: "next",
imageUrl: `${process.env.NEXT_PUBLIC_URL}/Intercaster.png`,
button: {
title: "Mint NFT in Polkadot",
action: {
type: "launch_frame",
name: "Mint NFT in Polkadot",
url: `${process.env.NEXT_PUBLIC_URL}`,
splashBackgroundColor: "#f5f5f5",
},
},
})}
/>
</head>
This metadata defines:
- The image displayed in the Farcaster client
- The button text and action (launching the full application)
- The URL to your deployed application
When a user interacts with your Frame, the application receives context about the user and their interaction:
frontend/src/app/page.tsx
// In frontend/src/app/page.tsx
useEffect(() => {
const load = async () => {
setIsLoading(true);
try {
await sdk.actions.ready();
const context = await sdk.context;
setUserContext(context as unknown as FrameContext);
if (context?.user?.fid) {
await verifyUser(context.user.fid as unknown as string);
}
} catch (error) {
console.error("Error loading user context:", error);
} finally {
setIsLoading(false);
}
};
load();
}, []);
The key feature of this application is that users can only mint NFTs if they have liked and recasted the original cast:
frontend/src/components/NFTMarketplace.tsx
// In frontend/src/components/NFTMarketplace.tsx
const checkEngagement = async () => {
if (!castHash || !fid) {
toast.error("Missing cast hash or FID");
return false;
}
try {
setIsCheckingEngagement(true);
const engagement = await checkUserEngagement(fid, castHash);
if (!engagement.hasLiked || !engagement.hasRecasted) {
toast.error("Please like and recast the post to mint NFT");
return false;
}
return true;
} catch (error) {
toast.error("Failed to check engagement with error: " + error);
return false;
} finally {
setIsCheckingEngagement(false);
}
};
const mintNft = async (collectionId: number, itemId: number) => {
// ...
try {
if (fid) {
await verifyUser(fid);
const isEngaged = await checkEngagement();
if (!isEngaged) {
setIsLoading(false);
return;
}
}
// Proceed with blockchain transaction
// ...
}
// ...
};
To connect your own Farcaster Frames to a Polkadot parachain:
We have used Neynar to interact with the Farcaster Ecosystem, the following API calls given the following files calls the third-party Neynar APIs to get the Frarcaster data. Check out the implementation in the follwing files:
frontend/src/app/api/verify-user/route.ts
to validate Farcaster user identityfrontend/src/app/api/check-engagement/route.ts
to verify likes and recastsfrontend/src/app/api/cast-details/route.ts
to fetch information about a cast
- Add the
fc:frame
meta tag to your application's head - Customize the button text and action to match your application
- Use the Farcaster Frame SDK to access user context
- Verify user identity and engagement before allowing NFT minting
- Use the Polkadot.js API to interact with your parachain
- Implement wallet connection and transaction signing
This section provides instructions for deploying the parachain in a Kubernetes environment.
- Kubernetes cluster (e.g., GKE, EKS, AKS, or local Minikube)
- Helm 3 installed
kubectl
configured to communicate with your cluster- Docker images for your parachain node
You must reserve a parachain identifier (ID) before registering your parachain on your relay chain. You'll be assigned the next available identifier.
To reserve a parachain identifier, follow these steps:
- Click on the Network tab in the top menu
- Select the Parachains option from the dropdown menu
- Select the Parathreads tab
- Click on the + ParaId button
4. After submitting the transaction, you can navigate to the Explorer tab and check the list of recent events for successful registrar.Reserved
-
Generate static node keys (aka network keys) Node keys are used to identify nodes on the P2P network with a unique PeerID. To ensure this identifier persists across restarts, it is highly recommended to generate a static network key for all nodes. This practice is particularly important for bootnodes, which have publicly listed addresses that are used by other nodes to bootstrap their connections to the network.
To generate a static node key:
docker run parity/subkey:latest generate-node-key
-
Generate keys for your collators (account+aura keys) For parachains using the collatorSelection pallet to manage their collator set, you will need to generate a set of keys for each collator:
- Collator Account: a regular substrate account
- Aura keys (part of the collator "session keys")
To perform this step, you can use subkey, a command-line tool for generating and managing keys:
docker run -it parity/subkey:latest generate --scheme sr25519
Save the following to genesis.patch.json (replace keys and configuration with your own):
genesis.patch.json
{
"balances":
{
"balances":
[
[
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
1152921504606846976,
],
[
"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
1152921504606846976,
],
],
},
"collatorSelection":
{
"candidacyBond": 16000000000,
"invulnerables":
[
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
],
},
"session":
{
"keys":
[
[
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
{
"aura": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
},
],
[
"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
{
"aura": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
},
],
],
},
"parachainInfo": { "parachainId": 4435 },
"polkadotXcm": { "safeXcmVersion": 4 },
"sudo": { "key": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" },
}
In this example:
- balances: initial account balances
- collatorSelection: configure the collatorSelection pallet - properties, in this example we set Alice and Bob as initial - invulnerable collators.
- session.keys: initial session keys
- parachainInfo.parachainId: parachain ID
- sudo.keys: initial sudo key account
Note that:
- 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY: Alice's account address
- 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty: Bob's account address
Generate the plain chain spec, make sure to change to your relay chain and parachin id
chain-spec-builder --chain-spec-path ./chain-spec/chainspec.plain.json create --chain-name "polkadot-nft-farcaster-frames" --chain-id polkadot-nft-farcaster-frames -t live --relay-chain paseo --para-id 4793 --runtime target/release/wbuild/polkadot-nft-farcaster-frames-runtime/polkadot_nft_farcaster_frames_runtime.compact.compressed.wasm patch ./chain-spec/genesis.patch.json
To initialize the genesis storage for your chain, you need convert your chainspec from plain to raw format. This process transforms the human-readable keys in the plain chainspec into actual storage keys and defines a unique genesis block.
chain-spec-builder --chain-spec-path ./chain-spec/chainspec.raw.json convert-to-raw ./chain-spec/chainspec.plain.json
-
Register parachain genesis code and state on relay-chain
- Export the genesis state:
./target/release/parachain-template-node export-genesis-state --chain ./chain-spec/chainspec.raw.json ./chain-spec/para-state
- Export the genesis runtime:
./target/release/parachain-template-node export-genesis-wasm --chain ./chain-spec/chainspec.raw.json ./chain-spec/para-wasm
- Register your parachain genesis configuration on the relay-chain by executing the registrar.register extrinsic on Paseo:
- id: your parachain ID
- genesisHead: select the
para-state
file - validationCode: select the
para-wasm
file
helm repo add parity https://paritytech.github.io/helm-charts/
The deployment uses Helm charts for managing the Kubernetes resources. Configuration files are located in the k8s
directory.
# Review the configuration before deployment
cat k8s/polkadot-node-deployment.yaml
Deploy the parachain node using the Parity Helm chart:
helm -n default upgrade --install polkadot-nft-parachain -f k8s/polkadot-node-deployment.yaml parity/node
Check that the pods are running correctly:
kubectl get pods -n default -l app=polkadot-nft-parachain
The deployment exposes RPC endpoints that can be accessed within the cluster or externally depending on your configuration:
# Get service details
kubectl get svc -n default -l app=polkadot-nft-parachain
Monitor the node logs for any issues:
kubectl logs -n default -l app=polkadot-nft-parachain -f
Execute extrinsic on your relay chain: onDemandAssignmentProvider.placeOrderAllowDeath
or onDemandAssignmentProvider.placeOrderKeepAlive
:
- maxAmount: 10000000000000 (13 zeros, ie. 10 ROC)
- paraId: your parachain ID
After executing this, you should have successfully produced your first block !
INFO tokio-runtime-worker substrate: [Parachain] ✨ Imported #1 (0xa075…10d6)
The k8s/polkadot-node-deployment.yaml
file contains various configuration options:
- Node type (validator, collator)
- Resource limits and requests
- Persistent storage configuration
- Network settings
- Chain specification
Modify these settings according to your deployment requirements.
To deploy the Farcaster Frames-enabled frontend:
cd frontend
yarn install
yarn build
NEXT_PUBLIC_URL
: URL for the Farcaster Frames Mini App, can be a vercel hosted URL.NEXT_PUBLIC_LOCAL_NODE_URL
: WebSocket URL of your parachain nodeNEXT_PUBLIC_LOCAL_NODE_CAPID
: CAIP for the WalletConnect as it uses chain ids based on the CAIP standardNEXT_PUBLIC_APP_NAME
: Name of your applicationNEXT_PUBLIC_APPKIT_PROJECT_ID
: Your WalletConnect project IDNEXT_PUBLIC_NEYNAR_API_URL
: We have used Neynar an an independent 3rd party provider that offers Farcaster dataNEYNAR_API_KEY
: Neynar API key to call REST APIs.
To properly publish your Farcaster Frame and make it discoverable in the Farcaster ecosystem, follow these steps:
Create a .well-known/farcaster.json
file in your public directory with the following structure:
{
"accountAssociation": {
"header": "YOUR_HEADER",
"payload": "YOUR_PAYLOAD",
"signature": "YOUR_SIGNATURE"
},
"frame": {
"name": "Polkadot NFT Demo",
"version": "1",
"iconUrl": "https://your-domain.com/logo.png",
"homeUrl": "https://your-domain.com/",
"buttonTitle": "Polkadot NFT",
"splashBackgroundColor": "#f5f5f5"
}
}
To generate the header
, payload
, and signature
values:
- Go to Farcaster Developer Hub
- Navigate to the "Domains" section
- Click "Add Domain" and follow the instructions to verify your domain
- Copy the generated values into your
farcaster.json
file
The frame
section in your farcaster.json
file defines how your Frame appears in Farcaster clients:
name
: The display name of your Frameversion
: The Frame version (currently "1")iconUrl
: URL to your Frame's icon (must be hosted on your domain)homeUrl
: The main URL of your FramebuttonTitle
: Text displayed on the Frame buttonsplashBackgroundColor
: Background color when loading your Frame
Before publishing, test your Frame using:
Once tested, you can publish your Frame by:
- Creating a cast with your Frame URL on Warpcast
- Sharing the cast with your audience
- Monitoring engagement through likes, recasts, and mints
-
🔄 This project welcomes contributions and suggestions.
-
😇 Please refer to the project's contribution guidelines and Code of Conduct.
-
🧑🏫 To learn about Polkadot in general, Polkadot.network website is a good starting point.
-
🧑🔧 For technical introduction, here are the Polkadot SDK documentation resources.
-
👥 For Farcaster Frames documentation, visit Farcaster's documentation.