Last updated

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

Applicant walletTRP APIYour backend (VASP)Applicant walletTRP APIYour backend (VASP)alt[Wallet not set at creation]POST /auth/tokenJWTPOST /forms/satoshi-test/sessionssession + applicantFormPathShare link /f/{formPublicId}/{sessionPublicId}GET /forms/.../public/... (poll)POST /forms/.../public/.../claimSend exact micro-amount on-chainGET /forms/.../public/... (poll until VERIFIED)GET /wallet-verification/sessions/{publicId}VERIFIED + txId

Every session moves through these statuses:

StatusMeaning
CREATEDSession exists; applicant must claim a wallet (only when no wallet was provided at creation).
AWAITING_PAYMENTWallet is known; waiting for the micro-transaction.
PENDING_CONFIRMATIONSPayment detected; waiting for required block confirmations.
VERIFIEDOwnership confirmed.
FAILEDVerification failed (wrong amount, provider error, etc.).
EXPIREDSession 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

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"
}
FieldRequiredDescription
networkyesbitcoin, ethereum, or trc20.
walletAddressnoWallet that will send the payment. If omitted, the applicant enters it on the public form.
tokenContractTRC-20 onlyTRC-20 contract address. Defaults to USDT mainnet when omitted.
applicantPublicIdnoLinks 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"
}
FieldRequiredDescription
walletAddressyesAddress that will send the micro-transaction.
tokenContractTRC-20 onlyRequired when network is trc20.
emailnoStored 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:

ParameterDefaultBehaviour
(none)Sync with the blockchain provider when cooldown allows.
sync=falseRead-only; no provider call.
sync=trueForce 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 tokenContract for 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 pollIntervalMs from the public form response or poll your backend session every few seconds.

API reference

For error codes see Error reference.