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" }

The two forms are mutually exclusive. Prefer the spread. Twenty wallets funded with the identical figure, in the same window, is itself a signature. The spread draws a different amount for each wallet, all inside your bounds, summing exactly to your total, and the draw is deterministic, so a settlement that runs twice produces the same plan rather than a different one.

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 actually arrived, measured against the balance before the plan started
short_byset only when delivery came up short. The funds stopped on the way, 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.

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",  "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_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}

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