Skip to content

Wallets API ​

Custodial wallet management, scoped per chain family. One EVM wallet address works on ALL configured EVM chains — same address, same key, different chains. A separate address family exists for non-EVM chains (currently Stellar) via the same endpoints, selected with ?chain=.

▸API ConfigurationNot configured
Every endpoint, payload, and response on this page follows this choice.

How It Works ​

Comet Engine provides server-custodial embedded wallets: the engine generates and safeguards each user's key, and the client never handles raw key material.

  • User authenticates → wallet is created idempotently, keyed by externalUserId (your own user ID as a string — not an engine-assigned ID)
  • One secp256k1 keypair per (tenant, user, EVM) — same address on every EVM chain
  • A separate keypair per (tenant, user, Stellar) when the Stellar chain is configured — a distinct address from the EVM wallet, not an alias of it
  • Private key encrypted with AES-256-GCM, split via Shamir's Secret Sharing
  • Platform relayer pays gas — users never need native tokens

externalUserId, not userId

Wallet endpoints identify the user with externalUserId (a string — your own user ID from your own system), not a numeric userId assigned by the engine. Pass it in the request body for Create Wallet, or as ?externalUserId= / the X-External-User-ID header for Get Wallet and Wallet Balance.


Create Wallet ​

Creates (idempotently) a custodial wallet for a user on the given chain.

Create Wallet (Celo)
curl -X POST "http://localhost:8100/api/v1/wallets/create" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -d '{
  "externalUserId": "123"
}'
Response200
{
  "address": "0xe3f53fe54f8e1b5e8c86f070e4c9c2c7fa641d0d",
  "chain": "celo",
  "existing": true,
  "message": "wallet works on all EVM chains (Celo, Base, Arbitrum, etc.)"
}

Verified

The response above is a real, live call against a STELLAR_RELAYER_SEED-configured engine — that address exists on Stellar testnet, funded with the 3 XLM starting balance described below. Not a hypothetical example.

Request Fields:

FieldTypeRequiredDescription
externalUserIdstringYesYour own user ID, as a string
chainstringNoChain family to provision. Defaults to celo. Pass stellar (or ?chain=stellar) for a Stellar-family address instead.

TIP

This endpoint is idempotent — calling it twice for the same (tenant, user, chain family) returns the existing wallet.

Chain must be configured

chain: "stellar" returns 400 chain "stellar" not configured unless STELLAR_RELAYER_SEED is set on the engine. See Setting Up Stellar → below for the exact steps. EVM chains are configured per-chain the same way.

Setting Up Stellar ​

Stellar wallets need the relayer account funded on-chain before it can fund anything else — an unfunded relayer fails every wallet creation with 500 stellar: relayer account ... is not funded on ....

  1. Generate a relayer keypair. Any ed25519 Stellar keypair works — for local dev, a quick way is a throwaway Go script using the same SDK the engine does:
    go
    kp, _ := keypair.Random() // github.com/stellar/go-stellar-sdk/keypair
    fmt.Println(kp.Address()) // G... — public, safe to share
    fmt.Println(kp.Seed())    // S... — secret, treat like a private key
  2. Fund it. On testnet, Stellar's own Friendbot faucet does this for free:
    bash
    curl "https://friendbot.stellar.org/?addr=<YOUR_G_ADDRESS>"
    On mainnet there's no faucet — send it real XLM (the account needs at least the 1 XLM base reserve, plus enough to cover startingBalanceXLM — 3 XLM per new user wallet it funds, by default).
  3. Set the seed in the engine's environment:
    env
    STELLAR_RELAYER_SEED=S...
  4. Restart the engine. GET /health should now list "stellar" in chains.
  5. Verify with a real Create Wallet call (above) — confirm the returned address exists on-chain, e.g. via https://horizon-testnet.stellar.org/accounts/<address>.

Every subsequent user wallet is funded automatically from this same relayer, the same way EVM's relayer tops up gas for new wallets — no per-user manual funding step.


Get Wallet ​

Get Wallet (Celo)
curl -X GET "http://localhost:8100/api/v1/wallets?externalUserId=123" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET"
Response200
{
  "address": "0xe3f53fe54f8e1b5e8c86f070e4c9c2c7fa641d0d",
  "chain": "celo"
}

externalUserId may also be sent as the X-External-User-ID header instead of a query param.


Wallet Balance ​

Returns native balance and configured token balances for the given chain.

Wallet Balance (Celo)
curl -X GET "http://localhost:8100/api/v1/wallets/balance?externalUserId=123" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET"
Response200
{
  "address": "0xe3f53fe54f8e1b5e8c86f070e4c9c2c7fa641d0d",
  "nativeWei": "500000000000000000",
  "usdtBaseUnits": "10000000"
}

Key Management (Security) ​

  1. Generated as a keypair per (tenant, user, chain family) — secp256k1 for EVM, ed25519 for Stellar.
  2. Encrypted at rest with AES-256-GCM using a user-scoped key derived from the master WALLET_ENCRYPTION_KEY + tenant + user. Stored as encrypted_key in MongoDB.
  3. Shamir split — the sealed key is split into 5 Shamir shares (3-of-5 threshold). Server stores shares[0] as server_shard for recovery.
  4. Decrypted in memory only during transaction signing; zeroed immediately after.
  5. Signed server-side — the engine constructs and broadcasts transactions without the client ever holding the private key.

Custodial model

The server holds the encrypted private key and decrypts it for signing. This is an embedded-wallet trust model. For non-custodial signing, build a separate flow.

Cross-Chain Support ​

The same EVM wallet address works on all configured EVM chains:

ChainChain IDNative Token
Celo11142220 (Sepolia) / 42220 (Mainnet)CELO
Base8453ETH
Arbitrum42161ETH

