CLI Signer API
HTTP endpoints the standalone server exposes for signing with the sui CLI keystore
HTTP API for signing transactions using the local sui CLI keystore. Private keys never leave the
sui binary — only transaction bytes are sent for signing.
This API is served by the dev wallet's standalone server
(pnpm dlx @mysten-incubation/dev-wallet serve). The endpoints are implemented as middleware that
can be mounted on any HTTP server.
Endpoints
GET /api/v1/accounts
List all accounts available in the Sui CLI keystore.
Request:
GET /api/v1/accounts HTTP/1.1
Authorization: Bearer <token>Response (200):
The accounts array is the raw, verbatim output of sui keytool list --json — the server runs that
command and returns its parsed JSON unchanged. The exact field shape is defined by the sui CLI,
not by the dev wallet, and may change between CLI versions. A representative entry looks like:
{
"accounts": [
{
"suiAddress": "0x1234567890abcdef...",
"publicBase64Key": "AO3a1234...",
"keyScheme": "ed25519",
"alias": "my-account"
}
]
}Because this is a passthrough, treat the field names and types as owned by
sui keytool list --json. Run that command locally to see the precise schema for your installed CLI
version.
POST /api/v1/sign-transaction
Sign BCS-serialized TransactionData using the sui CLI. Calls sui keytool sign under the hood.
Request:
POST /api/v1/sign-transaction HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/json
{
"address": "0x1234567890abcdef...",
"txBytes": "oEJ..."
}| Field | Type | Validation | Description |
|---|---|---|---|
address | string | Valid Sui address (isValidSuiAddress) | Sui address to sign with |
txBytes | string | Non-empty base64, max ~1M chars | BCS-serialized transaction data |
Response (200):
{
"suiSignature": "AKjTrX9...",
"digest": "7f9a2c1b..."
}| Field | Type | Description |
|---|---|---|
suiSignature | string | Sui signature (includes scheme flag) |
digest | string | Transaction digest |
Personal message signing is not supported — sui keytool sign only accepts TransactionData.
Error responses
All errors return JSON with an error field:
{ "error": "description of what went wrong" }The status codes are not uniform across endpoints — 404 and 413 only apply to
POST /api/v1/sign-transaction, and a failed account listing returns 500, not 404:
| Status | Endpoint | When |
|---|---|---|
400 | sign-transaction | Invalid JSON body, bad address format, or invalid txBytes |
401 | both | Missing or invalid bearer token (token middleware runs on /api/*) |
403 | both | Host header is not localhost/127.0.0.1/[::1] (DNS-rebinding guard) |
404 | sign-transaction | Address not found in the keystore |
413 | sign-transaction | Request body exceeds 2 MB (based on content-length) |
500 | accounts | sui CLI not found, or sui keytool list failed |
500 | sign-transaction | sui keytool sign failed for a reason other than a missing key |
Authentication
The API uses a token-in-URL approach (same pattern as Jupyter notebooks):
- On server start, a 256-bit random token is generated
- The token is printed to the terminal as part of the URL:
http://localhost:5174/?token=<token> - Opening that URL stores the token in
localStorage - All API requests require the token in the
Authorization: Bearer <token>header - Token comparison uses
timingSafeEqual()to prevent timing attacks
Client integration
The RemoteCliAdapter from @mysten-incubation/dev-wallet/adapters implements the browser-side
client for this API. The token is read from the URL printed at server start and persisted to
localStorage. See Remote CLI adapter for usage.
import { RemoteCliAdapter } from '@mysten-incubation/dev-wallet/adapters';
const adapter = new RemoteCliAdapter({
serverOrigin: 'http://localhost:5174',
token: '<token>',
});
await adapter.initialize();
// List accounts available on the server (not yet imported)
const available = await adapter.listAvailableAccounts();
// Import a specific account for use in the wallet
await adapter.importAccount({ address: '0x...' });