The Minmo API provides centralized access to Minmo services including agents, Lightning, and currency swap functionality.
# Start infrastructure and console service
npm run setup:dev
npm run dev:cs
# Access services
# - Console API: http://localhost:5000/api/v1/
# - API Documentation: http://localhost:5000/api/docs
POST /auth/register - Register new user accountPOST /auth/login - User login with JWT tokensGET /auth/user-info - Get current user and organizationsPOST /organizations - Create new organizationPOST /organizations/:id/api-keys - Create API keyGET /organizations/:id/api-keys - List API keysPOST /swap/quote - Get currency swap quotePOST /swap/onramp - Create KES → BTC swapPOST /swap/offramp - Create BTC → KES swapBreez Spark wallets convert between their bitcoin and stable (USDB) balances
through two endpoints. direction is to_stable (BTC → USD) or to_bitcoin
(USD → BTC), and amount is in source units: sats for to_stable, USDB base
units for to_bitcoin.
POST /teams/:teamId/wallets/:walletId/conversions/quote with
{ direction, amount } or { direction, convertAll: true } returns a
provider estimate (quotedAmount, estimatedSourceAmount,
estimatedDestinationAmount, feeSats, expiresAt).POST /teams/:teamId/wallets/:walletId/conversions executes it. Send
{ direction, convertAll: true } to convert the live source balance, or
{ direction, amount, quotedAmount, tokenIdentifier } from a reviewed quote.Both require the bitcoin:wallet_send permission. Executing also requires an
Idempotency-Key header: a retry with the same key and request replays a
completed conversion, while reusing it for a different request or after a
failed attempt returns 409. Partial conversions pay the
wallet's own Spark address through the provider's conversion path. The API
prepares a fresh estimate on submission and rejects it if the estimated spend
exceeds the requested source amount; amounts received may differ from the
estimate as rates move. For to_bitcoin, the provider may round a conversion
up to the whole balance rather than leave unconvertible token dust; the quote
reports this as convertsEntireBalance.
to_stable with convertAll uses the live BTC balance when the request
executes, and a full-balance amount must equal the live balance. Manual
stabilization preserves the wallet's automatic conversion policy and threshold.
to_bitcoin is refused with auto_conversion_enabled while the wallet's
stable balance policy is on, because the provider would convert the bitcoin
back to USD. Send disableAutoConversion: true to turn the policy off and
reconnect the wallet before converting. A pending manual conversion does not
signal a provider failure or pause the wallet.
If the provider reports a failed automatic conversion, the API records
stableBalance.autoConversionPaused and stops that wallet's active SDK
instance. Wallet reads remain available, and existing token holdings are not
converted back to BTC. A normal wallet edit preserves the pause. To resume,
review the provider outcome, then use the Console's Resume conversions
action or PATCH the wallet with enabled stable balance settings and
resumeAutoConversion: true. The API refuses to resume while a conversion is
pending or needs a refund, including an older unresolved conversion. Support
must reconcile the provider outcome before the wallet can resume. When automatic
conversion is off, normal wallet connections do not unset the active stable
label or start a conversion worker, so manually held stable tokens remain held.
OTC quote/update events persist the original rate and quote timestamp in their payload. Delivery and replay do not depend on FX cache keys, expiry, or provider availability. Older events without payloads recover their original quote from the timestamp and rate already stored in their deduplication key. Invalid legacy records remain pending for investigation; they are not replaced with current rates. Rate-expiry events retain a null payload.
Failed domain-event deliveries use an exponential retry delay starting at five
seconds and capped at five minutes, using locked_until as the next eligible
attempt time. Later eligible events can continue while failures wait. Inspect
events.last_error, attempt_count, and locked_until for persistent failures.
Each API process opens at most POSTGRES_POOL_SIZE TypeORM connections
(default 10) plus BREEZ_POSTGRES_POOL_SIZE Breez Spark storage connections
(default 2), shared across wallet SDKs. Swap automation runs at
most SWAP_AUTOMATION_CONCURRENCY workers across polling and event prompts.
All workers take their per-swap advisory locks on one shared pooled session,
so automation pins a single connection however many workers run. Each running
worker still needs a pooled connection for its queries, so the worker count
must stay below the pool size. Config validation enforces this, and the unset
default is 4 capped at one below the pool size.
Development and staging share the testing-managed-postgres Cloud SQL
instance. Terraform configures db-g1-small, whose documented default is
max_connections = 50, with 3 reserved for superusers. Verify the actual
limits after applying the tier change; these allocations assume one API
process per environment.
Their Terraform tfvars set TypeORM pools of 5 for dev and 10 for staging,
with Breez pools of 2 and 4 respectively. Both run 2 automation workers.
At 4 connections with 2
workers, background automation and pollers left API requests queued for
seconds in pg-pool.connect:
| Client | Connections |
|---|---|
| Dev API: TypeORM 5 + Breez 2 | 7 |
| Staging API: TypeORM 10 + Breez 4 | 14 |
| BTCPay (dev and staging) | 2 |
| NBXplorer | 1 |
| Planned total | 24 of 47 |
| Headroom for migrations and diagnostics | 23 |
BTCPay and NBXplorer counts above are planning estimates, not enforced pool caps: their Terraform connection strings do not specify maximum pool sizes. Actual use by these clients reduces the available headroom. Each additional dev API process consumes up to another 7 connections; each additional staging API process consumes up to another 14.
Production also configures db-g1-small, with 15 TypeORM connections plus
4 Breez connections per API process and 4 automation workers.
That leaves 28 of the expected 47 non-superuser slots before BTCPay,
NBXplorer, migrations, diagnostics, or additional API processes are counted.
Workers use the TypeORM pool and must not be added again to the connection
total. The shared advisory-lock session occupies one slot within that pool.
Cloud SQL documents the defaults in its database flags reference. Changing the tier can interrupt database availability; schedule the apply accordingly. Before expanding pools or worker concurrency, check the limits and aggregate client usage on each instance:
SHOW max_connections;
SHOW superuser_reserved_connections;
Connections are labelled
minmo-api-<env>-typeorm and minmo-api-<env>-breez in pg_stat_activity:
SELECT datname, application_name, state, count(*)
FROM pg_stat_activity
GROUP BY 1, 2, 3
ORDER BY count(*) DESC;
X-API-Key headerMinmo Nostr bearer credentials must be access tokens; refresh tokens are accepted
only by the refresh endpoint. Authorization reads the current account status and
roles, so suspended accounts and revoked roles lose access immediately.
Participant actions require any supplied beneficiaryId to match the authenticated
Nostr identity. The global /swap/disputed list requires a Minmo admin; team
operators use /teams/:teamId/swaps/disputed.
Team API-key permissions are limited to the shared integration permission catalog
(API_KEY_POLICY_PERMISSIONS). Global user management permissions cannot be
saved or used by team keys, including policies stored before this restriction.
Server code that resolves Lightning addresses must import the helpers from
@minmo/core/server/lightning-address. This transport requires HTTPS on port 443,
rejects redirects and non-public DNS results, and connects using the validated
addresses. Responses are limited to 1 MiB and requests to ten seconds. Browser
and mobile consumers continue using the browser-compatible @minmo/core helpers.
JWT requests can authorize entirely from the existing users.roles and
auth_identities.metadata.hexclaveAuthorization columns. No authorization
table or request-time Hexclave fallback is used. There is no feature flag: the
API blocks startup until every active Hexclave identity has either a usable
projection with more than five minutes of validity remaining or a locally
disabled projection, and until the Hexclave JWT signing keys are loaded. A
rolling deployment leaves the previous API revision serving traffic while the
first new instance performs this backfill. If safe coverage cannot be
established within ten minutes, the new instance fails startup rather than
serving against missing or stale authorization state.
Projections expire after ten minutes and fail closed when missing, malformed, stale, disabled, or expired. The repair worker runs every five minutes with a renewable database lease, bounded concurrency, and retry backoff. Rollback is a deployment rollback to the prior API revision; stale projection data is never accepted as a rollback mechanism.
External Hexclave changes are discovered by session refresh, explicit Minmo mutations, and the five-minute repair loop. There is no webhook dependency in this rollout; the ten-minute projection validity bounds authorization staleness and fails closed when repair cannot refresh it. Signing keys also refresh every five minutes outside the request path. An unknown JWT key fails closed until a background refresh discovers it; token verification never fetches JWKS synchronously.
Monitor authorization.projection.coverage and
authorization.projection.repair.completed; production readiness requires no
pending, stale, expired, malformed, or expiring active projections. Measure the
authorization budget from authorization.local.completed logs.
synchronousHexclaveCalls must remain zero. Measure the end-to-end Lightning
receive outcome separately from wallet.receive.completed; wallet loading,
Breez queue, SDK readiness, SDK execution, and response completion timings are
reported independently.
Run the direct-to-API authorization benchmark against a warm wallet read route:
AUTHORIZATION_BENCHMARK_URL=https://api.example.com/api/v1/teams/<team-id>/wallets \
AUTHORIZATION_BENCHMARK_TOKEN=<access-token> \
LIGHTNING_RECEIVE_BENCHMARK_URL=https://api.example.com/api/v1/teams/<team-id>/wallets/<wallet-id>/bitcoin/receive \
npm run benchmark:authorization --workspace=@minmo/api
The command performs 20 warmups and at least 100 measured requests, reports
authorization and end-to-end P50/P95/P99, requires the API's zero-Hexclave-call
confirmation header, and fails when authorization P95 is not below 100ms. Run
it with the Hexclave signing key already cached and block unexpected Hexclave
user/team/permission traffic at the test boundary. When the optional Lightning
receive URL is provided, it separately runs 5 warmups and at least 20 measured
invoice requests, reports success/failure counts and P50/P95/P99, and compares
the successful provider-inclusive P95 with the one-second target without making
that outcome the authorization release gate. Override the request JSON with
LIGHTNING_RECEIVE_BENCHMARK_BODY when the target wallet needs a larger amount.
For comprehensive documentation, deployment guides, API references, and development setup instructions, visit:
This includes:
Private - Minmo Internal Use Only
Content type
Image
Digest
sha256:0ebb7d0fe…
Size
519.5 MB
Last updated
about 6 hours ago
docker pull minmoto/api:production-0.5.15