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.
# 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"{
"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
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
kycpad opens a Stripe Identity session
Typedocumentwithrequire_matching_selfieandrequire_live_capture, tagged withmetadata.wallet. You get Stripe's hosted page back. - 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
Status settles
kycpad polls the session and records one of the statuses below. Onlyverifiedcan launch.
- 01signMessage(identity request)off-chain, free, 10 min TTL
- 02create VerificationSessiondocument + live selfie, metadata.wallet
- 03hosted ID + selfie flow
- 04poll session → verified_outputs
- 05HMAC fingerprint, enforce one walletraw details discarded
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
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.
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 elseProtocol
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.
- 01POST /api/launch/preparemultipart: image + fields
- 02pin metadata to IPFS, build create_v2 (+ buy)
- 03unsigned txs, mint pre-signed
- 04signAllTransactions → POST /confirmone approval
- 05verify bytes, relay create → buy
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.
POST /api/launch/confirm
- No address lookup tables
- Fee payer = verified wallet
- Every signature present and valid (ed25519)
- Only pump.fun + wallet guard programs
- Mint = the one prepared, and it co-signed
- Name, ticker, metadata URI unchanged
- create_v2 creator = verified wallet
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.
| Status | Rule |
|---|---|
| graduated | The pump.fun bonding curve completed and the coin moved to an AMM. |
| curve | Still on the bonding curve. progress = SOL raised / (SOL raised + SOL left to graduate). |
| dead | Dead = at least 7 days old with under 5% of the bonding curve filled. |
| unknown | The 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" } }.
/api/v1/devs/{wallet}public · 120/minverified: false and an empty record./api/v1/coins?tab=new|graduating|graduated&limit=1..60public · 60/mingraduating is sorted by curve progress, highest first./api/v1/coins/{mint}public · 120/min404 not_found for any coin that wasn't, which is itself a signal: an anon dev.Coin object
{
"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
}
}| Field | Type | Notes |
|---|---|---|
| dev | string | The verified wallet. Also the on-chain pump.fun creator. |
| permitNo | number | Global launch serial, as printed on the permit card. |
| status | enum | graduated · curve · dead · unknown |
| curve.progress | number | 0–1, share of the SOL needed to graduate that has been raised. |
| curve.solToGraduate | number | SOL still to be bought before the curve completes (before fees). |
| links.badge | url | 1200×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.
| Endpoint | Body | Returns |
|---|---|---|
| POST /api/kyc/start | { message, signature } | { url, state } · Stripe hosted URL |
| GET /api/kyc/status?wallet= | — | { wallet, status, lastError, verifiedAt } |
| POST /api/launch/prepare | multipart 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.
/api/v1/devs/2sqA7hhhJHckSFMMP8hJNHc86L4JNYNbh6Ts3ymMCWvP
Hit Send to call the live API from your browser.
Build
Recipes
Copy, paste, ship. Both run anywhere fetch exists.
// 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.
<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.
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
| What | Limit |
|---|---|
| Coin name | 1–32 characters |
| Ticker | 1–10 letters or digits, leading $ stripped |
| Description | 500 characters |
| Image | PNG, JPG, GIF or WebP, 3.5 MB. Stills are squared and re-encoded in the browser. |
| Initial buy | 0–10 SOL |
| Launches | 3 per verified wallet per 24 h |
| Launch cost | about 0.03 SOL in rent and fees, plus any initial buy |
| Identity request signature | valid 10 minutes |
| Rate limits | per IP: prepare 8/min, confirm 12/min, kyc start 6/min, public reads 60–120/min |
Reference
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_wallet · invalid_mint | Malformed address or missing field |
| 401 | — | Identity signature expired or doesn’t match the wallet |
| 402 | insufficient_sol | Wallet can’t cover launch cost plus initial buy |
| 403 | kyc_required | Wallet isn’t verified (or lost verification) |
| 404 | not_found | Coin 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. |
| 502 | upstream_unavailable | Stripe, Solana RPC or IPFS didn’t answer. Retry. |
Reference
Privacy & security
| Data | Stripe sees | kycpad keeps |
|---|---|---|
| ID document photo | Yes | Never |
| Selfie / face | Yes | Never |
| Name, address, DOB | Yes | Only an HMAC fingerprint |
| Country | Yes | Yes, ISO code |
| Wallet | As metadata | Yes, 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.