Hushxima
Marketplace

Top up wallets

Getting native currency into wallets, bundled with an order or on its own. One deposit, one payment, one plan to follow.

Buying a wallet gets you its history. Topping it up gets it working capital. This part of the API does the second thing.

The unit of work is a funding plan: a list of destinations, an amount for each, and a single deposit address that pays for all of them. You send one transfer, and the plan delivers to every destination. What lands is the net amount you asked for, because delivery costs are priced into the deposit rather than deducted from the wallet.

Two things worth saying up front, because they shape everything below.

A plan does not expire. The deposit address stays valid until you cancel the plan. There is no clock to race.

Destinations do not have to be ours. A plan can target wallets you bought here, by wallet_id, and addresses you got anywhere else, by address, in the same plan. We never need the destination's key.

Two ways in

Add a top-up to POST /quote and it is priced into the same bill. The buyer sends one transfer covering the wallets and their funding, so by the time the order settles the funding is already paid for.

// the same amount on every wallet
{ "chain": "solana", "count": 20, "topup_per_wallet": "50000000" }

// a total, spread across the lot
{ "chain": "solana", "count": 20,
  "topup_total": "1000000000", "topup_min": "30000000", "topup_max": "80000000" }

// an amount per named wallet
{ "chain": "solana", "wallet_ids": ["0f8c…", "7d21…", "b40a…"],
  "topup_allocations": [
    { "wallet_id": "0f8c…", "amount": "300000000" },
    { "wallet_id": "7d21…", "amount": "120000000" },
    { "wallet_id": "b40a…", "amount": "0" }
  ] }

The three forms are mutually exclusive. Do not reach for the first one: twenty wallets funded with the identical figure, in the same window, is itself a signature, and it is the pattern the routing exists to avoid producing.

topup_total when you do not care which wallet gets what. The spread draws a different amount for each, all inside your bounds, summing exactly to your total. The draw is deterministic, so a settlement that runs twice produces the same plan rather than a different one. Shares stay close to an even split: no wallet walks away with most of the order.

A share is also either nothing or a workable amount, never dust. If the total cannot give every wallet a workable share, the spread funds fewer wallets rather than spreading crumbs over all of them, and a wallet left out stays out of the plan entirely, costing you no delivery. Set topup_min if you would rather decide where that line sits.

topup_allocations when you do. One entry per wallet, naming what it should receive. This is the form to use when the wallets have jobs: one trades, one holds, one only ever pays gas, and no draw of ours is going to guess that shape.

Four things to know about it:

  • It needs an explicit wallet_ids lot. On a server-selected lot the ids you name are not the ones that would be reserved, so the ask has nothing to attach to and the call is refused.
  • It must cover that lot exactly: one entry per wallet, no wallet missing, no id that is not in the order. A wallet left out is far more likely to be one you forgot than one you meant to fund with nothing, so the API will not guess.
  • "0" is how you say "this one I am buying without funding". It stays out of the funding plan entirely and costs you no delivery either, since a wallet that receives nothing has nothing to route. At least one entry must be non-zero.
  • Amounts are whole atomic native units, like everywhere else: lamports on Solana, wei on EVM. A decimal is rejected rather than scaled.

Whichever form you used, the quote echoes it back. topup_allocations on GET /quote/{id} is the answer to "what did this order ask for", long after the order was placed, and it is the split the plan opens on.

Watch total_due on the quote rather than user_price_total. The top-up sits in topup_required and both are included in what you owe.

When the order settles, a plan opens against the wallets that were sold, and its id appears on the quote as topup_plan_id. Such a plan is marked prepaid: true, which is how a client tells "waiting for money" apart from "waiting for you to make a decision". Nothing more is owed on it.

What happens next depends on the size of the lot.

One wallet. There is exactly one destination and one amount, so there is no decision to make. The plan opens quoted and starts on its own.

Several. The plan opens draft and waits for you to say how the money is split, with POST /funding/plans/{plan_id}/configure. It arrives pre-filled with the shape you asked for, so accepting it as-is is a single call. The total may not exceed what was paid, since there is no second payment in this flow, and routing starts as soon as the call returns.

POST
/funding/plans/{plan_id}/configure

Authorization

bearerAuth
AuthorizationBearer <token>

