Satoshi Test Guide
The Satoshi Test verifies that a person controls a self-hosted crypto wallet by asking them to send a unique micro-transaction from that wallet to a TRP-controlled deposit address. The exact amount is generated per session so the payment can be matched on-chain.
Use it when you need proof of wallet ownership during onboarding, Travel Rule compliance, or before allowing withdrawals to an external address.
How it works
Every session moves through these statuses:
| Status | Meaning |
|---|---|
CREATED | Session exists; applicant must claim a wallet (only when no wallet was provided at creation). |
AWAITING_PAYMENT | Wallet is known; waiting for the micro-transaction. |
PENDING_CONFIRMATIONS | Payment detected; waiting for required block confirmations. |
VERIFIED | Ownership confirmed. |
FAILED | Verification failed (wrong amount, provider error, etc.). |
EXPIRED | Session TTL elapsed before completion. |
Supported networks: bitcoin, ethereum, trc20 (TRON TRC-20).
1. Authenticate
Same as the rest of the TRP API — exchange your API Key for a JWT:
POST /auth/token
x-api-key: <YOUR_API_KEY>See POST /auth/token.
2. Create a verification session
Option A — Forms API (recommended)
The Forms API returns a ready-to-share applicant link and a schema-driven UI contract if you build your own operator dashboard.
Step 2a — fetch operator form schema (optional)
GET /forms/satoshi-test/operator-schema
Authorization: Bearer <jwt>Returns field definitions (network, optional walletAddress, optional tokenContract for TRC-20, optional applicantPublicId).
Step 2b — create session
POST /forms/satoshi-test/sessions
Authorization: Bearer <jwt>
Content-Type: application/json
{
"values": {
"network": "bitcoin",
"walletAddress": "",
"applicantPublicId": "01900000-0000-7000-8000-000000000001"
}
}You may send fields at the top level instead of under values:
{
"network": "ethereum",
"walletAddress": "0xabcdef1234567890abcdef1234567890abcdef12"
}| Field | Required | Description |
|---|---|---|
network | yes | bitcoin, ethereum, or trc20. |
walletAddress | no | Wallet that will send the payment. If omitted, the applicant enters it on the public form. |
tokenContract | TRC-20 only | TRC-20 contract address. Defaults to USDT mainnet when omitted. |
applicantPublicId | no | Links the session to an existing applicant record in your project. |
Example response:
{
"status": true,
"data": {
"publicId": "01900000-0000-7000-8000-0000000000aa",
"status": "CREATED",
"network": "BITCOIN",
"walletAddress": "",
"formPublicId": "01900000-0000-7000-8000-0000000000bb",
"applicantFormPath": "/f/01900000-0000-7000-8000-0000000000bb/01900000-0000-7000-8000-0000000000aa",
"instructions": {
"targetAddress": "bc1q…",
"expectedAmount": "0.00042",
"requiredConfirmations": 1,
"expiresAt": "2026-06-19T14:30:00.000Z"
}
}
}Share the applicant link by prefixing applicantFormPath with your TRP Dashboard host, for example:
https://app.travel-rule.com/f/{formPublicId}/{sessionPublicId}Option B — Wallet Verification API (direct)
Use this when you already know the wallet address and do not need the hosted applicant form.
POST /wallet-verification/sessions
Authorization: Bearer <jwt>
Content-Type: application/json
{
"network": "trc20",
"walletAddress": "TXyz…",
"tokenContract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
}walletAddress is required on this endpoint. The response has the same session shape as the Forms API but without applicantFormPath unless the session was created through a form.
3. Applicant flow (public, no JWT)
These endpoints are public — the applicant does not need a TRP account or API key.
Poll current form step
GET /forms/{formPublicId}/public/{sessionPublicId}The response is a declarative form schema that changes with session status:
- claim_wallet — collect wallet address (and email if not provided earlier)
- instructions — show deposit address, exact amount, QR code
- pending — payment detected, waiting for confirmations
- verified / failed — terminal states
Poll every pollIntervalMs milliseconds (default 8000). Pass ?sync=false to read the cached status without triggering a blockchain refresh.
Claim wallet (when required)
If the operator did not supply walletAddress at session creation, the applicant must claim their wallet first:
POST /forms/{formPublicId}/public/{sessionPublicId}/claim
Content-Type: application/json
{
"walletAddress": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"email": "applicant@example.com"
}| Field | Required | Description |
|---|---|---|
walletAddress | yes | Address that will send the micro-transaction. |
tokenContract | TRC-20 only | Required when network is trc20. |
email | no | Stored on the session when not collected at setup time. |
After a successful claim the session moves to AWAITING_PAYMENT and the next poll returns payment instructions.
4. Track status from your backend
Poll the session from your VASP backend (JWT required):
GET /wallet-verification/sessions/{publicId}
Authorization: Bearer <jwt>Query parameters:
| Parameter | Default | Behaviour |
|---|---|---|
| (none) | — | Sync with the blockchain provider when cooldown allows. |
sync=false | — | Read-only; no provider call. |
sync=true | — | Force sync, bypass cooldown. |
Force refresh explicitly:
POST /wallet-verification/sessions/{publicId}/refresh
Authorization: Bearer <jwt>When status is VERIFIED, use payment.txId and walletAddress as proof of ownership in your compliance workflow.
Integration tips
- Session limits — each VASP can have a limited number of active sessions at once (default 3). Expired sessions are cleaned up automatically.
- TTL — sessions expire after a configurable number of minutes (default 20). The applicant must complete payment before
instructions.expiresAt. - Exact amount — the applicant must send exactly
instructions.expectedAmount. Partial or rounded amounts will not match. - TRC-20 — always specify
tokenContractfor non-USDT tokens. If the contract address equals the wallet address, TRP defaults to USDT TRC-20 mainnet (TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t). - Rate limiting — all Satoshi Test endpoints share a rate limiter. Avoid tight polling loops; use
pollIntervalMsfrom the public form response or poll your backend session every few seconds.
API reference
| Endpoint | Auth | Purpose |
|---|---|---|
GET /forms/satoshi-test/operator-schema | JWT | Operator form schema |
POST /forms/satoshi-test/sessions | JWT | Create session + applicant link |
GET /forms/{formPublicId}/public/{sessionPublicId} | — | Applicant form (poll) |
POST /forms/{formPublicId}/public/{sessionPublicId}/claim | — | Applicant claims wallet |
POST /wallet-verification/sessions | JWT | Direct session creation |
GET /wallet-verification/sessions/{publicId} | JWT | Session status |
POST /wallet-verification/sessions/{publicId}/refresh | JWT | Force blockchain sync |
For error codes see Error reference.