The plusmore RaaS API. Demo store open now. Sandbox open now. Production by approval.
SectionsOverview
plusmore RaaS API

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.

Status
Demo store open now. Sandbox open now. Production by approval. Sandbox keys are self serve: create one below. Production has no self serve signup: plusmore reviews every partner before production access, and your partner agreement is with plusmore. or read how to build your own gift card shop first with the open source prompt and starter.
Margin
The margin a shop keeps is the rate our gift card supplier gives us, passed through in full: plusmore keeps 0 percent of it. These are our gift card supplier’s approved rates, the same as the public list at plusmore.xyz/pool-n-earn/rates. Across the live catalogue, a third of listings are 5 percent or more, and rates vary by brand, some are zero.

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.

Sandbox, open now

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:

ValueLooks likeWhat it is for
API keypmr_test_ then a key id and a secretthe Authorization: Bearer header of every request
Signing secretpmrsig_test_...signing every request (never sent)
Webhook secretpmrwh_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 cardSlugAmountsRate
Demo Coffee (US, USD)demo-coffee10.00, 25.00, 50.005%
Demo Books (GB, GBP)demo-booksany amount from 10.00 to 100.003%
Demo Books, limited todaydemo-limited10.00 to 50.00 today3%

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:

SendYou get
Demo Books at 13.37 (face_minor 1337)503 restocking, retry after an hour
Demo Books at 66.60502 upstream_failed, nothing charged; retry with the same client_ref
Demo Books at 99.99409 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.00409 denomination_unavailable (limited today)
end_customer.country XX451 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:

The signed string
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)

Bash
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

Bash
plusmore_call GET /v1/ping

3. List the catalogue (query parameters sorted by name)

Bash
plusmore_call GET /v1/catalogue 'country=GB&limit=10'

4. Get a quote

Bash
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)

Bash
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

Bash
plusmore_call GET /v1/orders/order-0001
plusmore_call GET /v1/balance

7. See your events and check their signature

Bash
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:

Bash
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):

Bash
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:

Bash
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 carries x-plusmore-env: sandbox and x-request-id.
  • Keys. pmr_test_ keys work only on raas-sandbox.plusmore.xyz; a pmr_live_ key there answers 403 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_limited with Retry-After. Per key, for its whole 30 days: 200 test orders, 50 simulated deposits and 50 test events; a spent limit answers 429 limit_exceeded with 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 answers 503 capacity_unavailable until the next UTC day. When the sandbox holds all the keys it can store, key creation answers 503 "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 answers 400 invalid_request.
  • Quotes. A quote lives 60 seconds. An order that names an expired quote answers 409 price_changed with a fresh quote in details; order again with that quote_id.
  • Statements. GET /v1/statements?month=YYYY-MM answers the month's figures signed with Ed25519; check them against the key at GET /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.

HTTP
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
Ping
Call GET /v1/ping to check a key: it answers {"data":{"consumer_id":"..."}}. A missing or unknown key is 401 unauthorised; a suspended account is 403 consumer_suspended.

Money and numbers

WhatShape
A face value or a wholesale priceInteger minor units, two decimals (face_minor: 2500 is 25.00 in that currency)
A rateInteger basis points internally, shown to you as a percentage (net_rate_bps: 550 is the rate you keep on that brand)
Your floatUSD with six decimals, the USDC unit on Base (usd6: 12630000 is 12.63 USD)
Note
The rate on a brand is set by plusmore and can change; read it from the catalogue each time rather than caching it for long. Rates vary by brand and some are zero.
GET/v1/brands?currency=&country=&q=&page=&per_page=Production by approval
The gift card catalogue: every brand you can sell, its denominations and its live availability.
JSON 200 OK
{
  "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
}
Note
denominations is what you can sell right now; all_denominations is every value the brand carries. A brand not listed at all is excluded or has nothing orderable today.
GET/v1/brands/{slug}/availabilityProduction by approval
The same brand object for one slug, read fresh. A brand that cannot be sold answers 404.
GET/v1/fxProduction by approval
USD per one unit of every currency in the catalogue, computed by the same function an order debits against, so a price you show and the order never disagree.
JSON 200 OK
{
  "data": {"base":"USD",
    "rates": {"GBP":1.3252367947,"EUR":1.1403,"USD":1},
    "source": "plusmore"}
}
GET/v1/summaryProduction by approval
Live catalogue counts for your own shop copy. Show 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.
POST/v1/orders/checkProduction by approval
The pre payment check: call it right before you take your buyer’s money. It books nothing.
JSON request
{
  "brand_slug": "tesco", "currency": "GBP", "face_minor": 2500
}
JSON 200 OK
{
  "data": {"ok": true, "code": "OK",
    "wholesale_minor": 2363, "wholesale_usd6": 31700000,
    "ledger_balance_usd6": 100730000}
}
codeMeaning
OKContinue to payment
LEDGER_INSUFFICIENTYour float does not cover it; not retryable until you deposit more
SUPPLIER_RESTOCKINGYour float covers it, plusmore cannot fill it right now; retry after retry_after_sec
NOT_SOLDUnknown brand, or an amount it does not sell
CAP_EXCEEDEDOver your per order or rolling 24 hour cap
POST/v1/ordersProduction by approval
Create an order, once the check is OK and your buyer has paid.

