Skip to main content

Overview

If your product trades on behalf of end users, the tempting shortcut is to run everyone through one account and track ownership in your own database. It works until the first time you need to answer a real question: what is this user’s PnL, what happens if they want to withdraw, and how do you stop one user’s losses touching another’s collateral. Polymarket has no concept of sub-accounts. Bravado provides one through master-key provisioning: your partner key can create end users, each with their own wallet and their own scoped API key. This guide covers provisioning them safely, including the one place where a deterministic idempotency key is the right call.
Read time: about 13 minutes. Backend engineering context assumed. This is server-side work throughout.

TL;DR

  • A master key carries trade.users and can provision. A user key is bound to one wallet.
  • POST /v2/trade/users creates a wallet and returns a key bound to it. The key is shown once.
  • Use a deterministic idempotency key here, derived from your user id, so a retry cannot mint a second wallet.
  • Keys live server-side. Never ship one to a browser or mobile app.
  • Scope user keys to what they need. trade.withdraw is not included by default and should usually stay off.
  • Rate limits are per key, so each user gets their own budget rather than competing for one.

What you will do

  • Confirm your master key can provision
  • Create a user with an identifier from your own system
  • Store the returned key safely and map it to your user
  • Understand why this call in particular needs a deterministic idempotency key
  • Scope user keys to limit blast radius
  • Show a user their own balances, positions, and history

What you will need

Knowledge
  • Backend engineering, and a secrets store you trust
Tools and access
  • A Bravado master key with the trade.users scope
  • Somewhere encrypted to keep per-user keys

Confirm the master key

Two things to check in the response:
  • api_key.scopes contains trade.users
  • binding describes the partner rather than a single user
A key without trade.users cannot provision, and the failure is a 403 that looks like an auth problem rather than a scope problem.

Key types

Provision a user

Pass your own identifier as external_id so you can map the result back to your user record without maintaining a separate lookup.
The returned key is shown once. Store it in a secrets manager, encrypted at rest, keyed by your user id. Treat it exactly as you would a password. It can trade, and with the wrong scope it can move funds.

Why this call needs a deterministic key

Everywhere else in these guides, the advice is a fresh UUID per intended action. Provisioning is the exception. A duplicate order is bad. A duplicate wallet is worse: you now have one user with two wallets, funds split across them, positions in both, and no clean way to merge. Nothing in the API will stop you, because two provisioning calls with different idempotency keys are two legitimate requests. Keying on your own user id makes that impossible:
Retry it a hundred times, from a hundred workers, and you get one wallet and the same response every time.
This is the opposite of the guidance in Safe retries, and deliberately so. There, a random key per intended order is right because two identical orders are usually two real intentions. Here, two provisioning calls for the same user are never two intentions.

Architecture

1

User signs up in your product

They never see Bravado. Your app is the entire interface.
2

Your backend provisions

Call POST /v2/trade/users with the master key and your user id.
3

Store the key server-side

Encrypted, mapped to your user. It never leaves your infrastructure.
4

Trade on their behalf

Use that user’s key so positions, balances, and PnL are attributed to them.
Requests must originate from your backend. A user key in a browser bundle or mobile app can be extracted, and it can trade. Your frontend talks to your API; your API talks to Bravado. There is no safe way to shortcut this.

Scope user keys deliberately

Scopes limit what a compromised or misused key can do:
trade.withdraw is not included in a standard partner key and must be requested explicitly. If withdrawals run through your own flow rather than per-user, keep it off user keys entirely. It is the one scope where a mistake is irreversible.

Per-user rate limits

Limits are per key, so a hundred provisioned users have a hundred budgets rather than sharing one:
Worth reflecting in your own scheduling. A naive implementation that funnels all user polling through one shared worker will hit a self-imposed bottleneck that does not exist at the API level.

Show a user their account

Everything a normal integration does works with that user’s key:
And because each user has a real on-chain wallet address, the Data API works on them too. That means a provisioned user gets the same PnL history, category breakdown, and tax statements available for any public wallet, with no extra plumbing:

Wrapping up

One master key provisions many users, each with a real wallet and a scoped key. That gives you per-user attribution, per-user rate limits, and per-user risk isolation without building any of it. Two rules carry the weight: keys stay on your backend, and provisioning uses a deterministic idempotency key. The first prevents a class of compromise; the second prevents a mess that is genuinely painful to unwind.

Frequently asked questions

No. It can be extracted, and it can trade. Route every request through your backend.
Two orders with the same parameters are usually two real intentions, so a random key per order is correct. Two provisioning calls for the same user are never two intentions, so keying on the user id makes duplicates impossible.
It is shown once at creation. Contact support to rotate it rather than provisioning again, since a second provisioning call with a different identifier creates a second wallet.
No. Limits are per key, so each provisioned user has their own budget. Read it from GET /v2/trade/account with that user’s key.
Usually not. It is excluded by default and must be requested. If your product handles withdrawals through its own flow, keeping it off user keys removes the possibility of funds leaving by an unintended path.
Yes. Their wallet is a real on-chain address, so every Data API endpoint works on it, including R1 dispositions and per-year tax reports. See Generate a tax statement.

Resources