Create your own Lua engine

Embedding Lua in Node.js lets you run user-defined automation inside your process: fetch data, call an API, send mail, without a second runtime. wasmoon runs Lua 5.4 over WebAssembly. You bind JavaScript functions to Lua globals; scripts call those names like normal functions. Async JS returns Promises; from Lua you use :await() on the handle wasmoon gives you.

A typical pipeline (as in example-script.lua): pull Coinbase product (or candles), jsonStringify, OpenAI prompt, Gmail send. The bindings sketched below mirror that pattern; swap implementations for your own HTTP clients.

How it fits together

Piece Role
wasmoon LuaFactory + createEngine() builds a LuaEngine.
Your code engine.global.set('name', fn) exposes name to Lua.
Async fn may return a Promise. In Lua use :await() on the returned value.
Run await engine.doString(source) (or doFile) executes the script.

Optional: engine.global.loadLibrary(LuaLibraries.Math) (and other wasmoon core libs) if you need standard library pieces beyond what you bind.

Plain TypeScript (minimal host)

No framework - just create engine, register globals, run string. Wire fetchCoinbase, promptAi, mailSend to your real SDKs or fetch.

import { readFileSync } from "node:fs";
import { LuaFactory, LuaLibraries, type LuaEngine } from "wasmoon";

async function main() {
  const factory = new LuaFactory();
  const engine: LuaEngine = await factory.createEngine();

  for (const lib of [
    LuaLibraries.Base,
    LuaLibraries.String,
    LuaLibraries.Math,
  ]) {
    engine.global.loadLibrary(lib);
  }

  engine.global.set("jsonStringify", (v: unknown) => JSON.stringify(v));

  engine.global.set("coinbaseFetchProduct", (id: string) =>
    fetchCoinbaseProduct(id),
  );

  engine.global.set("promptOpenAi", (body: string, instructions?: string) =>
    promptOpenAi(body, instructions),
  );

  engine.global.set("sendGmail", (to: string, subject: string, text: string) =>
    sendGmail(to, subject, text),
  );

  const lua = readFileSync("./example-script.lua", "utf8");
  await engine.doString(lua);
}

// Implement these with your keys and HTTP clients; return Promises for async work.
async function fetchCoinbaseProduct(_id: string): Promise<unknown> {
  /* ... */
  return {};
}

async function promptOpenAi(
  _body: string,
  _instructions?: string,
): Promise<string> {
  /* ... */
  return "";
}

async function sendGmail(
  _to: string,
  _subject: string,
  _text: string,
): Promise<void> {
  /* ... */
}

main().catch(console.error);

JavaScript is the same without types: drop : LuaEngine, unknown, and Promise<...> annotations.

Same idea in fewer lines: one attachGlobals(engine) helper that registers only what that run needs.

Example script - product snapshot, GPT analysis, email

example-script.lua - uses coinbaseFetchProduct; switch to coinbaseFetchCandles if you bind that name the same way.

local coinbaseProduct = coinbaseFetchProduct("BTC-USDC"):await()
local coinbaseProductStringified = jsonStringify(coinbaseProduct)
print(coinbaseProductStringified)

local openAiInstructions = "Analyze the current product pair on Coinbase"
openAiInstructions = openAiInstructions .. ". Provide a summary of the market conditions"
openAiInstructions = openAiInstructions .. ". Recommend buy/hold/sell"

local openAiPromptResponse = promptOpenAi(coinbaseProductStringified, openAiInstructions):await()
print(openAiPromptResponse)

sendGmail("all", "BTC-USDC Market Analysis", openAiPromptResponse)
  • promptOpenAi(body, instructions?) - First string is the main payload; second is optional guidance.
  • sendGmail - "all" only works if your sendGmail implementation interprets it (e.g. expand to all saved recipients).

Globals (what to bind for the samples above)

Only register functions your scripts call. Names below match common samples; rename freely if your host uses shorter names.

Lua global Typical JS signature
jsonStringify (value) => string
jsonParse (json: string) => unknown (optional)
coinbaseFetchProduct (productId: string) => Promise<unknown>
coinbaseFetchCandles (productId, startIso, endIso, granularity) => Promise<unknown>
promptOpenAi (prompt: string, instructions?: string) => Promise<string>
sendGmail (to: string, subject: string, text: string) => Promise<void>

Candle example (after you bind coinbaseFetchCandles):

local candles = coinbaseFetchCandles(
  "BTC-USDC",
  "2026-01-01T00:00:00Z",
  "2026-01-02T00:00:00Z",
  "ONE_HOUR"
):await()
local reply = promptOpenAi(jsonStringify(candles), "Summarize volatility and trend."):await()
sendGmail("all", "BTC-USDC hourly", reply)

Granularity must match whatever your Coinbase client expects (e.g. "ONE_HOUR", "ONE_DAY").


Async in Lua - :await()

Returning a Promise from JS:

local p = coinbaseFetchProduct("ETH-USDC")
local data = p:await()

Without :await(), data is not the resolved value.


Adding another global

engine.global.set("httpGet", (url: string) => fetch(url).then((r) => r.text()));

Prefer keeping secrets and I/O in JS; Lua stays orchestration.

Outro

wasmoon + global.set + doString is enough to host user Lua in a plain Node process. The example-script.lua pipeline is fetch - stringify - prompt - notify; extend by binding more functions, not by growing framework-specific wrappers.

Previous Post Next Post