kycpad

kycpad docs · v1

Build on devs who can't reset.

How verification works, how launches are checked byte by byte, and a free API that tells your bot, terminal or site whether a coin's dev is a real person.

Overview

Anon devs reset. Verified devs can't.

On pump.fun a dev can rug, burn the wallet and come back an hour later as a stranger. kycpad closes that loop: every dev passes a Stripe Identity check, gets exactly one launch wallet for life, and every coin they launch stays on a public record anyone can query.

Verify once

Government ID + live selfie through Stripe. kycpad never sees either.

Launch on pump.fun

Real pump.fun coins. The verified wallet is the on-chain creator.

Query the record

Free JSON API and image badges for bots, terminals and sites.

Quickstart

Check a dev in one request

No key, no signup. Every public endpoint is CORS-enabled, so you can call it from a browser, a bot or a backend.

shell
# Is this dev a verified human? What did they launch before?
curl https://www.kycpad.fun/api/v1/devs/2sqA7hhhJHckSFMMP8hJNHc86L4JNYNbh6Ts3ymMCWvP

# The live board: new | graduating | graduated
curl "https://www.kycpad.fun/api/v1/coins?tab=graduating&limit=10"
GET /api/v1/devs/2sqA…json
{
  "wallet": "2sqA7hhhJHckSFMMP8hJNHc86L4JNYNbh6Ts3ymMCWvP",
  "verified": true,
  "verifiedAt": "2026-10-02T14:21:07.000Z",
  "record": {
    "launched": 2,
    "graduated": 1,
    "onCurve": 1,
    "dead": 0
  },
  "coins": [
    "…coin objects, same shape as /coins/{mint}"
  ],
  "links": {
    "page": "https://www.kycpad.fun/dev/2sqA7hhhJHckSFMMP8hJNHc86L4JNYNbh6Ts3ymMCWvP",
    "badge": "https://www.kycpad.fun/api/badge/dev/2sqA7hhhJHckSFMMP8hJNHc86L4JNYNbh6Ts3ymMCWvP"
  }
}

Protocol

How verification works

Verification binds a wallet to a real person without kycpad ever holding identity documents.

  1. 1

    Sign an identity request

    The wallet signs a fixed plain-text message containing its address and a timestamp. It is off-chain and costs nothing. The server rejects it after 10 minutes or if a single byte differs from the format it expects.
  2. 2

    kycpad opens a Stripe Identity session

    Type document with require_matching_selfie and require_live_capture, tagged with metadata.wallet. You get Stripe's hosted page back.
  3. 3

    Photo of your ID, then a live selfie

    Stripe checks the document is genuine and that the face in the selfie matches it. Printed photos and screens fail the liveness check.
  4. 4

    Status settles

    kycpad polls the session and records one of the statuses below. Only verified can launch.
Your wallet
kycpad
Stripe Identity
Solana / pump.fun
  1. 01signMessage(identity request)off-chain, free, 10 min TTL
  2. 02create VerificationSessiondocument + live selfie, metadata.wallet
  3. 03hosted ID + selfie flow
  4. 04poll session → verified_outputs
  5. 05HMAC fingerprint, enforce one walletraw details discarded
Identity documents flow between you and Stripe only. kycpad receives a verdict plus the fields it needs to build a fingerprint.

Status machine

Tap a status to see what it means and where it can go next.

verified

A real, unique person. This wallet can now launch, and it is the only one that ever can for this identity.

next Final

Starting verification from your own client

start-verification.tsts
import bs58 from 'bs58'

// 1. Build the exact message the server expects. It is valid for 10 minutes.
const wallet = provider.publicKey.toBase58()
const issued = new Date().toISOString()
const message = [
  'kycpad identity request',
  '',
  'I am starting ID verification for this wallet. Only this wallet will be allowed to launch coins under my verified identity.',
  '',
  `wallet: ${wallet}`,
  `issued: ${issued}`,
].join('\n')

// 2. Sign it. This is an off-chain message: no transaction, no fee.
const { signature } = await provider.signMessage(new TextEncoder().encode(message))

// 3. Ask kycpad for a Stripe Identity session tied to this wallet.
const res = await fetch('/api/kyc/start', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ message, signature: bs58.encode(signature) }),
})
const { url, state } = await res.json()

