SectionsOverview
The plusmore RaaS API
One API instead of a deal with every gift card brand. With it a partner resells plusmore’s live gift card catalogue and keeps the margin on every sale. Demo store open now. Sandbox open now. Production by approval. Start with the sandbox below. In the preview reference after it, names and routes may change before production opens.
A live gift card catalogue
Amounts and live availability for each gift card brand, refreshed as plusmore’s float moves.
A float you control
Fund it in USDC on Base from a wallet you register; you can never spend more than you deposited.
One check, one order
A pre payment check before you take money, then an order with your own idempotency key.
Idempotent by design
The same order reference always replays the same gift card; a payment can never issue two.
A sandbox to start
Create a key with no name and no email, then build on the real API with demo gift cards.
Every order, tracked
Follow every order by its reference, from the pre payment check to the gift card’s delivery.
Try the sandbox
The sandbox is the plusmore RaaS API with nothing real behind it. Every call, every error and every event behaves as it will in production, against two fictional brands, Demo Coffee and Demo Books, with a test balance and demo gift card codes that have no value. Nothing you do in the sandbox can move real money.
Get a key in one click. Press Create a sandbox key. We ask for no name and no email. You get three values, shown once:
| Value | Looks like | What it is for |
|---|---|---|
| API key | pmr_test_ then a key id and a secret | the Authorization: Bearer header of every request |
| Signing secret | pmrsig_test_... | signing every request (never sent) |
| Webhook secret | pmrwh_test_... | checking the signature of every event (never sent) |
Store all three in your secrets manager. A sandbox key starts with a test balance of 100 US dollars, works for 30 days, and can be revoked by its holder at any time (POST /v1/sandbox/keys/revoke). An address can create two keys a day. A key and everything it made are deleted 2 days after the key expires.
Create a sandbox key
No name and no email. One press runs a short security check, then your API key, signing secret and webhook secret appear here, shown once.
What is in the sandbox
| Gift card | Slug | Amounts | Rate |
|---|---|---|---|
| Demo Coffee (US, USD) | demo-coffee | 10.00, 25.00, 50.00 | 5% |
| Demo Books (GB, GBP) | demo-books | any amount from 10.00 to 100.00 | 3% |
| Demo Books, limited today | demo-limited | 10.00 to 50.00 today | 3% |
Your cost is the face value less the rate: cost_minor = face_minor - floor(face_minor * rate_bps / 10000). Your balance is in US dollars with six decimals (usd6); the sandbox converts at fixed rates (1 GBP = 1.25 USD), never market rates.
Test values. Each one reaches a path you must handle in production:
| Send | You get |
|---|---|
Demo Books at 13.37 (face_minor 1337) | 503 restocking, retry after an hour |
| Demo Books at 66.60 | 502 upstream_failed, nothing charged; retry with the same client_ref |
| Demo Books at 99.99 | 409 price_changed with the new cost (122.50 US dollars, above a new key's 100) and a fresh quote in details; fund first, then order again with that quote_id (quickstart step 8) |
demo-limited above 50.00 | 409 denomination_unavailable (limited today) |
end_customer.country XX | 451 region_blocked |
a client_ref starting pending- | the first one each day answers 202 pending and issues 30 seconds later |
Codes. Every issued order carries a code like DEMO-7K2M-Q9XA-4TBC. Its link opens a page that says "Demo gift card. It has no value." Demo codes can never be redeemed anywhere.
Funding. In the sandbox, POST /v1/sandbox/deposits/simulate with { "plus_amount": "100" } credits your test balance at once, converted at a fixed reference price of 3.00 US dollars per test PLUS, less the 0.30% pool fee: up to 10,000 test PLUS a deposit and 50 deposits a key. On chain test funding with test PLUS on Base Sepolia comes next.
Events. v0 sends no webhooks out. GET /v1/sandbox/events shows every event as the delivery your endpoint would get: the raw body and the real plusmore-signature header, signed with your webhook secret at the time you read it, so you can test your verification today. POST /v1/sandbox/webhooks/test makes a test event of any type (50 a key).
Quickstart (curl)
Every request carries the API key and a signature. The signature is an HMAC-SHA256, keyed by your signing secret, over seven lines joined by a newline:
v1
<METHOD>
<path>
<query, sorted by name, RFC 3986 encoded, or an empty line>
<x-plusmore-timestamp: unix seconds, within 300 seconds of our clock>
<x-plusmore-nonce: a new UUID for every request>
<hex SHA-256 of the raw body; for a GET, of zero bytes>and is sent as x-plusmore-signature: v1=<hex>. In the query a space is %20 and a plus sign is %2B: a raw +, a raw space or a # anywhere in the request target is refused with 400 invalid_request, so there is one way to write each request and the signature covers exactly what we read. A POST /v1/orders also carries Idempotency-Key, equal to the body's client_ref. When a signature fails, the sandbox answers 401 signature_invalid with an x-plusmore-signature-debug header that holds the reason, the SHA-256 it computed for your body and the exact string it signed (base64url): compare it with yours. Production never explains a failure.
1. Set your values and the signing function (bash, curl and openssl; nothing to install on macOS or Linux)
export RAAS_API_KEY='pmr_test_...'
export RAAS_SIGNING_SECRET='pmrsig_test_...'
# plusmore_call METHOD PATH [CANONICAL_QUERY] [BODY]: one signed request to the sandbox
plusmore_call() {
local METHOD="$1" REQ_PATH="$2" CANONICAL_QUERY="${3:-}" BODY="${4:-}"
local TS NONCE BODY_SHA CANONICAL SIG URL
if [[ "$REQ_PATH$CANONICAL_QUERY" == *[\ +#]* ]]; then
echo 'encode a space as %20 and a plus sign as %2B: a raw space, + or # is refused' >&2
return 2
fi
TS=$(date +%s)
NONCE=$(uuidgen | tr 'A-Z' 'a-z')
BODY_SHA=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')
CANONICAL=$(printf 'v1\n%s\n%s\n%s\n%s\n%s\n%s' "$METHOD" "$REQ_PATH" "$CANONICAL_QUERY" "$TS" "$NONCE" "$BODY_SHA")
SIG=$(printf '%s' "$CANONICAL" | openssl dgst -sha256 -hmac "$RAAS_SIGNING_SECRET" -hex | sed 's/^.* //')
URL="${RAAS_BASE:-https://raas-sandbox.plusmore.xyz}$REQ_PATH"
if [ -n "$CANONICAL_QUERY" ]; then URL="$URL?$CANONICAL_QUERY"; fi
local ARGS=(-sS -X "$METHOD" "$URL"
-H "Authorization: Bearer $RAAS_API_KEY"
-H "x-plusmore-timestamp: $TS" -H "x-plusmore-nonce: $NONCE" -H "x-plusmore-signature: v1=$SIG")
if [ -n "$BODY" ]; then ARGS+=(-H "content-type: application/json" --data-binary "$BODY"); fi
if [ -n "${IDEMPOTENCY_KEY:-}" ]; then ARGS+=(-H "Idempotency-Key: $IDEMPOTENCY_KEY"); fi
curl "${ARGS[@]}"
}2. Check your key
plusmore_call GET /v1/ping3. List the catalogue (query parameters sorted by name)
plusmore_call GET /v1/catalogue 'country=GB&limit=10'4. Get a quote
plusmore_call POST /v1/quotes '' '{"slug":"demo-coffee","currency":"USD","face_minor":2500}'5. Place an order (the client_ref is your own id for the purchase; send the same one to retry safely)
IDEMPOTENCY_KEY=order-0001 plusmore_call POST /v1/orders '' \
'{"client_ref":"order-0001","slug":"demo-coffee","currency":"USD","face_minor":2500,"end_customer":{"ref":"customer-42","country":"GB"}}'The answer is 201 with the order and its demo code. Sending the same request again answers 200 with the same order and "replay": true, and charges nothing twice. Use an opaque id for end_customer.ref, never an email or a name: we keep only its SHA-256.
6. Read the order and your balance
plusmore_call GET /v1/orders/order-0001
plusmore_call GET /v1/balance7. See your events and check their signature
plusmore_call GET /v1/sandbox/events 'limit=5'Each delivery has raw_body and headers["plusmore-signature"] (t=<unix>,v1=<hex>). To check one, compute the HMAC-SHA256 of <t>.<raw body> keyed by your webhook secret, compare it with v1 in constant time, and refuse a t more than 300 seconds from your clock:
printf '%s' "$T.$RAW_BODY" | openssl dgst -sha256 -hmac "$RAAS_WEBHOOK_SECRET" -hex | sed 's/^.* //'Always check the signature over the raw bytes before you parse them, and deduplicate on plusmore-event-id. Events never carry a gift card code: read it with GET /v1/orders/{client_ref}.
8. Try a test value: a price change (Demo Books at 99.99)
After the price change this gift card costs 122.50 US dollars, more than a new key's 100 US dollar test balance, so fund your test balance first (20 test PLUS credit 59.82 US dollars):
plusmore_call POST /v1/sandbox/deposits/simulate '' '{"plus_amount":"20"}'
IDEMPOTENCY_KEY=order-0002 plusmore_call POST /v1/orders '' \
'{"client_ref":"order-0002","slug":"demo-books","currency":"GBP","face_minor":9999,"end_customer":{"ref":"customer-42","country":"GB"}}'The order answers 409 price_changed with the new cost and a fresh quote_id in details. Nothing was booked, so send the same order again with that quote:
IDEMPOTENCY_KEY=order-0002 plusmore_call POST /v1/orders '' \
'{"client_ref":"order-0002","slug":"demo-books","currency":"GBP","face_minor":9999,"quote_id":"<details.quote_id>","end_customer":{"ref":"customer-42","country":"GB"}}'The answer is 201 with the order at the new cost and its demo code.
Reference notes
- Envelope. Success:
{ "data": ..., "request_id": "req_..." }. Error:{ "error": { "code", "message", "request_id", "retry_after_sec"?, "details"? } }. Every sandbox answer carriesx-plusmore-env: sandboxandx-request-id. - Keys.
pmr_test_keys work only onraas-sandbox.plusmore.xyz; apmr_live_key there answers403 wrong_environment, and the reverse holds in production. - Limits in the sandbox. Per key, every second: 5 reads, 5 quotes, 2 orders and 2 other writes; per address, 30 requests a second. A too fast request answers
429 rate_limitedwithRetry-After. Per key, for its whole 30 days: 200 test orders, 50 simulated deposits and 50 test events; a spent limit answers429 limit_exceededwith the limit and its use (a new key starts afresh). New keys: two a day per address (one request a second) and up to 25 a day for the whole sandbox; when the day's keys are taken, key creation answers503 capacity_unavailableuntil the next UTC day. When the sandbox holds all the keys it can store, key creation answers503"The sandbox is full right now. Try again later."; keys already issued keep working. - Requests. JSON bodies of at most 8 KB; a request must reach us whole within 15 seconds; the query is RFC 3986 encoded (a space is
%20, a plus sign%2B), and a raw+or#in the request target answers400 invalid_request. - Quotes. A quote lives 60 seconds. An order that names an expired quote answers
409 price_changedwith a fresh quote indetails; order again with thatquote_id. - Statements.
GET /v1/statements?month=YYYY-MManswers the month's figures signed with Ed25519; check them against the key atGET /v1/sandbox/statement-key(fetch it once and pin it). - Not in the sandbox yet: outbound webhook delivery, test PLUS on Base Sepolia and its faucet, the partner portal.
- Production keys come after the go live review: the company checks, the one off access fee and the board's approval.
Authentication
Every request carries your key in the X-Supplier-Key header. The base URL and the key come with production access, which is by approval; nothing here is a real host or a real key.
GET /v1/brands?currency=GBP HTTP/1.1 Host: your base url, issued with production access X-Supplier-Key: your key, issued with production access
Money and numbers
| What | Shape |
|---|---|
| A face value or a wholesale price | Integer minor units, two decimals (face_minor: 2500 is 25.00 in that currency) |
| A rate | Integer basis points internally, shown to you as a percentage (net_rate_bps: 550 is the rate you keep on that brand) |
| Your float | USD with six decimals, the USDC unit on Base (usd6: 12630000 is 12.63 USD) |
{
"data": [{
"slug": "adidas", "name": "Adidas", "currency": "GBP",
"countries": ["GB"], "all_denominations": [10,25,50,100,250,500],
"denominations": [10,25,50,100], "range": null,
"prices": [{"face_minor":1000,"wholesale_minor":945}],
"net_rate_bps": 550, "cash_equivalent": false,
"availability": {"state":"limited","float_light":"green","max_denom_today":100},
"source": "plusmore"
}], "page": 1, "per_page": 50, "total": 222
}{
"data": {"base":"USD",
"rates": {"GBP":1.3252367947,"EUR":1.1403,"USD":1},
"source": "plusmore"}
}gift_cards_listed as your one headline count, labelled gift card brands: it is the same total GET /v1/brands pages through. Read it here, never hardcode it.{
"brand_slug": "tesco", "currency": "GBP", "face_minor": 2500
}{
"data": {"ok": true, "code": "OK",
"wholesale_minor": 2363, "wholesale_usd6": 31700000,
"ledger_balance_usd6": 100730000}
}| code | Meaning |
|---|---|
| OK | Continue to payment |
| LEDGER_INSUFFICIENT | Your float does not cover it; not retryable until you deposit more |
| SUPPLIER_RESTOCKING | Your float covers it, plusmore cannot fill it right now; retry after retry_after_sec |
| NOT_SOLD | Unknown brand, or an amount it does not sell |
| CAP_EXCEEDED | Over your per order or rolling 24 hour cap |
Request body
| Field | Required | Description |
|---|---|---|
| client_ref | Required | Unique per payment, at most 128 characters of letters, digits, underscore, dot, colon or hyphen. This is your idempotency key: derive it from the payment itself (for example the transaction hash), never mint a new one on retry. |
| brand_slug, currency, face_minor | Required | The gift card you are issuing, exactly as quoted by the check |
| wallet_ref | Required | Any string of 8 or more characters identifying your buyer; plusmore applies its per wallet caps to it |
| expected_wholesale_minor | Optional | The price the check quoted; a mismatch is refused rather than silently charging a different amount |
{
"client_ref": "tx-ab12...ef34",
"brand_slug": "adidas", "currency": "GBP", "face_minor": 1000,
"wallet_ref": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef12",
"expected_wholesale_minor": 945
}{
"data": {"id":"284e89c7...","client_ref":"tx-ab12...ef34",
"status":"issued","voucher":{"code":null,"pin":null,
"url":"https://...","expires_at":"..."},
"ledger":{"balance_usd6":21014394},"replay":false}
}Funding your float
Send USDC on Base to your deposit address, from a wallet you registered as one of your funding wallets. A transfer is credited once it is confirmed on chain; your float is a hard limit, you can never spend more than you deposited.
Errors and idempotency
Every error is one JSON object with error and code.
| HTTP | code | When |
|---|---|---|
| 401 | unauthorised | Missing or unknown key |
| 402 | LEDGER_INSUFFICIENT | Your float does not cover the order |
| 404 | unknown_brand, brand_not_available, unknown_order | |
| 409 | denomination_unavailable | That amount is not sold by this brand right now |
| 409 | price_changed | expected_wholesale_minor no longer matches the live quote |
| 409 | order_in_progress | The same reference is already being processed; retry in a few seconds |
| 429 | cap_exceeded | Over your per order or rolling 24 hour cap |
| 502 | issue_failed | plusmore could not issue the gift card; your debit is reversed, retry with the same reference |
| 503 | SUPPLIER_RESTOCKING | Cannot fill right now; retry after the time given |
Build your own gift card shop
Run your own gift card shop on the plusmore RaaS API and sell every gift card brand in the live catalogue. Start from the build prompt, the open source starter or the live demo; all three are free, and selling needs production access, which is by approval.
The prompt
Paste it into an AI coding agent. It builds the whole shop, then proves it with ten checks.
The starter
The finished shop, MIT licensed: catalogue, brand pages, a USDC checkout, an admin page.
The live demo
The starter on the live catalogue in DEMO mode, with selling switched off. No key needed.
Built for AI agents
An AI coding agent builds the shop from one open source prompt, and the prompt ends with ten acceptance checks, so the agent proves its own work before it calls the shop done.
An API an agent can run
Plain JSON over REST and one key that stays on your server, with answers an agent can act on: it can price, order, retry safely, watch its float and reconcile.
| The agent needs to | What the API gives it |
|---|---|
| Price a gift card before it takes the buyer’s money | POST /v1/orders/check books nothing and answers the wholesale price |
| Order without ever charging twice | client_ref: the same reference always resolves to the same gift card |
| Know what to do when an order cannot go through | Stable codes with a next step: LEDGER_INSUFFICIENT (top up the float), SUPPLIER_RESTOCKING (retry after the time given) |
| Watch its money and reconcile | GET /v1/float: the balance, the deposits and debits, and whether it is running low |
| Read the catalogue and its prices | GET /v1/brands, GET /v1/summary and GET /v1/fx |
A centralised shop your agent keeps running
The starter is a centralised shop. Your server holds the key, buyers pay USDC on Base straight to your wallet, and your server checks each payment on chain before it orders through the API. Your agent can keep working on it from the same repository: the branding, a catalogue refresh, float alerts and order lookups.
Run the starter
Download the starter, or paste the prompt into an AI coding agent. Either way the shop opens in DEMO mode: it browses the live catalogue with selling switched off, and needs no key.
Loading the prompt. If it does not load, use Download instead.
npm install cp .env.example .env.local npm run dev # then open http://localhost:3000
You need Node.js 18.18 or later. The starter’s README.md also runs LIVE mode on your own machine, against the mock of the API it ships with.
Make it your brand
The starter ships unbranded. Four settings make it yours, and the live demo lists the same steps in a panel for as long as it runs in DEMO mode.
- Name. Set
NEXT_PUBLIC_SHOP_NAMEto your shop’s name. It shows in the header and in every page title. - Logo. Set
NEXT_PUBLIC_SHOP_LOGO_URLto an https image, or put your file inpublic/and set it to its path, such as/logo.svg. Leave it empty and the header shows the name. - Colours. Change
--accent(your brand colour) and--accent-text(the text on it) inapp/globals.css, once in:rootfor light mode and once in the dark mode block. - Domain. Point your own domain at the shop and set
NEXT_PUBLIC_SHOP_DOMAINto it, so the message buyers sign names your shop. - Restart or redeploy. The shop reads these settings from its environment when it serves a page.
Go live
Selling needs production access, which is by approval; there is no self serve key yet. Once you are approved, six values switch the shop from DEMO mode to LIVE mode.
- , by approval from plusmore
- A wallet on Base that receives your buyers’ payments
- USDC in that wallet to fund your float
| Set this | What it is |
|---|---|
PLUSMORE_API_BASEPLUSMORE_API_KEY | From your production access. Server only; with both set the shop runs in LIVE mode. |
SHOP_WALLETNEXT_PUBLIC_SHOP_WALLET | Your wallet on Base that receives buyer payments, the same address in both. |
BASE_RPC_URL | A Base JSON RPC endpoint, used to check each payment on chain. |
ADMIN_PASSWORD | The password for your shop’s own admin page and float statement. |
Fund the float before your first sale: Float and funding explains deposits, and Errors and idempotency lists every code your shop can meet.
A DeFi store
plusmore’s own gift card shop settles on Base through a checkout contract and pays cashback. An on chain store like it, for your brand, is available on request: plusmore sets it up with you, it is not self serve.
Q1 2026 design exploration
Everything from here down was designed in Q1 2026, before the plusmore RaaS API above. It is kept for history and is not served; every endpoint on this part of the page is marked Not live.
Rewards as a Service developer docs
Integrate tokenised loyalty rewards into any fintech product. Issue on Base, let users redeem it for gift cards, stake in the USDC pool and settle everything on chain, all through one REST API. This is the design reference from Q1 2026.
Rewards on spend
Partners fund a reward pool and their users earn on qualifying spend, automatically.
Gift card redemption
Redeem for a gift card from gift card brands worldwide across many regions, delivered by email.
USDC pool
Stakers fund the shop and share its real margin. History, not a promise.
Built on Base
Low cost, fast finality, the Layer 2 by Coinbase. Every movement settles on Base.
Open banking insights
Transaction enrichment and spending insights feed the rewards engine. Design stage.
White label rewards
Embed a branded rewards experience in your app. See the RaaS platform page.
How RaaS works end to end
funds
spends
or stake
on Base
Authentication
Authorization header. Keys are scoped to your partner account.POST /v2/rewards/issue HTTP/1.1 Authorization: Bearer <your API key> Content-Type: application/json X-Partner-ID: partner_abc123
Base URL and versioning
Every endpoint is versioned; this reference describes v2. Base URLs are issued with partner keys, none is public today.
| Environment | Base URL | Key prefix |
|---|---|---|
| Production | Issued with your partner keys | pm_live_ |
| Sandbox | Issued with your partner keys | pm_test_ |
Sandbox environment
Error handling
Every error returns one JSON envelope with error, code and message fields.
{
"error": true,
"code": "INVALID_WALLET",
"message": "The provided wallet address is not a valid Base address.",
"request_id": "req_8f3a..."
}Rate limits
Requests are rate limited per API key. Past the limit the API answers 429 RATE_LIMITED. The reference table lists the limit per endpoint.
How issuance works
is issued from your partner reward pool. When a user completes a qualifying transaction, your backend calls the issuance endpoint to credit to their wallet. The transfer is confirmed on Base within seconds.
Key concepts
| Concept | Description |
|---|---|
| partner_pool | Your funded reward pool. Issuance debits from this pool. |
| wallet_address | The user’s Base wallet that receives . |
| reward_rate | The conversion rate from dollar spend to (set at partner level). |
| transaction_id | Your unique ID for the qualifying transaction. Used for idempotency. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| wallet_address | string | Required | The user’s Base wallet address (0x...) |
| amount_usd | number | Required | Transaction amount in dollars |
| transaction_id | string | Required | Your unique transaction ID for idempotency |
| merchant_id | string | Optional | Merchant identifier for category based rewards |
| metadata | object | Optional | Arbitrary key value metadata (10 keys at most) |
{
"wallet_address": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef12",
"amount_usd": 42.50,
"transaction_id": "txn_abc123",
"merchant_id": "mch_coffeeshop_01",
"metadata": {
"category": "dining",
"channel": "app"
}
}{
"issuance_id": "iss_7f8a9b0c",
"wallet_address": "0x1a2b3c...ef12",
"plus_amount": 8.50,
"amount_usd": 42.50,
"reward_rate": 0.20,
"status": "pending",
"tx_hash": "0xabc123...def",
"created_at": "2026-03-01T12:00:00Z"
}{
"error": true,
"code": "INVALID_WALLET",
"message": "The provided wallet address is not a valid Base address."
}{
"error": true,
"code": "DUPLICATE_TRANSACTION",
"message": "A reward has already been issued for transaction_id txn_abc123.",
"existing_issuance_id": "iss_7f8a9b0c"
}{
"issuances": [
{
"wallet_address": "0x1a2b...ef12",
"amount_usd": 42.50,
"transaction_id": "txn_001"
},
{
"wallet_address": "0x9f8e...cd34",
"amount_usd": 18.00,
"transaction_id": "txn_002"
}
]
}Status values
| Status | Description |
|---|---|
| pending | Transaction submitted to Base, awaiting confirmation. |
| confirmed | delivered to the user’s wallet. |
| failed | Issuance failed. failure_reason carries the detail. |
How redemption works
Users redeem for gift cards from a catalogue of gift card brands worldwide and for partner perks. Redemptions settle on chain and the gift card arrives by email.
Query parameters
| Param | Type | Required | Description |
|---|---|---|---|
| category | string | Optional | Filter by category (gift_card, cashback, partner_perk) |
| brand | string | Optional | Filter by brand name (partial match) |
| min_plus | number | Optional | Minimum cost to include |
| page | integer | Optional | Page number (default 1) |
{
"items": [
{
"brand": "Amazon",
"category": "gift_card",
"plus_cost": 50.00,
"usd_value": 50.00,
"kyc_required": false,
"in_stock": true
}
],
"total": 1,
"page": 1,
"per_page": 25
}Request body
| Field | Type | Required | Description |
|---|---|---|---|
| wallet_address | string | Required | The user’s Base wallet address |
| catalogue_item_id | string | Required | ID of the catalogue item to redeem |
| quantity | integer | Optional | Number of items (default 1) |
Staking overview
This section describes the Q1 design: locked in tiers. The live protocol replaced it with the USDC pool. Stakers fund the shop and share 60 percent of the net margin on every gift card sold, paid from real sales, variable, never promised.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| wallet_address | string | Required | The user’s Base wallet address |
| plus_amount | number | Required | Amount of to stake |
| tier | string | Required | Staking tier: flex, 3m, 6m, 12m |
{
"wallet_address": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef12",
"plus_amount": 1000.00,
"tier": "6m"
}{
"stake_id": "stk_4a5b6c7d",
"wallet_address": "0x1a2b3c...ef12",
"plus_amount": 1000.00,
"tier": "6m",
"status": "active",
"matures_at": "2026-09-01T12:00:00Z",
"created_at": "2026-03-01T12:00:00Z"
}User and wallet
Register users, query wallet balances and read transaction histories. Each user is identified by their Base wallet address.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| wallet_address | string | Required | The user’s Base wallet address |
| string | Optional | User email for notifications | |
| external_id | string | Optional | Your internal user ID for mapping |
{
"wallet_address": "0x1a2b3c...ef12",
"total_plus": 2450.75,
"available_plus": 1450.75,
"staked_plus": 1000.00,
"pending_rewards": 12.30,
"lifetime_earned": 5200.00,
"lifetime_redeemed": 2749.25
}Webhooks
Receive real time notifications when events occur in the plusmore system. Register an HTTPS endpoint and the platform POSTs event payloads as they happen.
Events reference
{
"url": "https://example.com/webhooks/plusmore",
"events": [
"reward.issued",
"redemption.completed",
"staking.matured",
"pool.low_balance"
],
"secret": "whsec_your_signing_secret"
}Data objects
Common structures returned across several endpoints. Every timestamp is ISO 8601 UTC. Every monetary amount is a decimal number with at most 2 decimal places, in dollars unless the field says .
Error codes
Rate limits
| Endpoint | Limit |
|---|---|
| /v2/rewards/issue | 1,000 a minute |
| /v2/rewards/issue/batch | 50 a minute |
| /v2/redemption/* | 500 a minute |
| /v2/staking/* | 300 a minute |
| /v2/users/* | 500 a minute |
| All other | 1,000 a minute |
Changelog
| Version | Date | Changes |
|---|---|---|
| Reference | 25 Sep 2026 | Republished on the plusmore site as the design reference. No endpoint is live; the staking section carries the live split instead of the Q1 tiers. |
| v2.1.0 | 13 Mar 2026 | Batch issuance endpoint, webhook test delivery and expanded error codes. |
| v2.0.0 | Q1 2026 | Staking tiers, the redemption catalogue API and the Base mainnet design. |