Earn (credit earned income)
Credit a user's earned, cash-out-eligible wallet balance. POST /io/wallet/earn — the single server-to-server entry point for creator revenue, referral/affiliate, received P2P transfers, and earned admin grants. 1 credit = $1.
POST /io/wallet/earn credits a user's earned, cash-out-eligible balance. It is the single entry point that raises withdrawable_balance (alongside the spendable balance), so it is the only way earned income becomes eligible for withdrawal.
POST https://api.misar.io/io/wallet/earn
Server-to-server only — earned income only
This endpoint mints cash-out-eligible credit, so it is locked tighter than /grant: it accepts only a service key (x-wallet-service-key), never an SSO bearer. Use it only for genuinely earned income that arrived on the user's behalf from a third party. Card top-ups, subscription-bundled perks, promo bonuses and migration adjustments must never flow through here — that would open a card-refund → withdraw arbitrage path. Those use /topup-session and /grant, which touch only the spendable balance.
Earned sources
The source field is stored on the ledger row (ledger type earning):
source | Meaning | Status |
|---|---|---|
creator_revenue | MisarBlog author ad / read / citation / audio monetization, settled from revenue_events | Live (primary mechanism) |
referral / affiliate | Referral / affiliate commissions | Hook point (dormant until a referral program exists) |
p2p_transfer | Credits received from another user | Hook point (dormant until a P2P send flow exists) |
admin_grant / prize_payout | Support comps / prize payouts explicitly marked earned | Live |
Authentication
Service key only. See Authentication.
x-wallet-service-key: <WALLET_SERVICE_KEY>
Request
{
"user_id": "0b3f…", // federated SSO subject id
"credits": 12.34, // > 0; raises BOTH spendable and withdrawable. 1 credit = $1
"source": "creator_revenue", // one of the sources above
"idempotency_key": "blog-rev-settle:8c1f…"
}Idempotency is mandatory and must be namespaced
The grant is idempotent on idempotency_key against the global ledger, so a retried settlement or webhook can never double-credit. Keys must be namespaced per source, e.g. blog-rev-settle:<settlement_id>, referral:<id>, p2p:<transfer_id>, admin-earn:<grant_id>. See Idempotency.
Response
{ "success": true, "balance": 112.34, "withdrawable": 12.34, "idempotent": false }| Field | Description |
|---|---|
success | true when the grant applied (or was an idempotent replay) |
balance | Spendable balance after the grant |
withdrawable | Cash-out-eligible balance after the grant |
idempotent | true when the key was already applied (no-op replay) |
Fail-closed
Any error (bad auth, bad input, RPC failure) returns { "success": false }. Callers must retry with the same idempotency_key rather than assume success — a transient outage can never silently double-grant.
SDK
import { createWalletClient } from "@misar/wallet";
const wallet = createWalletClient({
baseUrl: "https://api.misar.io/io/wallet",
serviceKey: process.env.WALLET_SERVICE_KEY,
});
const res = await wallet.creditEarnings({
userId,
credits: 12.34,
source: "creator_revenue",
idempotencyKey: `blog-rev-settle:${settlementId}`,
});
// res.success, res.withdrawable, res.idempotent