JWT from /auth/login (users) or an org API key (mk_...) for integrations.

In: header

Path Parameters

plan_id*String

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

curl -X POST "https://example.com/funding/plans/string/configure" \  -H "Content-Type: application/json" \  -d '{    "wallets": [      {        "amount": "string"      }    ]  }'
{  "chain": "string",  "completed_at": "string",  "created_at": "string",  "deposit_address": "string",  "fee_amount": "string",  "fee_bps": 0,  "funding_delay": 0,  "funds_seen_at": "string",  "items": [    {      "address": "string",      "amount": "string",      "delivered_amount": "string",      "funded_at": "string",      "hops_done": 0,      "hops_total": 0,      "is_initial": true,      "progress_pct": 0.1,      "routed_amount": "string",      "short_by": "string",      "status": "string",      "wallet_id": "d9a8fbfc-848f-43a2-9168-911ed3a04a48"    }  ],  "net_total": "string",  "plan_id": "00713021-9aea-41da-9a88-87760c08fa72",  "prepaid": true,  "quote_id": "3c071a1d-db86-46a7-9dc8-72ba3fbca992",  "received_amount": "string",  "refund_address": "string",  "refund_amount": "string",  "refund_requested_at": "string",  "refund_tx": "string",  "refunded_at": "string",  "rescue_attempts": 0,  "rescue_completed": true,  "rescue_completed_at": "string",  "rescue_failed_probes": 0,  "rescue_requested": true,  "rescue_requested_at": "string",  "rescue_swept_count": 0,  "routing_total": "string",  "started_at": "string",  "status": "string",  "total_required": "string",  "wallet_count": 0}

On its own

POST /funding/plans builds a plan from nothing. Name the destinations and the amounts, and get back a priced plan with a deposit address.

{
  "chain": "solana",
  "wallets": [
    { "wallet_id": "0f8c…", "amount": "50000000" },
    { "address": "7Yq2…", "amount": "120000000" }
  ],
  "funding_delay": 30
}

quote_id is accepted here and is pure provenance. It records which order this funding belongs to, so GET /funding/plans?quote_id=… can answer "what was funded for that order". It is never the authorisation: the wallets still have to be yours, and they need not come from that order.

Send total_required to deposit_address, then call POST /funding/plans/{plan_id}/start. That call reads the deposit on-chain and either starts the delivery or tells you exactly how much is still missing.

{
  "started": false,
  "status": "awaiting_funds",
  "expected_amount": "170300000",
  "received_amount": "120000000",
  "missing_amount": "50300000",
  "deposit_address": "…",
  "message": "funds not received yet: 120000000 of 170300000 on …"
}

It is safe to call repeatedly. On a plan already running it reports the state instead of starting a second time.

POST
/funding/plans

Authorization

bearerAuth
AuthorizationBearer <token>

JWT from /auth/login (users) or an org API key (mk_...) for integrations.

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

curl -X POST "https://example.com/funding/plans" \  -H "Content-Type: application/json" \  -d '{    "chain": "string",    "wallets": [      {        "amount": "string"      }    ]  }'
{  "chain": "string",  "completed_at": "string",  "created_at": "string",  "deposit_address": "string",  "fee_amount": "string",  "fee_bps": 0,  "funding_delay": 0,  "funds_seen_at": "string",  "items": [    {      "address": "string",      "amount": "string",      "delivered_amount": "string",      "funded_at": "string",      "hops_done": 0,      "hops_total": 0,      "is_initial": true,      "progress_pct": 0.1,      "routed_amount": "string",      "short_by": "string",      "status": "string",      "wallet_id": "d9a8fbfc-848f-43a2-9168-911ed3a04a48"    }  ],  "net_total": "string",  "plan_id": "00713021-9aea-41da-9a88-87760c08fa72",  "prepaid": true,  "quote_id": "3c071a1d-db86-46a7-9dc8-72ba3fbca992",  "received_amount": "string",  "refund_address": "string",  "refund_amount": "string",  "refund_requested_at": "string",  "refund_tx": "string",  "refunded_at": "string",  "rescue_attempts": 0,  "rescue_completed": true,  "rescue_completed_at": "string",  "rescue_failed_probes": 0,  "rescue_requested": true,  "rescue_requested_at": "string",  "rescue_swept_count": 0,  "routing_total": "string",  "started_at": "string",  "status": "string",  "total_required": "string",  "wallet_count": 0}
POST
/funding/plans/{plan_id}/start

