Skip to content
Watermark
Menu

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.

  • One person, 1,000 wallets

    Every extra wallet needs another real bank card, so one card gets one wallet in your app.

  • Airdrop farms and sybil scripts

    Scripts can make wallets for free, but they can't make bank cards.

  • Bots draining your free stuff

    Free mints, faucets, referral and quest rewards, and the gas you sponsor only go to stamped wallets.

  • Fake signups for perks

    A new account with a fresh wallet gets nothing until a real card stands behind it.

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.

  1. 1

    The user taps their card, once

    They open your verify link, scan the QR with our Android app (Watermark Tap) and tap a bank card. Nothing is charged.

  2. 2

    A private proof

    Our prover turns the tap into a zero-knowledge proof: a real card, on your card list, signed this session. The card number is never stored or put on-chain.

  3. 3

    A stamp on Whitechain

    WatermarkRegistry checks the proof and stamps the wallet for your app, for up to 3 months. The user pays no fees.

  4. 4

    Your app checks the stamp

    Before you pay out, you do one on-chain read: isStamped(wallet, appId). There's no API key and no account with us.

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.

Read-onlyHolds no keysSigns nothingRuns locally
  1. 1.Clone the repo (opens in a new tab) and run pnpm install at its root (Node 24).
  2. 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. 3.Ask your agent a question from the table below.
MCP server config
{
  "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

ToolWhat it doesAsk it
Toolwatermark_check_walletWhat it doesIs this wallet stamped for this app, and until when?Ask it“Is 0x… stamped for our app?”
Toolwatermark_check_walletsWhat it doesThe same check for up to 200 wallets, in one call.Ask it“Which of these 200 wallets are stamped?”
Toolwatermark_stamp_historyWhat 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_infoWhat 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_policyWhat it doesThe card list (policy) an app id uses.Ask it“What card list does this app id use?”
Toolwatermark_explain_policyWhat 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_appsWhat it doesThe registry address and the deployed apps with their labels.Ask it“Which Watermark apps exist?”
Toolwatermark_supported_cardsWhat 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_policyWhat 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_appWhat 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_linkWhat 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_snippetWhat 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.

Prompt
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.

Solidity
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.

TypeScript
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.

watermark-gate.ts
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.

Verify link (live app)
https://usewatermark.xyz/verify?app=393344891601835728760966937624776276735512086066329273346256164761263100630

Customize it.

What you can change, and what runs today. The contracts enforce the on-chain rules, not us.

Live running nowYour setting you choose itPlanned not live
SettingOptionsHowToday
SettingStamp lengthOptions1, 2 or 3 monthsHowA 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
  • Live3 months (our app)
  • Your settingown app
SettingTapped recently (last hour, 24 hours, 7 days)OptionsAny window you likeHowYour 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
  • Your setting
SettingLonger than 3 monthsOptions6 or 12 monthsHowNot possible today: the 3-month cap is in the registry, so it needs a registry upgrade.Today
  • Planned
SettingClaims per wallet per dayOptions1, 5, 20 or any numberHowAirdrops 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
  • Your setting
  • Live20 a day (our sponsor)
SettingWhich cards countOptionsYour own card list of BIN prefixes (6 or 8 digits, up to 8,192): leave out prepaid, or only some banks' listed rangesHowDraft 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
  • Live4,208 RuPay bank-debit ranges, no prepaid
  • Your settingown app
SettingOne card, one wallet per appOptionsAlways onHowThe 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
  • Live
SettingWho pays network feesOptionsYou do, so your users pay $0HowStamps: 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
  • Liveour sponsor
  • Your settingown sponsor
SettingRate limits and budgetsOptionsRequests per minute, gas caps, daily budgetsHowEnvironment settings on the relayer (it is in the repo). The full list with defaults is below.Today
  • Your setting
SettingCard networksOptionsRuPay, Visa, MastercardHowRuPay 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
  • LiveRuPay
  • PlannedVisa, Mastercard
SettingChainsOptionsWhitechain Sepolia, other EVM chainsHowContracts 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
  • LiveWhitechain Sepolia
  • Your settingbackend check

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.

SettingWhat it limitsDefault
SettingSESSIONS_PER_MINUTE_PER_IPWhat it limitsNew verify sessions per IP per minuteDefault10
SettingMAX_OPEN_SESSIONSWhat it limitsOpen sessions at onceDefault5,000
SettingSTAMPS_PER_MINUTE_PER_IPWhat it limitsStamp requests per IP per minuteDefault30
SettingSTAMP_MAX_QUEUEWhat it limitsStamps waiting per chain before BUSYDefault20
SettingMAX_GAS_PER_STAMPWhat it limitsGas cap for one stampDefault3,500,000
SettingSTAMP_BUDGET_WEIWhat it limitsGas budget for stamps per UTC dayDefault0.1 WBT
SettingPAYS_PER_MINUTE_PER_IPWhat it limitsPayment requests per IP per minuteDefault10
SettingMAX_GAS_PER_PAYWhat it limitsGas cap for one paymentDefault300,000
SettingDAILY_BUDGET_WEIWhat it limitsGas budget for payments per UTC dayDefault0.1 WBT
SettingPAY_MAX_USDTWhat it limitsLargest payment the relayer sponsorsDefault5 USDT
SettingPAY_MAX_REVERTS_PER_WALLETWhat it limitsFailed payments per wallet per UTC dayDefault2
SettingPAY_PAUSEDWhat it limitsStop every sponsored payment at onceDefaultfalse
SettingCORS_ORIGINSWhat it limitsWebsites allowed to call the relayerDefaulthttp://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.