# Quickstart: sign your first message

This guide takes you from zero to a working signature in six steps. You will [create a machine user](/api-reference#tag/users), give it signing permission, [generate a key](/api-reference#tag/keys), and call the [sign endpoint](/api-reference#tag/keys). All vault API calls are authenticated with Ed25519 signatures.

## What you need

* Access to the vault web UI with a user account that can manage users and keys.
* Node.js 18 or later (for the code examples).
* OpenSSL available on your PATH.

## Step 1: Generate an Ed25519 keypair

This keypair identifies your machine user. Keep the private key secret.

```bash
openssl genpkey -algorithm ed25519 -out private.pem
openssl pkey -in private.pem -pubout -out public.pem
cat public.pem
```

Copy the public key contents. You will paste them into the vault in the next step.

## Step 2: Create a machine user in the vault UI

1. Open the vault web UI and navigate to **Users & Groups**.
2. Click **Create New** and choose **Create Trading Machine**.
3. Enter a **Name** (for example, `my-trading-bot`). The name is the machine user's permanent, unique identifier.
4. Paste your `public.pem` contents into the **Ed25519 public key** field.
5. Click **Submit**. The vault shows you the API key UUID **once**. Copy it now; it is not shown again.

You now have:

* An API key UUID (for example, `019e7e5b-f841-77d3-86ce-b436f2b6052a`).
* The machine user name you chose.

## Step 3: Add the machine user to a group with signing permission

Signing is governed by two policies:

* The **Administrative Policy** controls who can call the sign endpoint at all.
* The **Signing Policy** attached to the key's security group controls whether the request is approved.

For this quickstart, you need the machine user to be in a user group that the Administrative Policy's `import_generate_keys` and signing rules permit.

1. In the vault UI, go to **Users & Groups** and create a group (**Create New**, then **Create Group**, for example `bots`).
2. Add your machine user to the group.
3. Go to **Admin Policy** and confirm the `bots` group (or a group it belongs to) has permission to sign. If not, update the policy to allow it.

:::note
The exact Administrative Policy rule shape depends on how your organization configured the vault. Ask your vault administrator if you are unsure which group to use.
:::

## Step 4: Create a key

[Create a key](/api-reference#tag/keys) by calling `POST /v1/keys`. This example creates an Ed25519 key scoped to a venue called `my-venue` in the `testnet` environment.

```bash
# Replace <vault-host> and the auth headers with real values.
# This call requires all four auth headers (see the signing code below).
```

Or use the vault UI: go to **Offchain Keys**, click **New API Key**, fill in the scheme (`ed25519`), venue, environment, and an ID you choose. Note the key ID for step 6.

:::tip
Key IDs are caller-supplied and permanent. Pick an ID that is meaningful in your system, for example `eth-mainnet-signer` or `binance-hmac-key`.
:::

## Step 5: Add the key to a security group with a Signing Policy

A key must be assigned to a security group before it can sign. The security group links the key to a Signing Policy.

1. In the vault UI, open the **Security Groups** tab on the **Offchain Keys** page and create one (for example, `trading-keys`).
2. Attach a Signing Policy that approves signing requests from the `bots` user group.
3. Add your new key to the security group.

## Step 6: Call the sign endpoint

The script below builds the Ed25519-signed request and calls the [sign endpoint](/api-reference#tag/keys). Replace the constants at the top.

```js
const crypto = require('crypto');
const fs = require('fs');

// -- Configuration -----------------------------------------------------------
const CV_BASE      = 'https://<your-vault-host>';
const API_KEY_UUID = '<your-api-key-uuid>';        // from Step 2
const KEY_ID       = '<your-key-id>';              // from Step 4
const PRIVATE_PEM  = './private.pem';              // from Step 1

const privateKey = crypto.createPrivateKey(fs.readFileSync(PRIVATE_PEM, 'utf8'));

// Base64url, no padding.
const b64url = (buf) =>
  buf.toString('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');

// Build the SHA-256 transcript challenge.
// Transcript layout (spec verbatim):
//   0x01 version byte
//   [4-byte LE length] HTTP method
//   [4-byte LE length] URI path (no host/scheme)
//   [4-byte LE length] URI query string (empty if absent)
//   [4-byte LE length] request body bytes (empty if absent)
//   [4-byte LE length] Authorization value (scheme prefix stripped)
//   8-byte LE uint64: x-request-timestamp (milliseconds)
//   8 raw bytes: x-request-nonce
function transcriptChallenge({ method, path, query, body, apiKeyUuid, timestampMs, nonce }) {
  const fields = [
    Buffer.from(method, 'utf8'),
    Buffer.from(path, 'utf8'),
    Buffer.from(query, 'utf8'),
    body,
    Buffer.from(apiKeyUuid, 'utf8'),
  ];
  const parts = [Buffer.from([0x01])];
  for (const f of fields) {
    const len = Buffer.alloc(4);
    len.writeUInt32LE(f.length, 0);
    parts.push(len, f);
  }
  const ts = Buffer.alloc(8);
  ts.writeBigUInt64LE(BigInt(timestampMs), 0);
  parts.push(ts, nonce);
  return crypto.createHash('sha256').update(Buffer.concat(parts)).digest();
}

async function signMessage(message) {
  const path   = `/v1/keys/${KEY_ID}/sign`;
  const method = 'POST';
  const query  = '';
  // `message` is the HEX-encoded bytes to sign. A non-hex value returns 400 invalid hex.
  const messageHex = Buffer.from(message, 'utf8').toString('hex');
  const body   = Buffer.from(JSON.stringify({ message: messageHex }), 'utf8');

  const timestampMs = Date.now();
  const nonce       = crypto.randomBytes(8);

  const challenge  = transcriptChallenge({
    method, path, query, body,
    apiKeyUuid: API_KEY_UUID,
    timestampMs, nonce,
  });
  const assertion  = crypto.sign(null, challenge, privateKey);

  const tsBuf = Buffer.alloc(8);
  tsBuf.writeBigUInt64LE(BigInt(timestampMs), 0);

  const res = await fetch(`${CV_BASE}${path}`, {
    method,
    headers: {
      'authorization':       `ApiKey ${API_KEY_UUID}`,
      'content-type':        'application/json',
      'x-request-timestamp': tsBuf.toString('hex'),
      'x-request-nonce':     nonce.toString('hex'),
      'x-request-assertion': b64url(assertion),
    },
    body,
  });

  if (!res.ok) throw new Error(`Vault returned ${res.status}: ${await res.text()}`);
  const data = await res.json();
  return data.signature;
}

// Sign the UTF-8 bytes of "hello vault".
signMessage('hello vault')
  .then((sig) => console.log('Signature:', sig))
  .catch((e)  => { console.error(e.message); process.exit(1); });
```

A successful response looks like:

```json
{
  "signature": "<base64url or hex string depending on your venue's encoding>"
}
```

The signature encoding is determined by the venue configuration attached to the key. Check your venue settings if you need a specific output format.

## Next steps

* Read the [Machine Users and Agents](/develop/authentication/machine-users) page for a deeper explanation of the transcript and common pitfalls.
* See [Raw signing](/develop/signing/raw-sign) for all request fields including `key_version` and `extra_data`.
* See [Import and generate keys](/develop/keys-api) to manage keys programmatically over the API.
* Browse the full [Keys API reference](/api-reference#tag/keys) and [Users API reference](/api-reference#tag/users) for all available operations.
