Watch And Earn docs
v4.0.0
Live demoConsole demo Get help
● Cash wallet · How users cash out

Payout methods
PayPal, crypto, bank, gift cards.

Each method is off until you turn it on. PayPal can be paid automatically through the PayPal Payouts API; crypto, bank transfers and gift cards, and PayPal without API credentials, are paid by you and then marked paid in the console.

PayPal Payouts APIManual payoutsEncrypted gift-card codes

01The four methods

Console → Payout methods (the methods map of config/public.wallet, admin/src/manifest40.ts). Amounts are whole cents of the wallet currency.

MethodDefault minimumProcessing text shown to usersThe user entersWho sends the money
PayPal500 (5.00)1-3 business daysA PayPal e-mail addressThe PayPal Payouts API (optional), or you by hand
Crypto1000 (10.00)1-3 business daysAn asset from your list and a wallet addressYou, from your own wallet
Bank transfer2500 (25.00)3-7 business daysThe bank fields you listYou, from your own bank
Gift cardsthe card's face valuewithin 48 hoursA card and one of its face valuesYou, with a code from your supplier

For every method you also set:

  • Offer this method: on or off (off by default).
  • Minimum request (not for gift cards).
  • Fixed fee and fee % charged to the user (not for gift cards): fee = fixed + amount × % (up to two decimals), rounded down to whole cents; the user receives amount − fee, which must be above 0 (firebase/functions/src/wallet/money.ts). Both default to 0.
  • Name in the app and processing time shown to the user.
  • Only in these countries: two-letter codes, empty = all. The country is the one the user picks in the payout sheet (it starts at the device region). It is a declaration, not a location check.
Your own costs are separate

PayPal fees, crypto network fees, bank charges and gift-card purchase prices are what you pay. The fee fields above are only what you choose to charge users. Check your providers' current pricing yourself.

02PayPal Payouts API, step by step

Optional. With it, an approved PayPal request is sent by PayPal when the owner presses Send via PayPal. Without it you pay each PayPal request yourself (Manual payouts). Steps from firebase/README.md §13 and firebase/functions/src/wallet/paypal.ts, which records the PayPal documentation it was checked against on 2026-10-07.

  1. A PayPal Business account with PayoutsPayouts is a PayPal Business feature. Live accounts need PayPal's approval for Payouts; ask PayPal if your account does not have it. Follow PayPal's User Agreement and Payouts terms.
  2. Create a REST app, Sandbox firstSign in at developer.paypal.com → Apps & Credentials → Sandbox tab → Create App. Open the app and copy its Client ID and Secret. Sandbox and live are different apps with different credentials.
  3. Enter the credentials in the consoleConsole → PayPal Payouts (owner only): PayPal environment = Sandbox (test money), PayPal REST app client id, PayPal REST app secret (write-only). Press the secret's Test button: it asks PayPal for an OAuth token in the chosen environment and reports ok or the error. The Status page has the same check, "PayPal Payouts credentials answer (OAuth token)". The token is never shown or stored.
  4. Add the webhookIn the same REST app → Webhooks → Add webhook. URL:
    https://REGION-PROJECT.cloudfunctions.net/paypalWebhook
    Replace REGION-PROJECT with your region and project id (for example us-central1-my-project; firebase deploy prints the exact URL). Select these events:
    PAYMENT.PAYOUTS-ITEM.SUCCEEDED
    PAYMENT.PAYOUTS-ITEM.FAILED
    PAYMENT.PAYOUTS-ITEM.RETURNED
    PAYMENT.PAYOUTS-ITEM.BLOCKED
    PAYMENT.PAYOUTS-ITEM.CANCELED
    PAYMENT.PAYOUTS-ITEM.REFUNDED
    PAYMENT.PAYOUTS-ITEM.UNCLAIMED
    PAYMENT.PAYOUTS-ITEM.HELD
    PAYMENT.PAYOUTSBATCH.DENIED
  5. Copy the webhook idPayPal shows an id for the new webhook. Paste it into console → PayPal Payouts → PayPal webhook id. The function checks every webhook with PayPal's verify-webhook-signature API using this id; an unverified call gets 401 and changes nothing.
  6. Test in sandboxCreate a sandbox personal account in the developer dashboard (Sandbox → Accounts) and use its e-mail as the destination of a test request. Approve it, press Send via PayPal as the owner, and watch it go to processing and then paid. Try a failing case too.
  7. Go liveCreate the app on the Live tab, enter its client id and secret, set the environment to Live (real money), add a live webhook with the same URL and events, and paste the live webhook id. Fund your PayPal balance before you send.

What happens when the owner sends

  • The request goes from approved to processing and the backend calls POST /v1/payments/payouts with one item: the user's e-mail and the net amount in the wallet currency. The request id is the sender_batch_id; PayPal refuses a reused id for 30 days, so a retry can never pay twice.
  • The webhook settles it: SUCCEEDED → paid; FAILED, RETURNED, BLOCKED, CANCELED, REFUNDED or a denied batch → failed and the amount returns to the user's available balance; UNCLAIMED, HELD → stays processing (PayPal returns unclaimed money after 30 days, which arrives as RETURNED). A RETURNED or REFUNDED after paid moves it to failed and refunds the wallet.
  • pollPaypalPayouts checks processing batches every 30 minutes in case a webhook is missed.
  • A send error (for example an insufficient PayPal balance) puts the request back to approved with the error in provider.lastError. The Payouts page shows "Last PayPal error". Fix the cause and press Retry PayPal send (owner), or reject the request.