Request body

FieldRequiredDescription
client_refRequiredUnique 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_minorRequiredThe gift card you are issuing, exactly as quoted by the check
wallet_refRequiredAny string of 8 or more characters identifying your buyer; plusmore applies its per wallet caps to it
expected_wholesale_minorOptionalThe price the check quoted; a mismatch is refused rather than silently charging a different amount
JSON request
{
  "client_ref": "tx-ab12...ef34",
  "brand_slug": "adidas", "currency": "GBP", "face_minor": 1000,
  "wallet_ref": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef12",
  "expected_wholesale_minor": 945
}
JSON 201 Created
{
  "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}
}
Idempotency
Reusing the same client_ref always answers with the same gift card (replay: true), never a second one. A failed order (issue_failed) may be retried with the same reference. Never mint a new reference for a retry of the same payment, or your buyer may receive two gift cards.
GET/v1/orders/{client_ref}Production by approval
The current state of an order: the voucher once issued, or a failure reason.
GET/v1/floatProduction by approval
Your balance, your deposits and debits, and whether you are running low.

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.

HTTPcodeWhen
401unauthorisedMissing or unknown key
402LEDGER_INSUFFICIENTYour float does not cover the order
404unknown_brand, brand_not_available, unknown_order
409denomination_unavailableThat amount is not sold by this brand right now
409price_changedexpected_wholesale_minor no longer matches the live quote
409order_in_progressThe same reference is already being processed; retry in a few seconds
429cap_exceededOver your per order or rolling 24 hour cap
502issue_failedplusmore could not issue the gift card; your debit is reversed, retry with the same reference
503SUPPLIER_RESTOCKINGCannot fill right now; retry after the time given
Rule
The reference you send with an order (client_ref) is the whole idempotency story: the same reference always resolves to the same gift card. Build it from something that cannot repeat by accident, such as the hash of the payment that funded it.
Build your own shop

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 toWhat the API gives it
Price a gift card before it takes the buyer’s moneyPOST /v1/orders/check books nothing and answers the wholesale price
Order without ever charging twiceclient_ref: the same reference always resolves to the same gift card
Know what to do when an order cannot go throughStable codes with a next step: LEDGER_INSUFFICIENT (top up the float), SUPPLIER_RESTOCKING (retry after the time given)
Watch its money and reconcileGET /v1/float: the balance, the deposits and debits, and whether it is running low
Read the catalogue and its pricesGET /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.

The promptDownload
Loading the prompt. If it does not load, use Download instead.
Terminal, in the unzipped starter
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.

  1. Name. Set NEXT_PUBLIC_SHOP_NAME to your shop’s name. It shows in the header and in every page title.
  2. Logo. Set NEXT_PUBLIC_SHOP_LOGO_URL to an https image, or put your file in public/ and set it to its path, such as /logo.svg. Leave it empty and the header shows the name.
  3. Colours. Change --accent (your brand colour) and --accent-text (the text on it) in app/globals.css, once in :root for light mode and once in the dark mode block.
  4. Domain. Point your own domain at the shop and set NEXT_PUBLIC_SHOP_DOMAIN to it, so the message buyers sign names your shop.
  5. 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 thisWhat it is
PLUSMORE_API_BASE
PLUSMORE_API_KEY
From your production access. Server only; with both set the shop runs in LIVE mode.
SHOP_WALLET
NEXT_PUBLIC_SHOP_WALLET
Your wallet on Base that receives buyer payments, the same address in both.
BASE_RPC_URLA Base JSON RPC endpoint, used to check each payment on chain.
ADMIN_PASSWORDThe 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.

Archive

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.

plusmore RaaS API, Q1 2026 design

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.