Authorization

bearerAuth
AuthorizationBearer <token>

JWT from /auth/login (users) or an org API key (mk_...) for integrations.

In: header

Path Parameters

plan_id*String

Response Body

application/json

curl -X POST "https://example.com/funding/plans/string/start"
{  "deposit_address": "string",  "expected_amount": "string",  "message": "string",  "missing_amount": "string",  "received_amount": "string",  "started": true,  "status": "string"}

What it costs

total_required is the whole bill, and the plan breaks it down for you.

FieldWhat it is
net_totalwhat you asked to land in the destinations
routing_totaldelivery cost, which we front on your behalf (zero on EVM)
fee_amountthe platform take, at fee_bps

The fee is charged on the routed total, net plus delivery, because delivery is real money moving on your behalf. The default rate is 10 bps (0.10%), with a small floor per chain. The rate is frozen onto the plan when it is priced, so a plan you are already holding a deposit address for is never repriced.

Half of the fee is credited back to your organisation's balance.

Pacing

funding_delay is the gap, in seconds, between one destination starting and the next. It is capped at 7200, two hours.

Leave it at 0 and the destinations still do not all go at once. They go in waves. Set it and they go one at a time, spaced exactly as you asked.

Following a plan

GET /funding/status/{plan_id} is the polling endpoint. It returns the plan, its items, and a progress_pct across the whole thing.

Per item:

FieldMeaning
statuspending, running, funded, failed
hops_done / hops_totalon-chain steps completed out of the total for this destination
progress_pctthat ratio, for this destination alone
delivered_amountwhat the route confirmed into the wallet: the transfers we sent it that landed, summed. Null until the route finishes or fails
short_byset when the route stopped before sending everything. The funds are stranded in it, and a rescue recovers them

On Solana a single destination is many on-chain steps, so progress_pct moves long before anything has landed. Do not read it as "almost funded". Read status and delivered_amount for that.

delivered_amount is deliberately not the wallet's balance. A wallet you bought is yours to trade with, and one that spends while its funding is on the way would otherwise read as short-delivered. What the route sent it is a number nothing else can move. It follows that an item which reached funded cannot be short: it got there by confirming every hop.

The plan's own history

The status response also carries events, an append-only log of what was done to the plan's money after delivery: withdraw_requested, withdraw_confirmed and withdraw_failed for refunds, rescue_requested, rescue_completed and rescue_sweep_failed for rescues, oldest first, each with its time, destination, amount and transaction where there is one. A failed attempt stays in the log once the next one is queued, so the history reads as what happened rather than as the current word.

The current word is there too. refund_status follows the refund sweep (pending, broadcasting, awaiting_confirm, confirmed, failed), refund_error says why when it failed, and rescue_failed_sweeps counts rescue sweeps that died before returning their funds. Non-zero means money may still be stranded: rescue again.

GET
/funding/status/{plan_id}

Authorization

bearerAuth
AuthorizationBearer <token>

JWT from /auth/login (users) or an org API key (mk_...) for integrations.

In: header

Path Parameters

plan_id*String

Response Body

application/json

curl -X GET "https://example.com/funding/status/string"
{  "buyer_ref": "string",  "chain": "string",  "completed_at": "string",  "created_at": "string",  "deposit_address": "string",  "events": [    {      "amount": "string",      "at": "string",      "kind": "string",      "message": "string",      "to_address": "string",      "tx_hash": "string"    }  ],  "failed": 0,  "fee_amount": "string",  "fee_bps": 0,  "funded": 0,  "funding_delay": 0,  "funds_seen_at": "string",  "items": [    {      "address": "string",      "amount": "string",      "delivered_amount": "string",      "funded_at": "string",      "hops_done": 0,      "hops_total": 0,      "is_initial": true,      "progress_pct": 0.1,      "routed_amount": "string",      "short_by": "string",      "status": "string",      "wallet_id": "d9a8fbfc-848f-43a2-9168-911ed3a04a48"    }  ],  "net_total": "string",  "pending": 0,  "plan_id": "00713021-9aea-41da-9a88-87760c08fa72",  "prepaid": true,  "progress_pct": 0.1,  "quote_id": "3c071a1d-db86-46a7-9dc8-72ba3fbca992",  "received_amount": "string",  "refund_address": "string",  "refund_amount": "string",  "refund_error": "string",  "refund_requested_at": "string",  "refund_status": "string",  "refund_tx": "string",  "refund_updated_at": "string",  "refunded_at": "string",  "rescue_attempts": 0,  "rescue_completed": true,  "rescue_completed_at": "string",  "rescue_failed_probes": 0,  "rescue_failed_sweeps": 0,  "rescue_requested": true,  "rescue_requested_at": "string",  "rescue_sweep_error": "string",  "rescue_swept_count": 0,  "routing_total": "string",  "started_at": "string",  "status": "string",  "total_required": "string",  "wallet_count": 0}

