DEVELOPERS · QUICKSTART

Ship your first Drive in an afternoon.

A working Merkava Drive is fewer than 200 lines of Node.js. Seven endpoints, one HMAC signer, one manifest. By the end of this tutorial you'll have a runnable "Echo" Drive on your laptop, a validated manifest, and a developer application in flight. The full spec is the reference; this is the do-it path.

Start building →

What you'll need

Reading order: If you've never seen the spec, skim Platform Contract v1 first (10 minutes), then come back. This tutorial assumes you know the words "manifest," "scoped token," and "HMAC."

What you'll build

The Echo Drive. Every install records a row; the metrics endpoint reports the count. Pointless on its own, but it touches every required surface — health, metrics, events, billing, install, uninstall, manifest. Once Echo runs end-to-end, swap the inner implementation for whatever your Drive actually does. The Contract scaffolding doesn't change.

Final structure:

echo-drive/
  package.json
  .env.example
  src/
    server.js       // Express app + the 7 endpoints
    hmac.js         // signRequest + requireHmac middleware
    manifest.js     // the manifest JSON your Drive publishes
    store.js        // in-memory install store (replace w/ DB later)

1Initialize the project

mkdir echo-drive && cd echo-drive
npm init -y
npm install express
echo "MERIDIAN_AGENT_SECRET=invent-a-long-random-string-here-32-chars-min" > .env
echo ".env" >> .gitignore

One dependency. The Platform Contract is plain HTTPS — no SDK to install, no codegen, no client libraries. Express is for ergonomics; you can do this with the bare http module.

2Verify HMAC on inbound requests

Merkava Core signs every authenticated request with the shared MERIDIAN_AGENT_SECRET. Two headers — timestamp (Unix ms, not seconds) and a raw-hex SHA-256 HMAC. Signed payload is exactly ${timestamp}:${path}. Path includes the query string. Method and body are NOT in the signature.

Create src/hmac.js:

'use strict';
const crypto = require('node:crypto');

const SKEW_TOLERANCE_MS = 5 * 60 * 1000;

function signRequest(path, secret, timestamp = Date.now()) {
  const payload = `${timestamp}:${path}`;
  const sig = crypto.createHmac('sha256', secret).update(payload).digest('hex');
  return { 'X-Meridian-Timestamp': String(timestamp), 'X-Meridian-Signature': sig };
}

function verifyRequest(path, headers, secret, now = Date.now()) {
  const ts = Number(headers['x-meridian-timestamp']);
  const sig = headers['x-meridian-signature'];
  if (!ts || !sig) return { ok: false, error: 'missing-headers' };
  if (Math.abs(now - ts) > SKEW_TOLERANCE_MS) return { ok: false, error: 'timestamp-skew' };
  const expected = crypto.createHmac('sha256', secret).update(`${ts}:${path}`).digest('hex');
  if (expected.length !== sig.length) return { ok: false, error: 'sig-length' };
  if (!crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(sig, 'hex'))) {
    return { ok: false, error: 'sig-mismatch' };
  }
  return { ok: true };
}

function requireHmac(secret) {
  return (req, res, next) => {
    const path = req.originalUrl;
    const result = verifyRequest(path, req.headers, secret);
    if (!result.ok) return res.status(401).json({ ok: false, error: result.error });
    next();
  };
}

module.exports = { signRequest, verifyRequest, requireHmac };
Why constant-time compare? A naive === on the hex strings leaks signature bytes through timing differences. crypto.timingSafeEqual compares in constant time. The length check before it guards against the exception timingSafeEqual throws when buffers differ in length.

3Implement the seven endpoints

Two are public (/health, /drive/manifest). Five are HMAC-gated. Every response is wrapped in {ok, data, meta: {version, generated_at}}. Failures return {ok: false, error} with an appropriate HTTP status.

Create src/store.js first — the world's smallest install database:

'use strict';
const installs = new Map(); // install_id → { tenant_id, venture_id, created_at }
let events = [];

module.exports = {
  installs,
  recordInstall(tenantId, ventureId) {
    const id = 'ins_' + Math.random().toString(36).slice(2, 12);
    const row = { id, tenant_id: tenantId, venture_id: ventureId, created_at: Date.now() };
    installs.set(id, row);
    events.push({ type: 'install.created', payload: { install_id: id }, ts: Date.now() });
    return row;
  },
  revokeInstall(id) {
    const row = installs.get(id);
    if (!row) return null;
    installs.delete(id);
    events.push({ type: 'install.revoked', payload: { install_id: id }, ts: Date.now() });
    return row;
  },
  listEvents(since = 0) {
    return events.filter((e) => e.ts > since);
  },
};

Then src/server.js:

'use strict';
require('dotenv').config?.(); // or load .env however you prefer
const express = require('express');
const { requireHmac } = require('./hmac');
const store = require('./store');
const manifest = require('./manifest');