Status
Designed, not live. This page describes the partner API as it was designed in Q1 2026. None of these endpoints is served today: plusmore runs on chain with the Pool and Earn contracts on Base, the gift card shop and the cashback ledger. The reference stays published so partners can review the surface. Talk to us before building against it.

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

1
Partner
funds
2
User
spends
3
Issue
4
Redeem
or stake
5
Settle
on Base

Authentication

Bearer token authentication
Every request carries a valid API key in the Authorization header. Keys are scoped to your partner account.
HTTP
POST /v2/rewards/issue HTTP/1.1
Authorization: Bearer <your API key>
Content-Type: application/json
X-Partner-ID: partner_abc123
Careful
Keep your keys secret. Never expose API keys in client side code or public repositories. Sandbox keys start with pm_test_ and production keys start with pm_live_.

Base URL and versioning

Every endpoint is versioned; this reference describes v2. Base URLs are issued with partner keys, none is public today.

EnvironmentBase URLKey prefix
ProductionIssued with your partner keyspm_live_
SandboxIssued with your partner keyspm_test_

Sandbox environment

Developer sandbox, as designed
Test before you ship
The sandbox mirrors production but uses test tokens with no real value. Issue, redeem and stake freely while building your integration. Rate limits are relaxed to speed up development. Not available yet.
Note
Sandbox tokens have no real value. Every  issued in the sandbox is a test token on Base Sepolia. It cannot be redeemed or moved to mainnet.

Error handling

Every error returns one JSON envelope with error, code and message fields.

JSON
{
  "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.

Design
Fixed supply.  has a fixed supply of one million. Issuance moves  from your funded partner pool to the user; nothing is minted.

Key concepts

ConceptDescription
partner_poolYour funded reward pool. Issuance debits from this pool.
wallet_addressThe user’s Base wallet that receives .
reward_rateThe conversion rate from dollar spend to  (set at partner level).
transaction_idYour unique ID for the qualifying transaction. Used for idempotency.
POST/v2/rewards/issueNot live
Issue  to a user wallet for a qualifying transaction.

Request body

FieldTypeRequiredDescription
wallet_addressstringRequiredThe user’s Base wallet address (0x...)
amount_usdnumberRequiredTransaction amount in dollars
transaction_idstringRequiredYour unique transaction ID for idempotency
merchant_idstringOptionalMerchant identifier for category based rewards
metadataobjectOptionalArbitrary key value metadata (10 keys at most)
JSON request
{
  "wallet_address": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef12",
  "amount_usd": 42.50,
  "transaction_id": "txn_abc123",
  "merchant_id": "mch_coffeeshop_01",
  "metadata": {
    "category": "dining",
    "channel": "app"
  }
}
JSON 201 Created
{
  "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"
}
JSON 400 Bad request
{
  "error": true,
  "code": "INVALID_WALLET",
  "message": "The provided wallet address is not a valid Base address."
}
JSON 409 Conflict
{
  "error": true,
  "code": "DUPLICATE_TRANSACTION",
  "message": "A reward has already been issued for transaction_id txn_abc123.",
  "existing_issuance_id": "iss_7f8a9b0c"
}
POST/v2/rewards/issue/batchNot live
Issue  to several wallets in one request. At most 500 issuances per batch.
JSON request
{
  "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"
    }
  ]
}
GET/v2/rewards/issue/{issuance_id}Not live
Read the current status of an issuance.

Status values

StatusDescription
pendingTransaction submitted to Base, awaiting confirmation.
confirmed delivered to the user’s wallet.
failedIssuance 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.

Note
KYC may be required. Some redemption options need the user to have completed KYC. Check the kyc_required field in the catalogue response.
GET/v2/redemption/catalogueNot live
Browse the redemption options. Filter by category, brand or minimum  amount.

Query parameters

ParamTypeRequiredDescription
categorystringOptionalFilter by category (gift_card, cashback, partner_perk)
brandstringOptionalFilter by brand name (partial match)
min_plusnumberOptionalMinimum  cost to include
pageintegerOptionalPage number (default 1)
JSON 200 OK
{
  "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
}
POST/v2/redemption/redeemNot live
Redeem  for a catalogue item.

Request body

FieldTypeRequiredDescription
wallet_addressstringRequiredThe user’s Base wallet address
catalogue_item_idstringRequiredID of the catalogue item to redeem
quantityintegerOptionalNumber of items (default 1)
Live model
The margin split. Every gift card sold shares its real margin: 60 percent to the USDC stakers and 40 percent to the buyers’ cashback pot, with 0 percent kept by the company. The aggregate split is on chain; see the proof page.
GET/v2/redemption/{redemption_id}Not live
Check the status of a redemption. Returns the redemption object with its current status and fulfilment detail.

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.