// 4. Open Stripe's hosted flow (ID photo + live selfie), then poll until it settles.
if (url) window.open(url, '_blank')
let status = state.status
while (status === 'requires_input' || status === 'processing') {
  await new Promise((r) => setTimeout(r, 3000))
  status = (await fetch(`/api/kyc/status?wallet=${wallet}`).then((r) => r.json())).status
}
// status is now 'verified', 'duplicate' or 'canceled'

Protocol

One person, one wallet

The whole point is that a rugger can't come back fresh. So an identity can back exactly one launch wallet, ever.

When Stripe returns a verified result, kycpad builds a fingerprint from the legal name and document address, normalises it, and keys it with an HMAC secret. Only the hash is stored. A unique index on that hash means a second wallet presenting the same person lands in duplicate instead of verified, even if both finish in the same second.

lib/kyc.tsts
const key = `${first_name}|${last_name}|${address.line1}|${address.postal_code}|${address.country}`
  .normalize('NFKD').toLowerCase().replace(/\s+/g, ' ').trim()

const personHash = createHmac('sha256', `kycpad:person:${SECRET}`).update(key).digest('hex')
// stored: personHash, country, verified_at — nothing else

Protocol

Launching a coin

Launches are ordinary pump.fun coins. kycpad builds the transactions, your verified wallet signs them, and kycpad checks every byte before relaying.

Your wallet
kycpad
Stripe Identity
Solana / pump.fun
  1. 01POST /api/launch/preparemultipart: image + fields
  2. 02pin metadata to IPFS, build create_v2 (+ buy)
  3. 03unsigned txs, mint pre-signed
  4. 04signAllTransactions → POST /confirmone approval
  5. 05verify bytes, relay create → buy
Your wallet approves once. If the create lands but the initial buy fails, the coin is still live and the error is returned as buyError.
launch.tsts
import { VersionedTransaction } from '@solana/web3.js'

const fromB64 = (s: string) => VersionedTransaction.deserialize(Uint8Array.from(atob(s), (c) => c.charCodeAt(0)))
const toB64 = (tx: VersionedTransaction) => btoa(String.fromCharCode(...tx.serialize()))

// 1. Prepare: kycpad checks your verification, pins metadata to IPFS and builds the transactions.
const form = new FormData()
form.set('wallet', wallet)
form.set('name', 'Accountable Dog')
form.set('symbol', 'ACCT')            // 1–10 letters/digits, "$" is stripped
form.set('description', 'A dog with a passport.')
form.set('image', file)               // PNG, JPG, GIF or WebP, max 3.5 MB
form.set('buySol', '0.5')             // optional initial buy, 0–10 SOL
form.set('twitter', 'https://x.com/acctdog')

const prep = await fetch('/api/launch/prepare', { method: 'POST', body: form })
if (!prep.ok) throw new Error((await prep.json()).error) // 403 kyc_required, 402 insufficient_sol, 429 …
const { mint, createTx, buyTx } = await prep.json()

// 2. One approval in the wallet signs both (the mint key already co-signed the create).
const txs = [fromB64(createTx), ...(buyTx ? [fromB64(buyTx)] : [])]
const [signedCreate, signedBuy] = await provider.signAllTransactions(txs)

// 3. Confirm: the server re-verifies every byte, then relays create → buy.
let res
do {
  res = await fetch('/api/launch/confirm', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ mint, createTx: toB64(signedCreate), buyTx: signedBuy ? toB64(signedBuy) : null }),
  })
} while (res.status === 202) // still landing: re-send the same signed bytes

const { status, signature, buyError } = await res.json() // status: 'live'

Protocol

Transaction checks

The server never signs on your behalf, but it does refuse to relay anything that differs from what it prepared. Tick a box to see which check stops it.

Try to tamper with the signed bytes

POST /api/launch/confirm

  1. No address lookup tables
  2. Fee payer = verified wallet
  3. Every signature present and valid (ed25519)
  4. Only pump.fun + wallet guard programs
  5. Mint = the one prepared, and it co-signed
  6. Name, ticker, metadata URI unchanged
  7. create_v2 creator = verified wallet
200 { "status": "live", "signature": "5Kd…" }

All checks pass, so kycpad relays create, then the buy.