To operate on a specific EVM chain, pass ?chain=celo (or base, arbitrum, ...) to asset endpoints.

Stellar is a separate address family, not another EVM chain — ?chain=stellar returns a different address than any ?chain=celo/base/arbitrum call for the same user, and that address is not usable on any EVM chain or vice versa.

Gas Relaying ​

All wallet operations have gas paid by the platform relayer:

  • ensureGas() checks the wallet's native balance
  • If below threshold, tops up from the relayer wallet
  • Transaction signed with user's key, sent from relayer's nonce

Stellar transactions are relayed the same way, funded from the relayer account configured via STELLAR_RELAYER_SEED.


Wallet Config (Tenant-Owned Wallets) ​

Every relayer-authority operation above — topping up a custodial wallet's gas, approving a spender by default, and every relayer-authority operation IMM/Treasury make (see IMM & Treasury) — normally signs with one shared global key per chain, the same relayer every tenant on that chain rides. Wallet Config lets a tenant dedicate their own wallet instead: one that covers every feature they use on a chain, or a different wallet per feature.

What this does and doesn't change

The engine still holds the key — generated by the engine, or handed to it by you and sealed immediately — the custody model is unchanged from the rest of this page. What changes is whose key signs a given operation: your own, instead of the platform-wide default. Nothing here is required; a tenant who configures nothing keeps using the global relayer exactly as before.

Feature scopes ​

A feature wallet is registered against a (chain, feature) pair. feature can be:

  • "*" — this wallet signs for every relayer-authority operation on this chain for your tenant (the "one master wallet" case).
  • A specific scope, e.g. "imm.vault" or "corridor" — this wallet is dedicated to just that one feature, letting you isolate signing authority per integration.

An exact feature match always wins over "*" if you've configured both.

FeatureGoverns
imm.vaultMinting into the IMM vault, its gas top-ups, and its AMM-rebalance gas
corridorMinting into an honestly-mintable corridor node during seeding or cycle execution
*Every relayer-authority operation above, on the given chain

Option 1: Generate a new wallet ​

The engine mints a brand-new keypair and dedicates it to you — the simplest path if you don't already have a wallet you want to reuse.

Generate (covers every feature on Celo)
curl -X POST "http://localhost:8100/api/v1/admin/wallet-config/relayer/generate" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -d '{
  "chain": "celo",
  "feature": "*"
}'
Response200
{
  "status": "success",
  "wallet": {
    "chain": "celo",
    "feature": "*",
    "address": "0xabc1230000000000000000000000000000000000",
    "scheme": "evm"
  }
}
Generate (dedicated to just the IMM vault)
curl -X POST "http://localhost:8100/api/v1/admin/wallet-config/relayer/generate" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -d '{
  "chain": "celo",
  "feature": "imm.vault",
  "scheme": "evm"
}'
Response200
{
  "status": "success",
  "wallet": {
    "chain": "celo",
    "feature": "imm.vault",
    "address": "0xdef4560000000000000000000000000000000000",
    "scheme": "evm"
  }
}

Pass "scheme": "stellar" for a Stellar keypair instead — note a freshly generated Stellar account still needs a real XLM funding transaction before it can transact (see Setting Up Stellar above); generating it here only provisions the keypair.

Option 2: Import a wallet you already control ​

Hand the engine a key you already hold — it's sealed immediately (the same AES-256-GCM sealing described in Key Management above) and never stored or returned in plaintext again.

Import an EVM Key
curl -X POST "http://localhost:8100/api/v1/admin/wallet-config/relayer/import" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -d '{
  "chain": "celo",
  "feature": "*",
  "scheme": "evm",
  "privateKeyHex": "0xyour-existing-private-key"
}'
Response200
{
  "status": "success",
  "wallet": {
    "chain": "celo",
    "feature": "*",
    "address": "0xyour-derived-address",
    "scheme": "evm"
  }
}
Import a Stellar Secret Seed
curl -X POST "http://localhost:8100/api/v1/admin/wallet-config/relayer/import" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -d '{
  "chain": "stellar-testnet",
  "feature": "imm.vault",
  "scheme": "stellar",
  "secretSeed": "SABC..."
}'
Response200
{
  "status": "success",
  "wallet": {
    "chain": "stellar-testnet",
    "feature": "imm.vault",
    "address": "GABC...",
    "scheme": "stellar"
  }
}

Never commit a real key to docs, code, or version control

Treat privateKeyHex/secretSeed like a password. Once imported it's sealed at rest and this call never returns it again — if you ever suspect it's been exposed, import a fresh key immediately; there's no way to rotate the old one's secrecy after the fact.

Checking what's configured ​

List My Feature Wallets
curl -X GET "http://localhost:8100/api/v1/admin/wallet-config/wallets" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET"
Response200
{
  "count": 2,
  "wallets": [
    {
      "chain": "celo",
      "feature": "*",
      "address": "0xabc..."
    },
    {
      "chain": "celo",
      "feature": "imm.vault",
      "address": "0xdef..."
    }
  ]
}

Combining with Approvals ​

Wallet Config decides whose key signs; Approvals decides whether an action runs at all. They're independent and compose naturally: register your own dedicated imm.vault wallet here, then require two of your team's own signatures before imm.vault.fund ever reaches it.

Production checklist ​

  • [ ] WALLET_ENCRYPTION_KEY is configured on the engine — wallet-config refuses to generate or import anything without it.
  • [ ] You've decided deliberately between one master wallet ("*") and per-feature wallets — the latter isolates blast radius if one integration's signing authority is ever compromised.
  • [ ] An imported key's plaintext has been removed from wherever you generated or copied it from — it exists nowhere in recoverable form after this call.

Released under the MIT License.