An automatic challenge for @pkcprotocol/pkc-js communities that verifies an author's EVM wallet address meets a condition from a smart contract call.
When an author publishes to a community with this challenge enabled, the community node calls a read-only smart contract method with the author's wallet address as the argument and compares the return value against a configured condition (e.g. >1000). The challenge tries three sources for the wallet address:
- Wallet address — the
author.wallets[chainTicker]address, verified via EIP-191 signature - ENS/BSO domain — if the author's address is a
.ethor.bsodomain, it resolves to an on-chain address - NFT avatar — the current owner of the author's avatar NFT
If any source produces a wallet that passes the contract call condition, the challenge succeeds. No user interaction is required.
- Node.js
>=22 - ESM-only environment
bitsocial challenge install @bitsocial/evm-contract-challengeEdit your community to use the challenge:
bitsocial community edit your-community.bso \
'--settings.challenges[0].name' @bitsocial/evm-contract-challenge \
'--settings.challenges[0].options.chainTicker' eth \
'--settings.challenges[0].options.address' '0xEA81DaB2e0EcBc6B5c4172DE4c22B6Ef6E55Bd8f' \
'--settings.challenges[0].options.abi' '{"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}' \
'--settings.challenges[0].options.condition' '>10000000000000000000' \
'--settings.challenges[0].options.error' 'You need at least 10 Bitsocial tokens to post.'If your RPC server is already running, first install the challenge on the server:
bitsocial challenge install @bitsocial/evm-contract-challengeThen from your RPC client, connect and set the challenge on your community by name — no npm install or challenge registration needed on the client side:
import PKC from "@pkcprotocol/pkc-js";
const pkc = await PKC({
pkcRpcClientsOptions: ["ws://localhost:9138"]
});
const community = await pkc.createCommunity({ address: "your-community-address.bso" });
await community.edit({
settings: {
challenges: [
{
name: "@bitsocial/evm-contract-challenge",
options: {
chainTicker: "eth",
address: "0xEA81DaB2e0EcBc6B5c4172DE4c22B6Ef6E55Bd8f",
abi: '{"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}',
condition: ">10000000000000000000",
error: "You need at least 10 Bitsocial tokens to post."
}
}
]
}
});If you are running your own node locally without connecting over RPC, you can install via npm and register the challenge manually:
npm install @bitsocial/evm-contract-challengeimport PKC from "@pkcprotocol/pkc-js";
import { evmContractChallenge } from "@bitsocial/evm-contract-challenge";
PKC.challenges["@bitsocial/evm-contract-challenge"] = evmContractChallenge;Then set the challenge on your community:
await community.edit({
settings: {
challenges: [
{
name: "@bitsocial/evm-contract-challenge",
options: {
chainTicker: "eth",
address: "0xEA81DaB2e0EcBc6B5c4172DE4c22B6Ef6E55Bd8f",
abi: '{"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}',
condition: ">10000000000000000000",
error: "You need at least 10 Bitsocial tokens to post."
}
}
]
}
});Each example uses a read-only contract function that takes a single address argument. The condition compares against the raw return value including decimal places (e.g. 10 USDC with 6 decimals = 10000000 raw).
balanceOf — standard ERC-20 / ERC-721 token balance:
{"constant":true,"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}getScore — Gitcoin Passport score (returns uint256 with 4 decimals):
{"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"getScore","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}| Description | chainTicker |
address |
ABI | condition |
|---|---|---|---|---|
| At least 10 Bitsocial (BSO) tokens | eth |
0xEA81DaB2e0EcBc6B5c4172DE4c22B6Ef6E55Bd8f |
balanceOf |
>10000000000000000000 |
| Minimum 10 USDC | eth |
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
balanceOf |
>10000000 |
| Any WETH balance | eth |
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 |
balanceOf |
>0 |
| Gitcoin Passport score above 20 (proof of personhood) | op |
0xd6c51bB9E23bD7f1fEa22A3F2f85E3BFC8338Cb0 |
getScore |
>200000 |
| At least 10 MATIC on Polygon | matic |
0x0000000000000000000000000000000000001010 |
balanceOf |
>10000000000000000000 |
| Any stETH balance (Lido staked ETH) | eth |
0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84 |
balanceOf |
>0 |
For chains other than Ethereum mainnet (e.g. Optimism, Polygon), you will also need to set
rpcUrlsto one or more JSON-RPC endpoints for that chain.
All option values must be strings.
| Option | Default | Description |
|---|---|---|
chainTicker |
"eth" |
The chain ticker (e.g. eth, matic) |
rpcUrls |
— | Comma-separated JSON-RPC URLs for the chain (uses viem defaults if omitted) |
address |
(required) | The contract address to call |
abi |
(required) | The ABI of the contract method as a JSON object (not an array) |
condition |
(required) | Condition the return value must pass (=, >, or < followed by a value, e.g. >1000) |
error |
"Contract call response doesn't pass condition." |
Custom error message shown when the condition fails |
You can provide multiple RPC endpoints as a comma-separated string:
https://eth.llamarpc.com,https://rpc.ankr.com/eth,https://eth.drpc.org
When multiple URLs are provided, viem's fallback transport is used with automatic ranking enabled (rank: true). This means:
- Requests are sent to the highest-ranked RPC endpoint
- If a request fails, it automatically falls back to the next endpoint
- viem periodically pings all endpoints in the background and reorders them by latency and stability
- A single URL works the same as before (no fallback overhead)
- If
rpcUrlsis omitted, viem's built-in default RPCs are used
This improves reliability — if one RPC provider goes down, the challenge automatically uses the next available endpoint.
npm run typecheck
npm run build
npm test