Webhook answers

paypalWebhook answers 401 for a bad signature, 400 for bad JSON, 503 while PayPal is not configured, 405 for anything but POST, and 200 otherwise. Events are de-duplicated (paypalEvents). Firebase console → Functions → logs shows the details.

03Manual payouts: crypto, bank, gift cards (and PayPal by hand)

The backend never stores or asks for a private key, a seed phrase or a bank login. You send the money with your own tools and record it.

  1. ApproveConsole → Payouts → open the request → Approve (manager or owner).
  2. See where to send itLists show a masked destination. Press Show full destination to see what the user entered; this is audited and refused on the demo console.
  3. Send the money yourselfFrom your crypto wallet (check the asset and network match), your bank, PayPal's website, or buy the gift card from your supplier.
  4. Mark paidPress Mark paid and enter the reference: the transaction hash (crypto), the bank reference, the gift-card code, or the PayPal transaction id. Optional: your cost, what the payout really cost you (for example a discounted gift card); the Business page then uses it. Mark paid only after the money has really left.

Marking paid moves the amount out of the user's locked balance, adds it to lifetime paid, writes a cost event (payoutEvents) and an inbox message. Reject (with a reason the user sees) returns the amount to the user's available balance.

Crypto assets

Console → Payout methods → Crypto assets, one per line as id|name|address pattern. Defaults:

usdt-trc20|USDT (TRON)|^T[1-9A-HJ-NP-Za-km-z]{33}$
usdt-polygon|USDT (Polygon)|^0x[0-9a-fA-F]{40}$
usdc-base|USDC (Base)|^0x[0-9a-fA-F]{40}$
btc|Bitcoin|^(bc1[0-9a-z]{11,71}|[13][1-9A-HJ-NP-Za-km-z]{25,34})$
ltc|Litecoin|^(ltc1[0-9a-z]{11,71}|[LM3][1-9A-HJ-NP-Za-km-z]{26,33})$

The id is lower-case letters, digits, - and _ (up to 40). The pattern is a regular expression from ^ to $, up to 300 characters; the app and the backend check every address with it (reason bad_address). A line that is not valid is skipped and listed on the Status page. A pattern checks the format only, not that the address exists or is on the right network: double-check before you send. Amounts are in the wallet currency; you decide the exchange rate when you send.

Bank fields

Console → Payout methods → Bank details the user fills in, one field id per line. Default accountHolder, iban, bic; for US accounts for example accountHolder, accountNumber, routingNumber. Every field is required (up to 64 characters). A field named iban is checked with the IBAN checksum (reason bad_iban).

Gift-card catalog

Console → Payout methods → Gift-card catalog, one card per line as id|brand|face values in cents|countries|your cost in cents, for example store-card|Store card|500,1000,2500|US,CA|. The brand is text you type: the app shows no logo you do not own, so do not add third-party logos. Gift cards stay hidden while the catalog is empty. The user always pays the face value (no fee); "your cost" is for your own records.

Brands and resale

Only list brands whose cards you can legally buy and pass on to users in their countries, under the brand's and your supplier's terms.

04Gift-card codes are stored encrypted

  • The code you enter at Mark paid is encrypted with AES-256-GCM before it is stored (provider.giftcardCode, firebase/functions/src/wallet/codes.ts). The console's copy of your action is overwritten with "[stored encrypted]".
  • The key is generated by the backend the first time a code is stored and kept in config/private.wallet.codeKey. You never type it, and the console keeps it when it rewrites config/private.
  • Only the user who owns the request can read the code, in the app (revealGiftCode); the first reveal is recorded. The inbox message only says the card is ready; the code is never written in clear anywhere. The console help says "shown to the user once", but as built the owner can reveal it again later.
Back up Firestore

If config/private.wallet.codeKey is lost, stored codes cannot be decrypted. Include config/private in your Firestore backups and never delete that field by hand.

05Limits that apply to every method

Console → Wallet & payouts. Checked by requestPayout on the server (firebase/functions/src/wallet/payouts.ts):

SettingDefaultEffect
Payout limit per user per day (cents)5000All methods together, UTC day. 0 = no limit.
Payout limit per user per month (cents)50000UTC month. 0 = no limit.
Account age needed for a payout request (days)3Requests from younger accounts are refused (account_age).
First payout needs a verified e-mail (or Apple/Google sign-in)onGuest accounts must link Google or Apple first (email_unverified). As built the check runs on every request, not only the first.
Always review requests above (cents)2000Even with auto-approve on, a bigger request waits for you.
Auto-approve small requestsoffSee Auto-approve.

Also fixed in code: 3 requests per user per hour; one request per clientRequestId (a repeated tap returns the same request); the amount must not exceed the available balance. More in Fraud signals and limits.