Build on Gloam
Partner program and API
Add private payments to your app with one API key. You set your own fee, and every payment your key brings in is counted to you as it happens. On testnet nothing is charged.
- Base URL
- https://gloam.trade/api/v1
- Auth
- Bearer API key
- Format
- JSON, version 1
- Charged
- Nothing on testnet
How the program works
Your users keep their own keys and their own money. They prove each payment in their browser or agent with the SDK. Your server sends the proven payment through the Gloam relay with your API key, and that is how a payment is counted to you.
- Sign in at /partners/dashboard with your wallet. You sign a short message (Sign-In with Ethereum); it costs nothing and moves no money.
- Make a key. It is shown once. Gloam keeps only a keyed fingerprint of it (HMAC-SHA256 with a server secret), so nobody can read it back. Rename, rotate (a new secret, the old one stops at once) or revoke it any time.
- Set your fee and a payout address per network.
- Relay payments with the key. The portal shows your volume, your would-be fees and every payment, updated every 20 seconds.
What you can charge, and why
| Payment | Your fee | Limit | Why |
|---|---|---|---|
| Private payment | Flat, per payment | $0.00 to $1.00 | The amount is hidden from everyone, the relay included, so a share of it cannot be worked out. |
| Cash out | Share of the amount | 0% to 1% | The token and amount leaving the vault are public on chain. |
| Deposit | Share of the amount | 0% to 1% | The token and amount entering the vault are public too. |
Fees are in the token the user moved, paid to your payout address on that network. A payment message (the encrypted heads-up after a private payment) carries no fee. A change to your setting applies to new payments; earlier ones keep the fee they were counted with.
Testnet: nothing is charged
Gloam runs on Robinhood Chain testnet and Tempo testnet. Today the portal records what your key brought in and the fee your setting would have earned, and every figure is labeled that way. No fee is taken from anyone, and every API response says "charged": false.
Taking a fee for real needs a fee output inside the zero-knowledge circuits and the vault contract, so the fee is part of the proof and nobody can skip or change it. That is part of the mainnet ceremony and audit (see the production gate). Until then, a dashboard figure is an attribution count, not money owed.
USD figures use $1 for stablecoins and the live ETH price at the time of the payment. Stock tokens are counted but not valued.
Authentication and limits
Send the key in the Authorization header on every call except the index at GET /api/v1. Test keys (gloam_test_) work on testnets; live keys (gloam_live_) open with mainnet. Keep the key on your server: the API sends no CORS headers, so browsers on other sites cannot call it.
curl https://gloam.trade/api/v1/me \
-H "Authorization: Bearer $GLOAM_API_KEY"Each key gets 300 requests a minute across every endpoint. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds); a 429 adds Retry-After. The relay also keeps its own per-sender and per-network caps, with your key as the sender.
Responses and errors
Every response is JSON in the same envelope, with Cache-Control: no-store and a Gloam-Api-Version: 1 header. Quote the requestId when you ask us about a call.
// success
{ "v": 1, "ok": true, "data": { ... }, "requestId": "req_5e0c2a91d4b7f310" }
// failure
{ "v": 1, "ok": false,
"error": { "code": "invalid_key", "message": "This API key is not valid. It may have been revoked or rotated." },
"requestId": "req_9b3f..." }| Code | Status | Meaning |
|---|---|---|
missing_key | 401 | No Authorization header. Send Authorization: Bearer gloam_test_... |
invalid_key | 401 | Not a Gloam key, or a key that was revoked or rotated. |
key_env_mismatch | 403 | A live key on a testnet, or a test key on a mainnet. |
rate_limited | 429 | Over 300 requests in a minute for this key. Retry-After says when to retry. |
invalid_json | 400 | The body is not a JSON object. |
bad_request | 400 | A relay field is malformed (proof, root, nullifier, commitments, asset, to, amount, memo). |
network | 400 | chainId is not a network the relay serves (46630 or 42431). |
bad_action | 400 | action is not transfer, unshield or memo. |
screened | 403 | A cash out to a sanctioned address. The relay says nothing more. |
asset_policy | 403 | On Tempo, the token issuer's policy blocks this cash out. |
AlreadySpent | 409 | The note was already spent. |
UnknownRoot | 409 | The proof was made against a vault state the pool no longer knows. Resync and prove again. |
not_seen | 409 | A payment message for a payment that is not in the vault yet. |
InvalidProof, InsufficientPoolBalance, ... | 422 | The dry run against the pool reverted, with the pool's own reason. Nothing was sent. |
relay_off, relay_empty | 503 | The relay is not set up, or out of gas, on this network. |
rpc | 502 | The network did not answer. Try again. |
bad_tx, tx_not_found, tx_failed, not_a_deposit, too_old | 400 to 422 | Deposit attribution: the hash is malformed, not found, failed, not a deposit into Gloam's vault, or older than 24 hours. |
already_attributed | 409 | That deposit is already counted to a partner. |
invalid_proof | 400 | Not a Gloam proof, or a damaged one. |
bad_tag, unknown_network, unknown_asset, bad_amount | 400 | Payment request fields. |
no_live_pool, unknown_network | 404 | No live vault on that network, or an unknown network in the path. |
unavailable | 503 | Reading the vault took too long. Try again shortly. |
keys_unconfigured, storage_unavailable | 503 | The server is missing its key pepper or its store. |
internal | 500 | Our fault. The requestId helps us find it. |
Relay a payment
POST /api/v1/relaytakes the same body as the app's own relay. It screens, dry-runs and submits exactly as the app does, then counts the payment to you. The proof is the authorization and a cash out's recipient is fixed inside it, so the relay can only submit or refuse a payment, never redirect it.
# a private payment (transfer): counted as a private payment, flat fee
curl https://gloam.trade/api/v1/relay \
-H "Authorization: Bearer $GLOAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"chainId": 46630,
"action": "transfer",
"proof": "0x...", "root": "0x...", "nullifier": "0x...",
"commitments": ["0x...", "0x..."]
}'
# a cash out (unshield): counted with its public token and amount
-d '{ "chainId": 42431, "action": "unshield", "proof": "0x...", "root": "0x...",
"nullifier": "0x...", "asset": "0x20c0...", "to": "0xRecipient", "amount": "250000000" }'
# a payment message (memo): relayed, not counted
-d '{ "chainId": 46630, "action": "memo", "paymentCommitment": "0x...", "memo": "0x..." }'{
"v": 1, "ok": true,
"data": {
"hash": "0x9c1e...",
"chainId": 46630, "network": "robinhood", "action": "transfer",
"attribution": {
"recorded": true,
"kind": "private_payment",
"activity": {
"id": "act_...", "kind": "private_payment", "network": "robinhood",
"txHash": "0x9c1e...", "ts": 1791302400000,
"asset": null, "amount": null, "volumeUsd": null,
"feeUsd": 0.05, "feeToken": null, "feeBasis": "$0.05 per private payment"
}
},
"charged": false
},
"requestId": "req_..."
}The hash comes back as soon as the transaction is sent; wait for the receipt yourself. attribution.recorded is false with a reason (not_billable for memos) when nothing was counted; a payment that was sent is never failed over accounting. GET /api/v1/relay says which networks the relay can submit on right now.
Count a deposit
Deposits come from the user's own wallet, not the relay, so your app reports them: POST /api/v1/depositswith the transaction hash. The server reads the transaction from the chain. It must be a successful deposit into Gloam's vault, at most 24 hours old, and not already counted to anyone; the first partner to report it gets it.
curl https://gloam.trade/api/v1/deposits \
-H "Authorization: Bearer $GLOAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "chainId": 46630, "txHash": "0x..." }'
# 201: { "v": 1, "ok": true, "data": { "activity": { "kind": "deposit", "symbol": "USDG", "amount": "1000000000", ... }, "charged": false } }Your volume and fees
GET /api/v1/activity?limit=50 returns what the portal shows: totals, counts by kind and network, public volume and would-be fees by token, the last 14 days, and up to 200 recent payments.
{
"totals": { "privatePayments": 128, "cashOuts": 9, "deposits": 4,
"publicVolumeUsd": 5210.5, "commissionUsd": 19.43, "unpriced": 0 },
"byNetwork": { "robinhood": { "private_payment": 90, ... }, "tempo": { ... } },
"byAsset": [ { "symbol": "USDG", "network": "robinhood", "volume": "4210.5", "commission": "8.42" } ],
"daily": [ { "day": "2026-10-06", "privatePayments": 12, "cashOuts": 1, "deposits": 0,
"volumeUsd": 250, "commissionUsd": 1.23 } ],
"recent": [ ... ],
"fees": { "privatePaymentCents": 5, "cashoutBps": 25, "depositBps": 0 },
"charged": false
}Vaults and the leaf list
GET /api/v1/vaults (or /vaults/robinhood, /vaults/tempo) gives each network's vault address, the tokens it takes, whether the relay is up, and a fresh read of the tree size and root.
GET /api/v1/vaults/{network}/leavesis the vault's public leaf list: every commitment in order, with its leaf index, block and transaction. Rebuild the Merkle tree from it and check the root with the pool's isKnownRoot before you prove against it; the SDK's syncTree does the same from the chain.
curl https://gloam.trade/api/v1/vaults/tempo \
-H "Authorization: Bearer $GLOAM_API_KEY"
# data.vault: { "network": "tempo", "chainId": 42431, "pool": "0x841D...",
# "assets": [ { "symbol": "OUSD", ... } ], "relay": { "enabled": true, ... },
# "chain": { "block": "37...", "leafCount": 412, "root": "0x..." } }Check a proof
POST /api/v1/proofs/verify checks a proof of funds (gloamfunds1:), proof of payment (gloampay1:), payroll total (gloamroll1:) or balance disclosure (gloamdisc1:) on the server, with the same checks the /verifypage runs: the zero-knowledge proof, who it was made for, that it points at Gloam's own vault, that its plain fields match, and the live chain checks. ok is true only when every check passes and the proof has not expired.
curl https://gloam.trade/api/v1/proofs/verify \
-H "Authorization: Bearer $GLOAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "proof": "gloamfunds1:eyJ2IjoxLCJraW5kIjoi..." }'
# data: { "format": "gloamfunds1", "kind": "funds", "ok": true, "expired": false,
# "checks": [ { "label": "The proof checks out", "state": "pass", ... },
# { "label": "Made for \"Acme Bank\"", "state": "pass", ... }, ... ],
# "claims": { "chainId": 46630, "asset": "0x7E95...", "threshold": "1000000000", ... } }Payment request links
POST /api/v1/payment-requests makes a link that opens Pay in the Gloam app with the details filled in. The details ride after the #, so the payer's browser never sends them to a server. This endpoint builds the link and keeps nothing; you can also build the same link yourself (see private pay).
curl https://gloam.trade/api/v1/payment-requests \
-H "Authorization: Bearer $GLOAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "gloamr1.AbC...", "network": "robinhood", "asset": "USDG",
"amount": "1250", "note": "Invoice 042", "name": "Acme" }'
# 201: data.url = "https://gloam.trade/app/vault?tab=move&mode=pay#to=gloamr1...&amount=1250&asset=0x7E95..."Public figures
GET /api/v1/stats returns what /transparency shows for each network: what the vault holds, and counts of deposits, private transfers, cash outs and payment messages. Aggregates only, cached about a minute.
With the SDK
GloamApiClient in @gloamtrade/sdk wraps every endpoint, unwraps the envelope and throws GloamApiError with the code, status and request id.
import { GloamApiClient, settleGloamPayment } from "@gloamtrade/sdk";
const gloam = new GloamApiClient({ apiKey: process.env.GLOAM_API_KEY! });
await gloam.me(); // test the key
const { hash, attribution } = await gloam.relay(sendIntent);
await gloam.attributeDeposit({ chainId: 46630, txHash });
const report = await gloam.verifyProof(proofText);
const { url } = await gloam.paymentRequest({ to, asset: "USDG", amount: "25" });
// x402 sellers: sweep received payments through the relay with your key
await settleGloamPayment({ ...params, submit: gloam.submitter() });What is recorded
- Per payment: network, kind, transaction hash, time, the key that sent it, and the fee your setting gave it.
- Only for deposits and cash outs: the token and amount, which the chain already shows.
- Never: notes, secrets, balances, private amounts, or who received a private payment. Your API key is never stored, only its keyed fingerprint.