Plan statuses

StatusMeaning
draftpaid for with an order, waiting for you to choose the split
quotedpriced, deposit address issued, nothing received yet
awaiting_fundsstart was called and the deposit is short
fundedthe full amount arrived, delivery can begin
runningdelivery in flight
completedevery destination funded
partialterminal, some funded and some failed
failedterminal, nothing landed
cancelleddiscarded before delivery started
refundedthe deposit went back to you and the transaction confirmed

When something goes wrong

Three different remedies, and they are not interchangeable.

Cancel, before delivery starts. Drops the plan. If the deposit already holds funds you must pass a refund_address, because it is your money and we do not pick the destination. Once delivery is in flight, cancel is refused: money is already moving and stopping halfway would strand it.

Rescue, after a delivery failed partway. Queues a background sweep that goes looking for whatever a failed delivery left behind and pulls it back to the plan's deposit wallet. It returns as soon as the job is queued, because a single plan can mean thousands of on-chain reads. It is refused while the plan is still running, and it is Solana only, since on EVM there is nowhere for funds to get stranded.

Follow it on the plan itself, with rescue_requested, rescue_completed, rescue_attempts, rescue_swept_count and rescue_failed_probes. That last one is the important one. Non-zero means the sweep could not read everything it needed to, so money may still be out there and the scan will retry.

Refund, to get a deposit back. Sweeps whatever is left on the plan's deposit wallet to an address you name. Use it on a plan that ended failed or partial. It is refused on a completed plan, where the remaining balance is the fee and the reserve, both already accounted for. refund_requested_at is set when the sweep is queued, and refunded_at only once it confirms. The pair is how you tell "refund in flight" from "money back".

POST
/funding/plans/{plan_id}/cancel

Authorization

bearerAuth
AuthorizationBearer <token>

JWT from /auth/login (users) or an org API key (mk_...) for integrations.

In: header

Path Parameters

plan_id*String

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

curl -X POST "https://example.com/funding/plans/string/cancel" \  -H "Content-Type: application/json" \  -d '{}'
{  "chain": "string",  "completed_at": "string",  "created_at": "string",  "deposit_address": "string",  "fee_amount": "string",  "fee_bps": 0,  "funding_delay": 0,  "funds_seen_at": "string",  "net_total": "string",  "plan_id": "00713021-9aea-41da-9a88-87760c08fa72",  "prepaid": true,  "quote_id": "3c071a1d-db86-46a7-9dc8-72ba3fbca992",  "received_amount": "string",  "refund_address": "string",  "refund_amount": "string",  "refund_requested_at": "string",  "refund_tx": "string",  "refunded_at": "string",  "rescue_attempts": 0,  "rescue_completed": true,  "rescue_completed_at": "string",  "rescue_failed_probes": 0,  "rescue_requested": true,  "rescue_requested_at": "string",  "rescue_swept_count": 0,  "routing_total": "string",  "started_at": "string",  "status": "string",  "total_required": "string",  "wallet_count": 0}
POST
/funding/plans/{plan_id}/rescue

Authorization

bearerAuth
AuthorizationBearer <token>

JWT from /auth/login (users) or an org API key (mk_...) for integrations.

In: header

Path Parameters

plan_id*String

Response Body

application/json

curl -X POST "https://example.com/funding/plans/string/rescue"
{  "message": "string",  "requested": true}
POST
/funding/plans/{plan_id}/refund

Authorization