60%
Of net margin
to USDC stakers
40%
Cashback pot
to gift card buyers
0%
Company
keeps nothing
30 days
Fee free exit
no lock in
Plainly
Real margin, paid as earned. What stakers receive is a share of the shop’s real margin. It rises and falls with sales and is never promised. Past figures are history, not a forecast. The exit rules live on the risk page.
POST/v2/staking/stakeNot live
Stake  at a chosen tier, as designed in Q1.

Request body

FieldTypeRequiredDescription
wallet_addressstringRequiredThe user’s Base wallet address
plus_amountnumberRequiredAmount of  to stake
tierstringRequiredStaking tier: flex, 3m, 6m, 12m
JSON request
{
  "wallet_address": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef12",
  "plus_amount": 1000.00,
  "tier": "6m"
}
JSON 201 Created
{
  "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"
}
GET/v2/staking/{stake_id}Not live
Read the current state of a staking position, including the accrued share and the maturity date.
DEL/v2/staking/{stake_id}Not live
Unstake  before the maturity date.
Careful
Early exit in this design. Unstaking before maturity forfeited the accrued share and charged 5 percent of the principal; flex tier stakes had no charge. The live pool’s exit rules are on the risk page.

User and wallet

Register users, query wallet balances and read transaction histories. Each user is identified by their Base wallet address.

POST/v2/users/registerNot live
Register a new user wallet with your partner platform.

Request body

FieldTypeRequiredDescription
wallet_addressstringRequiredThe user’s Base wallet address
emailstringOptionalUser email for notifications
external_idstringOptionalYour internal user ID for mapping
GET/v2/users/{wallet_address}/balanceNot live
Read the  balance and its breakdown for a user wallet.
JSON 200 OK
{
  "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
}
GET/v2/users/{wallet_address}/transactionsNot live
Read a paginated list of every  transaction for a user wallet: issuances, redemptions, staking events and transfers.

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.

Note
Verify signatures. Every webhook payload carries an x-plusmore-signature header with an HMAC SHA256 digest. Verify it before processing the event.

Events reference

reward.issued has been issued and confirmed on chain.
reward.issue_failedA  issuance attempt failed. The payload carries the failure reason.
redemption.completedA redemption has been fulfilled and the reward delivered to the user.
staking.activatedA new staking position has opened.
staking.maturedA staking position has reached maturity. Principal and share released.
staking.early_exitA user unstaked before maturity. The early exit fee applied.
pool.low_balanceYour partner reward pool balance is below the warning threshold.
user.kyc_approvedA user has completed KYC verification and is approved for full redemption.
POST/v2/webhooksNot live
Register a webhook endpoint to receive event notifications.
JSON request
{
  "url": "https://example.com/webhooks/plusmore",
  "events": [
    "reward.issued",
    "redemption.completed",
    "staking.matured",
    "pool.low_balance"
  ],
  "secret": "whsec_your_signing_secret"
}
POST/v2/webhooks/testNot live
Send a test event to your registered webhook endpoint, to verify the integration before going live.

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

400INVALID_WALLET: The provided wallet address is not a valid Base address.
400MISSING_FIELD: A required field is missing from the request body.
401UNAUTHORIZED: Invalid or missing API key. Check your Authorization header.
402INSUFFICIENT_POOL: Your partner reward pool does not hold enough for this issuance.
403KYC_REQUIRED: This action needs the user to have completed KYC verification.
404NOT_FOUND: The requested resource does not exist.
409DUPLICATE_TRANSACTION: A reward has already been issued for this transaction_id.
429RATE_LIMITED: Too many requests. Retry after the period in the Retry-After header.

Rate limits

EndpointLimit
/v2/rewards/issue1,000 a minute
/v2/rewards/issue/batch50 a minute
/v2/redemption/*500 a minute
/v2/staking/*300 a minute
/v2/users/*500 a minute
All other1,000 a minute
Note
Need higher limits? Partners can ask for custom limits. Talk to us on Telegram.

Changelog

VersionDateChanges
Reference25 Sep 2026Republished 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.013 Mar 2026Batch issuance endpoint, webhook test delivery and expanded error codes.
v2.0.0Q1 2026Staking tiers, the redemption catalogue API and the Base mainnet design.