Wallets like Phantom append their own guard instruction (Lighthouse) and compute-budget instructions. Those are allowed. Anything else, including transfers, unknown programs or extra coin creations, is rejected before broadcast.

Protocol

The public record

Every verified dev has a page and a permit-style record that only grows. Coins are classified from on-chain bonding-curve state, not self-reported.

StatusRule
graduatedThe pump.fun bonding curve completed and the coin moved to an AMM.
curveStill on the bonding curve. progress = SOL raised / (SOL raised + SOL left to graduate).
deadDead = at least 7 days old with under 5% of the bonding curve filled.
unknownThe curve account could not be read at request time.

Build

Public API

Read-only, keyless, CORS *, cached for about 30 seconds at the edge. Errors share one shape: { "error": { "code", "message" } }.

GET/api/v1/devs/{wallet}public · 120/min
A dev's verification status, record counts and every coin they launched. Unverified wallets return verified: false and an empty record.
GET/api/v1/coins?tab=new|graduating|graduated&limit=1..60public · 60/min
The live board. graduating is sorted by curve progress, highest first.
GET/api/v1/coins/{mint}public · 120/min
One coin launched on kycpad. Returns 404 not_found for any coin that wasn't, which is itself a signal: an anon dev.

Coin object

GET /api/v1/coins/{mint}json
{
  "mint": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
  "name": "Accountable Dog",
  "symbol": "ACCT",
  "description": "A dog with a passport.",
  "image": "https://ipfs.io/ipfs/bafkrei…",
  "dev": "2sqA7hhhJHckSFMMP8hJNHc86L4JNYNbh6Ts3ymMCWvP",
  "permitNo": 12,
  "launchedAt": "2026-10-02T15:03:44.000Z",
  "status": "curve",
  "curve": {
    "progress": 0.4127,
    "complete": false,
    "marketCapSol": 61.204,
    "solToGraduate": 48.31
  },
  "links": {
    "page": "https://www.kycpad.fun/coin/7xKX…gAsU",
    "pump": "https://pump.fun/coin/7xKX…gAsU",
    "badge": "https://www.kycpad.fun/api/badge/coin/7xKX…gAsU",
    "website": null,
    "twitter": "https://x.com/acctdog",
    "telegram": null
  }
}
FieldTypeNotes
devstringThe verified wallet. Also the on-chain pump.fun creator.
permitNonumberGlobal launch serial, as printed on the permit card.
statusenumgraduated · curve · dead · unknown
curve.progressnumber0–1, share of the SOL needed to graduate that has been raised.
curve.solToGraduatenumberSOL still to be bought before the curve completes (before fees).
links.badgeurl1200×630 PNG share card for this coin.

Write endpoints

These power the site and require a wallet signature or a verified wallet. They are documented so you can build your own client, not for scraping.

EndpointBodyReturns
POST /api/kyc/start{ message, signature }{ url, state } · Stripe hosted URL
GET /api/kyc/status?wallet=—{ wallet, status, lastError, verifiedAt }
POST /api/launch/preparemultipart form{ mint, createTx, buyTx }
POST /api/launch/confirm{ mint, createTx, buyTx }{ status, signature, buyError } · 202 while landing

Build

API console

Real requests against this deployment. Pick an endpoint, edit the value, hit Send.

GET

/api/v1/devs/2sqA7hhhJHckSFMMP8hJNHc86L4JNYNbh6Ts3ymMCWvP

Hit Send to call the live API from your browser.

Build

Recipes

Copy, paste, ship. Both run anywhere fetch exists.

ts
// Posts every new kycpad launch to a Telegram channel, with the dev's record attached.
const API = 'https://www.kycpad.fun/api/v1'
const seen = new Set<string>()

async function tick() {
  const { coins } = await fetch(`${API}/coins?tab=new&limit=20`).then((r) => r.json())
  for (const coin of coins.reverse()) {
    if (seen.has(coin.mint)) continue
    seen.add(coin.mint)

    const dev = await fetch(`${API}/devs/${coin.dev}`).then((r) => r.json())
    const { launched, graduated, dead } = dev.record
    const text = [
      `New launch: ${coin.name} ($${coin.symbol}) · permit #${coin.permitNo}`,
      `Dev: ID verified · ${launched} launched · ${graduated} graduated · ${dead} dead`,
      coin.links.page,
    ].join('\n')

    await fetch(`https://api.telegram.org/bot${process.env.TG_TOKEN}/sendMessage`, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ chat_id: process.env.TG_CHAT, text }),
    })
  }
}