const app = express();
app.use(express.json());

const SECRET = process.env.MERIDIAN_AGENT_SECRET;
if (!SECRET) { console.error('MERIDIAN_AGENT_SECRET not set'); process.exit(1); }

const VERSION = '1.0.0';
const wrap = (data) => ({ ok: true, data, meta: { version: VERSION, generated_at: new Date().toISOString() } });

// 1. Health — public liveness probe
app.get('/api/meridian/health', (req, res) => res.json(wrap({ status: 'ok', uptime_s: Math.floor(process.uptime()) })));

// 2. Manifest — public; what the Garage reads
app.get('/api/meridian/drive/manifest', (req, res) => res.json(wrap(manifest)));

// 3. Metrics — HMAC; aggregated counters
app.get('/api/meridian/metrics', requireHmac(SECRET), (req, res) => {
  res.json(wrap({ installs_active: store.installs.size }));
});

// 4. Events — HMAC; append-only log, ?since= filterable
app.get('/api/meridian/events', requireHmac(SECRET), (req, res) => {
  const since = Number(req.query.since) || 0;
  res.json(wrap({ events: store.listEvents(since) }));
});

// 5. Billing — HMAC; per-tenant billing summary (Echo is free)
app.get('/api/meridian/billing', requireHmac(SECRET), (req, res) => {
  res.json(wrap({ tenant_id: req.query.tenant_id || null, plan: 'free', amount_cents: 0, currency: 'usd' }));
});

// 6. Install — HMAC; provision a new install, return scoped_token
app.post('/api/meridian/install', requireHmac(SECRET), (req, res) => {
  const { tenant_id, venture_id } = req.body || {};
  if (!tenant_id) return res.status(400).json({ ok: false, error: 'missing tenant_id' });
  const row = store.recordInstall(tenant_id, venture_id || null);
  // scoped_token authorizes future tenant-scoped reads from this Drive
  res.json(wrap({ install_id: row.id, scoped_token: 'st_' + row.id, capabilities: ['read'] }));
});

// 7. Uninstall — HMAC; revoke an install
app.delete('/api/meridian/install/:id', requireHmac(SECRET), (req, res) => {
  const row = store.revokeInstall(req.params.id);
  if (!row) return res.status(404).json({ ok: false, error: 'not-found' });
  res.json(wrap({ install_id: row.id, revoked: true }));
});

const PORT = Number(process.env.PORT) || 8787;
app.listen(PORT, () => console.log(`Echo Drive listening on :${PORT}`));

That's the entire Drive. Every endpoint has the same shape — wrap your data, attach meta, return ok:false with a plain-English error message on failure.

4Write your manifest

The manifest is the Drive's self-description. The Garage reads it to render your card; Merkava Core reads it on install to know what permissions you need. Create src/manifest.js:

'use strict';

module.exports = {
  schema_version: 1,
  drive: {
    id: 'echo',                     // stable slug; lower-case, hyphenated
    name: 'Echo',
    tagline: 'Counts installs. Demonstrates the Platform Contract.',
    category: 'Utilities',
    motion: 'build',                // build | scale | intel
    homepage: 'https://echo-drive.example.com',
    support_url: 'https://echo-drive.example.com/support',
    icon_url: 'https://echo-drive.example.com/icon.png',
    pricing_tier: 'free',           // free | starter | standard | pro | premium | flagship
  },
  endpoints: {
    health: '/api/meridian/health',
    metrics: '/api/meridian/metrics',
    events: '/api/meridian/events',
    billing: '/api/meridian/billing',
    install: '/api/meridian/install',
    uninstall: '/api/meridian/install/:id',
    manifest: '/api/meridian/drive/manifest',
  },
  capabilities: ['install', 'uninstall', 'metrics', 'events', 'billing'],
  events_emitted: ['install.created', 'install.revoked'],
  events_consumed: [],              // add Merkava events your Drive listens for
  contract_version: '1.0.0',
};
The id is permanent. Once your Drive is approved and live, the slug becomes part of every install record, every payout reference, every operator's settings page. Pick one you can live with.

5Run locally + expose with HTTPS

node src/server.js
# Echo Drive listening on :8787

# In a second terminal — expose to the public internet
ngrok http 8787
# Forwarding   https://abcd-1234.ngrok-free.app -> localhost:8787

Sanity-check the public endpoints from anywhere — they don't need HMAC:

curl https://abcd-1234.ngrok-free.app/api/meridian/health
# {"ok":true,"data":{"status":"ok","uptime_s":42},"meta":{"version":"1.0.0",...}}

curl https://abcd-1234.ngrok-free.app/api/meridian/drive/manifest
# Returns your manifest JSON

For HMAC-gated endpoints, sign the request yourself with the same secret to verify your verifier:

