Docs
Watermark for businesses.
Keep bots away from anything you give out or protect, like airdrops, sponsored gas, faucets, rewards, votes and AI agent payouts. Only wallets backed by a real bank card get in. There's no KYC, and your users never pay fees.
What it stops.
Each extra wallet needs another real bank card. Bots can't fake that.
What it does not do
- It isn't KYC or identity. We never learn who the user is.
- One card is not one person. Two cards can make two stamps.
- A borrowed card works too. A contactless tap has no PIN.
- Only RuPay cards from India work today. Visa and Mastercard are planned, but not live yet.
How it works.
Four steps. The user does one tap; your app does one read.
Connect your AI agent.
Your AI agent can check wallets for you. MCP (Model Context Protocol) lets apps like Claude Desktop, Claude Code or Cursor use our 12 Watermark tools.
- 1.Clone the repo (opens in a new tab) and run
pnpm installat its root (Node 24). - 2.Add this to your MCP client's server config (the exact file depends on the client). Put your own path to the repo in
args. - 3.Ask your agent a question from the table below.
{
"mcpServers": {
"watermark": {
"command": "node",
"args": ["/absolute/path/to/watermark/mcp/src/index.ts"],
"env": { "WHITECHAIN_RPC": "https://rpc.ankr.com/whitechain_testnet" }
}
}
}From mcp/README.md (opens in a new tab). The tools only read the chain or return unsigned data. No tool takes card data.
The 12 tools
| Tool | What it does | Ask it |
|---|---|---|
Toolwatermark_check_wallet | What it doesIs this wallet stamped for this app, and until when? | Ask it“Is 0x… stamped for our app?” |
Toolwatermark_check_wallets | What it doesThe same check for up to 200 wallets, in one call. | Ask it“Which of these 200 wallets are stamped?” |
Toolwatermark_stamp_history | What it doesAn app's stamps: total, unique wallets, how many are live, the latest ones with explorer links. | Ask it“How many wallets has our app stamped, and which are live?” |
Toolwatermark_app_info | What it doesAn app's on-chain owner, card list root and session signer, plus an audit against the published list. | Ask it“Does this app id enforce the card list it publishes?” |
Toolwatermark_get_policy | What it doesThe card list (policy) an app id uses. | Ask it“What card list does this app id use?” |
Toolwatermark_explain_policy | What it doesPlain words: who passes, what a stamp reveals, what it doesn't prove. | Ask it“Explain this app's card list in plain words.” |
Toolwatermark_list_apps | What it doesThe registry address and the deployed apps with their labels. | Ask it“Which Watermark apps exist?” |
Toolwatermark_supported_cards | What it doesWhich cards can be proven today. RuPay debit is live; Visa and Mastercard are planned. | Ask it“Which cards can my users use today?” |
Toolwatermark_draft_policy | What it doesTurns BIN prefixes from a public BIN list and rules into a card list, its root and warnings. Signs nothing. | Ask it“Draft a card list for my app: RuPay debit, no prepaid.” |
Toolwatermark_prepare_register_app | What it doesAn unsigned transaction that registers your own app and card list, plus the app id it will get. | Ask it“Prepare the transaction to register our app.” |
Toolwatermark_create_verify_link | What it doesThe link that sends a user to the verify page for an app. | Ask it“Give me the verify link for my users.” |
Toolwatermark_integration_snippet | What it doesSolidity or TypeScript code with the real registry address filled in. | Ask it“Give me the Solidity gate for our claim contract.” |
Or let your coding agent add it
Paste this prompt into Claude Code, Cursor or any coding agent. It reads our docs and adds the stamp check to your app.
Add Watermark anti-bot gating to this project. A Watermark stamp means a wallet was linked to one real bank card. Only stamped wallets may get what this app gives away or protects: airdrops, mints, rewards, faucet tokens, sponsored gas, votes or access.
1. Read https://usewatermark.xyz/watermark.txt first. Use only the addresses, functions and tool names it gives. Don't invent any.
2. Find every place this repo gives something away or lets a wallet in: airdrop claims, mints, rewards, faucets, sponsored gas, votes, allowlists. Check whether they live in Solidity contracts, a TypeScript/JavaScript backend, or both.
3. App id: <YOUR_APP_ID> below is a placeholder; keep it in one config constant. For real RuPay bank cards, set it to the live app id: 393344891601835728760966937624776276735512086066329273346256164761263100630. Register our own app only if we want our own card list: draft it with watermark_draft_policy, then watermark_prepare_register_app (unsigned; I send it, and we must run our own session signer). Tell me which you chose and why.
4. Solidity: contracts on Whitechain Sepolia (chain 1874) can gate directly. Add the IWatermarkRegistry interface and the onlyStamped modifier from the docs, with the registry 0x998934Efa31a6bE75945fDfAa126d3D6401F8Fd6 and <YOUR_APP_ID>, and put onlyStamped on each claim function. A contract on another chain can't read the registry: check in the backend instead.
5. Backend: before paying out, call check() from @watermark/sdk, or the plain viem isStamped read in the docs (the SDK isn't on npm yet). Refuse when stamped is false. Never trust a stamp flag sent by the browser.
6. For users who aren't stamped, show a "Get verified with Watermark" link to https://usewatermark.xyz/verify?app=<YOUR_APP_ID> (what watermark_create_verify_link returns). Tell them to verify the same wallet they claim with.
7. Never ask for, store or log card numbers or any card data. We only need the wallet address.
8. Keep every existing test passing. Add a test showing an unstamped wallet is refused (mock the registry or the check). Add no keys or secrets; reading a stamp needs none.
Finish with a short summary: files changed, where each check sits, the app id and why, how to run the new test, and anything left for me to do.Add it to your app.
Check the stamp before you pay out. Pick the place your payout lives.
1. In your contract (Solidity)
For contracts on Whitechain Sepolia, where the registry is. Put onlyStamped on each claim function.
interface IWatermarkRegistry {
function isStamped(address wallet, uint256 appId) external view returns (bool ok, uint64 expiresAt);
}
modifier onlyStamped() {
(bool ok, uint64 expiresAt) = watermark.isStamped(msg.sender, watermarkAppId);
require(ok && expiresAt > block.timestamp, "Watermark: no live stamp");
_;
}2. In your app (TypeScript SDK)
Reads the chain directly: no API key. The SDK lives in the repo (sdk/) and is not on npm yet; watermark.txt has the same read in plain viem.
import { check, checkMany } from "@watermark/sdk";
const registry = "0x998934Efa31a6bE75945fDfAa126d3D6401F8Fd6";
const appId = "393344891601835728760966937624776276735512086066329273346256164761263100630";
const { stamped, expiresAt } = await check(userAddress, appId, { registry });
if (!stamped) showGetStampedButton(); // links to the /verify page (verifyLink)
// Airdrop or allowlist export: up to 200 wallets per call, one multicall
const results = await checkMany(addresses, appId, { registry });3. On your server, right before a payout
Use this when your contract is on another chain or the payout is off-chain. Never trust a “stamped” flag sent by the browser.
import { check, type CheckOptions } from "@watermark/sdk";
const REGISTRY = "0x998934Efa31a6bE75945fDfAa126d3D6401F8Fd6";
const APP_ID = "393344891601835728760966937624776276735512086066329273346256164761263100630";
export async function assertStamped(wallet: string, opts: CheckOptions = {}): Promise<void> {
const { stamped } = await check(wallet, APP_ID, { registry: REGISTRY, ...opts });
if (!stamped) throw new Error(`NOT_STAMPED: verify at https://usewatermark.xyz/verify?app=${APP_ID}`);
}4. Send unverified users to the verify link
A “Get verified with Watermark” button. Ask users to verify the same wallet they claim with. It works for app ids our relayer serves, such as the live app below; your own app runs its own session signer.
https://usewatermark.xyz/verify?app=393344891601835728760966937624776276735512086066329273346256164761263100630Customize it.
What you can change, and what runs today. The contracts enforce the on-chain rules, not us.
| Setting | Options | How | Today |
|---|---|---|---|
| SettingStamp length | Options1, 2 or 3 months | HowA stamp lasts until the end of a month 1 to 3 months ahead. The registry enforces that range on-chain. Your app's session signer picks the month when it signs each session; a renewal only ever extends a stamp. | Today
|
| SettingTapped recently (last hour, 24 hours, 7 days) | OptionsAny window you like | HowYour backend reads when the wallet was last stamped: the SDK's stampHistory (or the registry's Stamped event) gives each stamp's block; read that block's time and decide. A contract can't read past events, so do this off-chain. | Today
|
| SettingLonger than 3 months | Options6 or 12 months | HowNot possible today: the 3-month cap is in the registry, so it needs a registry upgrade. | Today
|
| SettingClaims per wallet per day | Options1, 5, 20 or any number | HowAirdrops and mints: your own contract counts claims (the Drop example keeps one claim per wallet). Sponsored payments: each sponsor contract has its own daily quota per wallet, fixed when it is deployed (QUOTA_PER_DAY). | Today
|
| SettingWhich cards count | OptionsYour own card list of BIN prefixes (6 or 8 digits, up to 8,192): leave out prepaid, or only some banks' listed ranges | HowDraft it with watermark_draft_policy (SDK draftPolicy), then register it as your own app with watermark_prepare_register_app (SDK prepareRegisterApp) and send that transaction from your wallet. A list can't change after you register it: a new list means a new app id. Your own app runs its own session signer. | Today
|
| SettingOne card, one wallet per app | OptionsAlways on | HowThe registry ties each card to one wallet per app with a one-way card id. For a new campaign where the same cards may join again, use a new app id. | Today
|
| SettingWho pays network fees | OptionsYou do, so your users pay $0 | HowStamps: the relayer pays the gas. Payments: deploy your own WatermarkSponsor (DeploySponsor.s.sol with APP_ID, RELAYER_ADDRESS, QUOTA_PER_DAY) and run the relayer with SPONSOR_ADDRESS. Caps per payment and per day: PAY_MAX_USDT, MAX_GAS_PER_PAY, DAILY_BUDGET_WEI. | Today
|
| SettingRate limits and budgets | OptionsRequests per minute, gas caps, daily budgets | HowEnvironment settings on the relayer (it is in the repo). The full list with defaults is below. | Today
|
| SettingCard networks | OptionsRuPay, Visa, Mastercard | HowRuPay debit cards can be proven today. Visa and Mastercard card lists can be drafted, but no card can be proven yet: each needs its own circuit. | Today
|
| SettingChains | OptionsWhitechain Sepolia, other EVM chains | HowContracts on Whitechain Sepolia check the stamp directly. On any other chain, check from your backend before you pay out. Running the registry on another chain means deploying it there, and our relayer only serves Whitechain Sepolia and the alt mempool devnet today. | Today
|
Relayer settings
If you run your own relayer (it is in the repo, relayer/), these environment settings cap what bots can cost you. Every payment is simulated before any gas is spent.
| Setting | What it limits | Default |
|---|---|---|
SettingSESSIONS_PER_MINUTE_PER_IP | What it limitsNew verify sessions per IP per minute | Default10 |
SettingMAX_OPEN_SESSIONS | What it limitsOpen sessions at once | Default5,000 |
SettingSTAMPS_PER_MINUTE_PER_IP | What it limitsStamp requests per IP per minute | Default30 |
SettingSTAMP_MAX_QUEUE | What it limitsStamps waiting per chain before BUSY | Default20 |
SettingMAX_GAS_PER_STAMP | What it limitsGas cap for one stamp | Default3,500,000 |
SettingSTAMP_BUDGET_WEI | What it limitsGas budget for stamps per UTC day | Default0.1 WBT |
SettingPAYS_PER_MINUTE_PER_IP | What it limitsPayment requests per IP per minute | Default10 |
SettingMAX_GAS_PER_PAY | What it limitsGas cap for one payment | Default300,000 |
SettingDAILY_BUDGET_WEI | What it limitsGas budget for payments per UTC day | Default0.1 WBT |
SettingPAY_MAX_USDT | What it limitsLargest payment the relayer sponsors | Default5 USDT |
SettingPAY_MAX_REVERTS_PER_WALLET | What it limitsFailed payments per wallet per UTC day | Default2 |
SettingPAY_PAUSED | What it limitsStop every sponsored payment at once | Defaultfalse |
SettingCORS_ORIGINS | What it limitsWebsites allowed to call the relayer | Defaulthttp://localhost:3000 |
Pricing.
Free during the hackathon testnet.
Limits and privacy.
- Test networks only. A real launch needs PCI and legal review and bank and network terms.
- Our prover sees the card data briefly, in memory, to make the proof, then wipes it. You trust that wipe; proving on the phone is the real fix.
- We store nothing from the card: no KYC files, no documents, no card numbers. On-chain: the wallet, the app id, a one-way card id and the expiry month.
- No early revoke: a stamp runs to its expiry. A lost wallet can't move its stamp.
- Your code never needs card data: only a wallet address and a yes or no.
The full list: watermark.txt (opens in a new tab), sections 11 (Privacy) and 12 (Limits), and docs/PRIVACY.md (opens in a new tab).
Get help.
Code and issues
Full reference
Questions
