Wallet verification
Prove your customer controls a self-custodied wallet before releasing a withdrawal, without sending personal data to Clav.
What it is for
Wallet verification proves that an end customer controls the self-custodied address they want to withdraw to. It covers the requirement in art. 76-A, §5 of BCB Resolution 277/2022, as amended by BCB Resolution 521/2025.
The customer signs a message with their own wallet. Clav checks the signature, stores the proof and, on every withdrawal, tells you whether that address is verified.
No personal data
You send only an opaque customer identifier. Who the person behind it is stays in your system.
Cryptographic proof
Clav checks the signature against the given address and keeps the signed message as evidence.
Audit trail
Every issuance, attempt, approval, failure and revocation becomes an immutable event you can look up on the platform.
Enabled per organization
Wallet verification is off by default. If you do not see the Verified wallets menu, ask the Clav team to enable the feature for your organization.
Supported networks
Network (chain) | Accepted signature scheme |
|---|---|
EVM | EIP-191 (personal_sign), on any Ethereum-compatible network |
BITCOIN | BIP-322, with BIP-137 as a fallback |
TRON | signMessageV2 (version 1 is rejected) |
SOLANA | Ed25519 |
Addresses are accepted in any casing and normalized to the network’s canonical form before they are stored. Always use the address returned in the response, not the one you sent.
How it works
You open a challenge
When the customer adds a withdrawal address, you create a challenge with the network, the address and the customer’s identifier in your system. Clav returns the exact message to be signed and a link to the signing page.
The customer signs
Send the customer to the link. The page connects their wallet and asks for the signature, with your organization’s name front and center so they recognize the request. If you prefer, show the message in your own interface and collect the signature there.
Clav checks and records
The signature is checked against the address. If it matches, the address becomes a verified wallet. If it does not, the challenge ends as failed and the reason is recorded.
You check before every withdrawal
When releasing a withdrawal, ask the API whether the address is verified. The answer accounts for revocations made after the proof.
From the platform, no integration
For a pilot you can skip the code. In Verified wallets, click Verify a wallet, fill in Customer identifier, Network and Address, and click Create link. Send the link to the customer through the channel you already use with them.
The screen has two tabs: wallets already verified and challenges waiting for a signature. Opening a wallet shows the signed message, the method used and the full event trail.
From the API
The integration uses the REST API at https://api.clav.tech/v1, authenticated
with an API key that has the wallet:verify scope. The API keys
guide shows how to issue one. Every query is
restricted to the organization that owns the key.
Open a challenge
POST /v1/wallet-proofs
Authorization: Bearer <token>
Content-Type: application/json
{
"externalCustomerId": "customer-8842",
"chain": "EVM",
"address": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"ttlSeconds": 600
}ttlSeconds sets how long the challenge accepts a signature, between 60 and
86400 seconds. Without it, the challenge lasts 600 seconds (10 minutes).
{
"id": "65f0a1b2c3d4e5f60718293a",
"nonce": "a3f1c0d9e8b7a6f5e4d3c2b1a0998877665544332211ffeeddccbbaa99887766",
"message": "...",
"expiresAt": "2026-09-16T12:10:00.000Z",
"verificationUrl": "https://.../p/a3f1c0d9..."
}Send the customer to verificationUrl. If you collect the signature yourself,
show message exactly as it came: one different character produces another
signature and the proof fails.
The message is signed byte for byte
Do not translate, reformat or strip line breaks from message. Clav rebuilds
the original text to check the signature.
Follow a challenge
GET /v1/wallet-proofs/{nonce}The status field moves through these states:
status | Meaning |
|---|---|
PENDING | Waiting for the signature. |
VERIFIED | Signature checked. The address became a verified wallet. |
FAILED | The signature was rejected. See failureReason. |
EXPIRED | A signature arrived after the deadline. |
A challenge nobody tried to sign stays PENDING even after its deadline. To
know whether it is still valid, compare expiresAt with the current time.
A challenge accepts a single signature. If it is rejected, open a new challenge.
To be notified without polling, subscribe to the wallet.verified and
wallet.failed webhooks.
Check before the withdrawal
GET /v1/wallets?chain=EVM&address=0x5b38da6a701c568545dcfcb03fcb875f56beddc4{
"verified": true,
"wallet": {
"id": "65f0a1b2c3d4e5f60718293b",
"chain": "EVM",
"address": "0x5b38da6a701c568545dcfcb03fcb875f56beddc4",
"externalCustomerId": "customer-8842",
"method": "SIGNATURE",
"status": "ACTIVE",
"challengeId": "65f0a1b2c3d4e5f60718293a",
"verifiedAt": "2026-09-16T12:04:11.000Z",
"revokedAt": null
}
}Release the withdrawal based on verified alone. It is true only when there
is a proof with status ACTIVE. A revoked wallet still shows up in wallet,
but with verified: false. An address that is invalid for the network also
answers verified: false, with no HTTP error.
Revoke a wallet
POST /v1/wallets/{id}/revoke
Content-Type: application/json
{ "reason": "Ticket 4412: customer removed the address" }From the response onward, the check returns verified: false and the customer
has to prove the address again. The proof stays on record, with status
REVOKED, and the reason goes to the audit trail.
Failure reasons
failureReason | What happened |
|---|---|
INVALID_ADDRESS | The address does not belong to the given network. |
MALFORMED_SIGNATURE | The signature could not be read or is not valid. |
ADDRESS_MISMATCH | The signature is valid, but another address made it. |
UNSUPPORTED_SCHEME | The wallet used a signature scheme we do not accept. |
CHALLENGE_EXPIRED | The challenge deadline passed. |
ATTEMPTS_EXCEEDED | The challenge received too many attempts. |
VERIFIER_UNAVAILABLE | The signature could not be checked at that moment. |
On ADDRESS_MISMATCH, Clav keeps the address that actually signed. It is useful
data for an anti-money-laundering investigation.
Frequently asked questions
Does Clav receive the customer's name or tax id?
No. You send only externalCustomerId, an identifier that makes sense in your
system. Clav stores and returns that value but never interprets it.
Does the customer need a Clav account?
No. The signing page opens from the challenge link and does not ask for a login.
What happens if the customer verifies the same address again?
The existing verified wallet is updated with the new proof. No duplicate record is created.
Can I call the wallet check on the withdrawal path?
Yes, it was built for that. It is a single indexed read.
Is a VERIFIED challenge enough to release the withdrawal?
No. The challenge is the record of how the wallet was proved. To decide on the
withdrawal, use GET /v1/wallets, which also accounts for later revocations.