node -e '
  const { signRequest } = require("./src/hmac");
  const path = "/api/meridian/metrics";
  const url = "https://abcd-1234.ngrok-free.app" + path;
  const headers = signRequest(path, process.env.MERIDIAN_AGENT_SECRET);
  fetch(url, { headers }).then((r) => r.json()).then(console.log);
'

6Validate the manifest

Before submitting, run your live manifest URL through the Merkava manifest validator. It checks the schema, the seven endpoints, and your HMAC handshake — the same gate the application form runs.

curl 'https://app.withmerkava.com/api/public/manifest-validate?url=https://abcd-1234.ngrok-free.app/api/meridian/drive/manifest'
# {"ok":true,"checks":{"schema":"pass","endpoints":"pass","hmac":"pass"}, ...}

If any check fails, the response tells you what — fix it, re-run. The validator is unauthenticated and CORS-open, so you can wire it into your CI / pre-submit script.

7Apply

Submit your manifest URL through the developer application form. We approve developers individually — keeping the bar high keeps the Garage trustworthy for operators.

After approval you receive your scoped MERIDIAN_AGENT_SECRET, a developer dashboard at app.withmerkava.com/developer, and a slot in the admin listing review queue. After your listing is approved, your Drive shows up in the Garage and operators can install. Stripe Connect onboarding happens once — payouts auto-flow at 70% of every operator's invoice.

Submit your application →

Common pitfalls

Timestamp in seconds, not milliseconds
The Contract uses Unix milliseconds. Math.floor(Date.now() / 1000) works locally because both sides are wrong; production fails because Merkava signs in ms.
Including sha256= in the signature
Common from copying GitHub-webhook code. The Platform Contract sends raw hex — no prefix. X-Meridian-Signature: 5f3c....
Signing the body or method
Only ${timestamp}:${path} is signed. Path includes query string. Method, body, and other headers are not in the signature.
Sending HMAC on public endpoints
/health and /drive/manifest are public. Crawlers hit your manifest. If you require HMAC there, the Garage can't render your card.
Forgetting the response wrapper
Every response is {ok, data, meta} on success or {ok:false, error} on failure. Returning bare data is treated as a malformed response.
Storing the secret on the client
MERIDIAN_AGENT_SECRET never leaves the server. If you find yourself prefixing it with NEXT_PUBLIC_ or VITE_, stop and rethink.
Hot-reloading the secret
If you rotate the secret, every Drive must reconnect. There's no key-rolling phase. Plan rotations as coordinated events.
QUESTIONS

Quickstart — questions, plainly answered.

How long does the quickstart actually take?

An afternoon for a developer comfortable with Node.js. The seven endpoints are mostly boilerplate — manifest, install, uninstall, event-receive, health, manifest-fetch, webhook. The actual work is the HMAC signing/verifying scaffolding, which the cookbook at /resources/drive-hmac handles in five languages. Production-readiness (config UI, error handling, observability) is a separate effort beyond the quickstart.

What do I need before starting?

Three things: Node.js 18+, an HTTPS-reachable URL (use ngrok or a Railway/Render free tier for development), and a Merkava developer account. Apply at /resources/developers if you don't have one. The tutorial is Node-specific but the underlying concepts apply identically to Python, Go, Ruby, or PHP.

Can I run the quickstart entirely locally?

Yes for development. You'll need an HTTPS tunnel (ngrok is the standard) so Merkava Core can reach your local Drive at install time. The signing handshake works fine over ngrok; once your Drive is reachable, Core treats it the same as any cloud-hosted Drive. Switch to a real host when you're ready to publish.

Does the quickstart Drive get listed in the public Garage?

No — quickstart Drives stay private to your developer account by default. To list publicly: complete the quickstart, refine against the production checklist (scope minimization, error handling, observability), then submit through the developer portal. Listing review checks manifest validity, scope minimization, security posture, and claimed-vs-actual capabilities.

What's the minimum a quickstart Drive needs to do?

Respond to the seven required endpoints, validate inbound HMAC signatures, sign outbound events, and publish a valid manifest. The tutorial walks through an "Echo" Drive that does all seven correctly. From there you replace the Echo handler with your actual business logic — the boilerplate stays the same.

What if I get stuck during the quickstart?

Run the published HMAC test vectors at /resources/drive-hmac-test-vectors against your sign function — that catches 80% of stuck-at-handshake bugs. Email [email protected] with the manifest, your Drive URL, and the error message; the dev-relations response is same-day during the early-customer phase.

Can I follow the quickstart in a language other than Node.js?

The narrative steps work in any language. Replace the Node.js code blocks with the equivalent from the multi-language cookbook — Python, Go, Ruby, and PHP are byte-for-byte equivalent. The signing and event handling logic is identical across stacks; only the syntax differs.

What's next

Ready to apply?

If Echo runs and your manifest validates, you're ready. Submit the form; we'll get back within 3 business days.

Apply for developer access →