bearerAuth
AuthorizationBearer <token>

JWT from /auth/login (users) or an org API key (mk_...) for integrations.

In: header

Path Parameters

plan_id*String

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

curl -X POST "https://example.com/funding/plans/string/refund" \  -H "Content-Type: application/json" \  -d '{    "refund_address": "string"  }'
{  "message": "string",  "refunding": "string"}

Listing and totals

GET /funding/plans is the plan list behind an operations screen. Beyond status, it takes a group, the coarse bucket a dashboard tab filters on (in_progress, completed, rescued, failed or refunded), and q, a case-insensitive substring search across plan id, quote id, deposit address, funded wallet address and buyer reference. quote_id is the exact match, for listing one order's funding without guessing at a substring.

GET /funding/stats gives the dashboard tiles: plans and wallets funded, volume, fees billed and the half credited to you, split by chain and filterable by date range. It also separates initial fundings, a wallet's first ever, from top-ups on wallets already funded.

GET
/funding/plans

Authorization

bearerAuth
AuthorizationBearer <token>

JWT from /auth/login (users) or an org API key (mk_...) for integrations.

In: header

Query Parameters

group?|

Coarse bucket, what the dashboard tabs filter on: in_progress (quoted/awaiting_funds/funded/running), completed, rescued (a finished rescue, whatever the plan's own status), failed (failed/partial/cancelled) or refunded. Combines with status.

limit?|
Formatint64
offset?|
Formatint64
q?|

Free text: plan id, quote id, deposit address, a funded wallet address, or a buyer ref. Substring match, case-insensitive.

quote_id?|

Only the plans raised on this order. Exact — the way to list one order's funding without guessing at a substring.

Formatuuid
status?|

Exact plan status: quoted, awaiting_funds, funded, running, completed, partial, failed, cancelled, refunded.

Response Body

application/json

curl -X GET "https://example.com/funding/plans"
{  "items": [    {      "buyer_ref": "string",      "chain": "string",      "completed_at": "string",      "created_at": "string",      "deposit_address": "string",      "failed_count": 0,      "fee_amount": "string",      "fee_bps": 0,      "funded_count": 0,      "funding_delay": 0,      "funds_seen_at": "string",      "initial_count": 0,      "net_total": "string",      "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",      "org_name": "string",      "plan_id": "00713021-9aea-41da-9a88-87760c08fa72",      "prepaid": true,      "quote_id": "3c071a1d-db86-46a7-9dc8-72ba3fbca992",      "received_amount": "string",      "refund_address": "string",      "refund_amount": "string",      "refund_requested_at": "string",      "refund_tx": "string",      "refunded_at": "string",      "rescue_attempts": 0,      "rescue_completed": true,      "rescue_completed_at": "string",      "rescue_failed_probes": 0,      "rescue_requested": true,      "rescue_requested_at": "string",      "rescue_swept_count": 0,      "routing_total": "string",      "started_at": "string",      "status": "string",      "topup_count": 0,      "total_required": "string",      "wallet_count": 0    }  ],  "total": 0}
GET
/funding/stats

Authorization

bearerAuth
AuthorizationBearer <token>

JWT from /auth/login (users) or an org API key (mk_...) for integrations.

In: header

Query Parameters

chain?string|null
since?|

RFC3339 lower bound (inclusive) on plan creation.

until?|

RFC3339 upper bound (exclusive).

Response Body

application/json

curl -X GET "https://example.com/funding/stats"
{  "by_chain": [    {      "chain": "string",      "completed_plans": 0,      "fee_org_total": "string",      "fee_pending_total": "string",      "fee_platform_total": "string",      "fee_swept_total": "string",      "fee_total": "string",      "initial_plans": 0,      "initial_wallets": 0,      "net_total": "string",      "plans": 0,      "topup_plans": 0,      "topup_wallets": 0,      "wallets_funded": 0    }  ],  "completed_plans": 0,  "fee_org_total": "string",  "fee_pending_total": "string",  "fee_platform_total": "string",  "fee_swept_total": "string",  "fee_total": "string",  "initial_plans": 0,  "initial_wallets": 0,  "net_total": "string",  "plans": 0,  "topup_plans": 0,  "topup_wallets": 0,  "wallets_funded": 0}

Last updated on

On this page