setInterval(tick, 30_000)
tick()

Build

Badges & embeds

Every verified dev and every coin has a 1200×630 PNG rendered on demand. Drop it into a site, a README or a pinned post.

Live kycpad verified dev badge
html
<a href="https://www.kycpad.fun/dev/2sqA7hhhJHckSFMMP8hJNHc86L4JNYNbh6Ts3ymMCWvP">
  <img
    src="https://www.kycpad.fun/api/badge/dev/2sqA7hhhJHckSFMMP8hJNHc86L4JNYNbh6Ts3ymMCWvP"
    alt="ID-verified dev on kycpad"
    width="600" height="315"
  />
</a>

Badges return 404 for wallets that aren't verified, so a broken image is never a fake "verified". Responses are cached for 30 minutes at the edge.

Build

Verify on chain

You shouldn't have to trust our database. The creator kycpad reports is the creator pump.fun stores in the bonding-curve account.

verify-creator.tsts
import { Connection, PublicKey } from '@solana/web3.js'

const PUMP = new PublicKey('6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P')
const connection = new Connection('https://api.mainnet-beta.solana.com')

// Don't trust us: read the creator straight from pump.fun's bonding curve account.
export async function creatorOf(mint: string) {
  const [curve] = PublicKey.findProgramAddressSync(
    [Buffer.from('bonding-curve'), new PublicKey(mint).toBuffer()],
    PUMP,
  )
  const info = await connection.getAccountInfo(curve)
  if (!info || info.data.length < 81) return null
  return new PublicKey(info.data.subarray(49, 81)).toBase58() // creator pubkey, bytes 49..81
}

const mint = '<coin mint>'
const coin = await fetch(`https://www.kycpad.fun/api/v1/coins/${mint}`).then((r) => r.json())
console.log((await creatorOf(mint)) === coin.dev ? 'creator matches the verified dev' : 'MISMATCH')

Reference

Limits

WhatLimit
Coin name1–32 characters
Ticker1–10 letters or digits, leading $ stripped
Description500 characters
ImagePNG, JPG, GIF or WebP, 3.5 MB. Stills are squared and re-encoded in the browser.
Initial buy0–10 SOL
Launches3 per verified wallet per 24 h
Launch costabout 0.03 SOL in rent and fees, plus any initial buy
Identity request signaturevalid 10 minutes
Rate limitsper IP: prepare 8/min, confirm 12/min, kyc start 6/min, public reads 60–120/min

Reference

Errors

HTTPCodeWhen
400invalid_wallet · invalid_mintMalformed address or missing field
401—Identity signature expired or doesn’t match the wallet
402insufficient_solWallet can’t cover launch cost plus initial buy
403kyc_requiredWallet isn’t verified (or lost verification)
404not_foundCoin wasn’t launched on kycpad, or the launch expired
422—Signed transaction failed a check, see Transaction checks
429—Rate limited. Respect the retry-after header.
502upstream_unavailableStripe, Solana RPC or IPFS didn’t answer. Retry.

Reference

Privacy & security

DataStripe seeskycpad keeps
ID document photoYesNever
Selfie / faceYesNever
Name, address, DOBYesOnly an HMAC fingerprint
CountryYesYes, ISO code
WalletAs metadataYes, public

Reference

FAQ

Can I verify a second wallet?

No. One identity backs one launch wallet. A second attempt ends in duplicate. That is the feature.

What if I lose my wallet?

Your identity stays bound to that wallet and there is no automated recovery, so treat its seed phrase like your passport.

Do traders need to verify?

No. Only devs who launch. Anyone can buy and sell kycpad coins on pump.fun like any other coin.

Is my coin different on pump.fun?

It is a standard pump.fun coin. The difference is that its creator is a verified wallet with a public record here.

Does verification cost anything?

No. Signing the identity request is free and kycpad doesn’t charge for the check.

Ready to put your face behind it?

Verification takes about two minutes.

Get verified