Lightweight DataGrout connectors for game engines.
Connect your game to DG's full intelligence layer in minutes. Free tier auto-provisions on first connect -- no API key, no dashboard, no configuration.
local Tether = require(game.ServerStorage.Tether)
local dg = Tether.connect()
-- Query game logic facts (free)
dg:query("fishing-game", "fish(X, rare, _)", function(results)
spawnRareFish(results)
end)
-- Let the LLM write your game rules (Community+)
dg:generate_rules("rare fish only appear at night during rain", "fishing-game", function(rules)
print("Rules generated: " .. rules)
end)
-- Call any DG tool directly -- 150+ available on signup
dg:call("data-grout@1/math.trend@1", { values = salesData }, function(result)
updateLeaderboard(result)
end)| Engine | Language | Client | Status |
|---|---|---|---|
| Roblox | Luau | tether-lua/Tether.lua |
shipped (Wally, Studio plugin, manual) |
| LÖVE (love2d) 11 / 12 | Lua | tether-lua/love/tether_love.lua |
shipped |
| Solar2D (Corona) | Lua | tether-lua/solar2d/tether_solar2d.lua |
shipped |
| Defold | Lua | tether-lua/defold/tether_defold.lua |
shipped |
| Godot 4.3+ | GDScript | tether-gdscript/addons/tether/ |
shipped (addon; see its README) |
| Unity | C# | tether-csharp/ |
planned |
| Unreal | C++ | tether-cpp/ |
planned |
All Lua clients share one implementation, tether-lua/TetherCore.lua (plain Lua 5.1+ / LuaJIT / Luau, zero dependencies). Each engine adapter is a short file that supplies HTTP, JSON, timers and identity storage. The API below is identical on every engine; the Godot addon exposes the same methods in GDScript (tether-gdscript/README.md).
Every client is written in its language's house style and checked in CI: StyLua + luacheck for Lua, gdformat + gdlint for GDScript, rustfmt + clippy for the CLI. Both the Godot and Roblox style guides indent with tabs; .editorconfig pins them to 4 columns for editors that honor it, and on GitHub you can append ?ts=4 to a file URL.
Two paths depending on your workflow. (A Studio plugin with a live logic-cell inspector is planned; Studio gained WebSocket support in late 2025, so that is where it will land first.)
# wally.toml
[dependencies]
Tether = "datagrout/tether@0.4.0"wally install
tether init # bootstrap credentials → writes TetherConfig.lua
rojo serve # TetherConfig syncs alongside Tether into Studiotether-cli is a small Rust binary: cargo install tether-cli or download from the releases page.
TetherConfig.lua is gitignored automatically. Teammates run tether init once on their machine.
The package builds (via tether-lua/default.project.json) to a single Tether ModuleScript with TetherCore and TetherUtils as children, so require(Packages.Tether) returns the module directly.
Drop tether-lua/Tether.lua, TetherCore.lua and TetherUtils.lua into ServerStorage. First connect() call bootstraps a free-tier account automatically.
Security: keep Tether and
TetherConfig.luain ServerStorage (or ServerScriptService), never ReplicatedStorage — everything in ReplicatedStorage replicates to every player's client, credentials included. Tether is server-only anyway: Roblox only allows HTTP from the server.
Copy tether-lua/TetherCore.lua and the adapter for your engine into one folder of your project (LÖVE also needs TetherJson.lua; it ships no JSON library). Then:
-- LÖVE
local tether = require("lib.tether.tether_love")
function love.load() dg = tether.connect({ agent_name = "my-game" }) end
function love.update() tether.update() end -- pumps responses + timers
-- Solar2D
local tether = require("lib.tether.tether_solar2d")
local dg = tether.connect({ agent_name = "my-game" })
-- Defold (from a long-lived game object's script)
local tether = require("tether.tether_defold")
function init(self) self.dg = tether.connect({ agent_name = "my-game" }) enddg:query, dg:assert, dg:watch, dg:invoke, dg:call and the rest work exactly as on Roblox. LÖVE 12 has the required https module built in; LÖVE 11 needs lua-https.
Where the identity lives. On these engines the game binary is the client, so shipping a client_secret inside it would hand your credentials to every player. Instead the first connect() runs the DG onramp and stores a per-installation free-tier identity in the engine's save location (LÖVE save directory, Solar2D DocumentsDirectory, Defold sys.save). Nothing secret ships in the build. If you want one shared identity — a dev build, a game server, a relay you run — pass credentials = {...} to connect() or put a TetherConfig.lua from tether init on the package path; Tether warns when it sees bundled credentials in a fused/device/release build.
Conformance. tether-lua/test/run.lua is the protocol's test suite: it runs the core against a mock gateway and drives each adapter with fake engine globals. Every adapter must pass it (lua tether-lua/test/run.lua). tether-lua/test/live.lua runs the same contract against a real gateway (TETHER_BASE_URL=http://localhost:4000 lua tether-lua/test/live.lua, curl on PATH, onramp enabled).
See tether-protocol/PROTOCOL.md for the JSON-RPC message format, auth flow, the _dg metadata block, failure grammars, idempotency, and the client conformance list.
Every call pays the gateway base. Deterministic tools (logic.*, data.*, math.*, frame.*, batteries.*) add nothing on top, so they cost one credit each and are what a game loop should call. dg:batch runs up to 20 logic operations for two credits: one call per tick, not four. Model-backed tools (prism.*, latent.*, inference.*, agents.*) add a model term and belong on join, on quest generation, on rule setup, not per frame.
The gateway reports what each call cost inside its _dg block; Tether strips it from your results and keeps it for you:
dg:status().credits
-- { charged = 1, estimated = 1, remaining = 4999,
-- terms = { gateway_base = 1, tool_premium = 0, model = 0 } }When the balance runs out the gateway answers insufficient_credits; Tether switches to status credits_exhausted, fails calls locally for 30 seconds so a game loop cannot hammer the network, then lets one call through as a probe. Recovery is automatic. Low balances are warned to the developer console, never to players.
Credits for games come from a trade: a "Powered by DataGrout" splash on join, in exchange for a larger allotment. The program is commercial, not protocol; the tiers and their allotments are being redefined alongside metered pricing for real-time clients, so treat the numbers below as the shape of the deal rather than a quote.
| Tier | Requirement | Allotment |
|---|---|---|
| Free | None | development-sized |
| Community | "Powered by DataGrout" splash (3 s on join) | enough to run model-backed tools in a live game |
| Partner | Splash + DG link in game description | larger |
| Featured | Splash + link + featured on the DG showcase | largest |
| Paid | Billing on the dashboard | purchase-based |
Splash variants a developer may pick from: "Logic by DataGrout", "Minds by DataGrout", "Intelligence by DataGrout", "Reasoning by DataGrout", or "This world thinks. Powered by DataGrout." Upgrading is self-serve at https://app.datagrout.ai/tether; Tether detects the new allotment on the next call, no code changes.
dg:query(namespace, prolog, callback) -- Query LC facts
dg:assert(namespace, facts, callback?) -- Assert facts into LC
dg:batch(namespace, ops, opts?, callback) -- Several logic ops, one charged call
dg:watch(namespace, pattern, callback, opts) -- Poll for fact changes
dg:invoke(prompt, opts, callback) -- LLM inference [model term]
dg:orchestrate(agent_id, input, callback) -- Multi-agent workflow [model term]
dg:generate_rules(description, ns, callback) -- LLM writes your Prolog [model term]
dg:batteries().list(callback) -- Browse battery catalog
dg:batteries().install(id, ns, callback) -- Install a battery
dg:game_rules(namespace, callback) -- List installed batteriesdg:batch takes the ops logic.batch accepts: { op = "query", prolog = ... }, { op = "assert", facts = {...} }, { op = "forget", ... }; later ops may reference earlier query bindings with "{{op1.X}}".
dg:call(tool_name, args, callback)Exposes the full DG catalog: 150+ tools are available on signup, more behind feature flags. discovery.summary lists what your server can see.
-- Deterministic tools -- one credit, safe in game loops
dg:call("data-grout@1/math.trend@1", { values = {10,12,15,11,18} }, cb)
dg:call("data-grout@1/math.rank@1", { values = playerScores }, cb)
dg:call("data-grout@1/data.filter@1", { data = items, where = "level > 5" }, cb)
dg:call("data-grout@1/data.sort@1", { data = leaderboard, by = "score" }, cb)
dg:call("data-grout@1/frame.group@1", { data = events, by = "player_id" }, cb)
-- Model-backed tools -- call intentionally
dg:call("data-grout@1/prism.analyze@1", { data = gameLog, goal = "find patterns" }, cb)
dg:call("data-grout@1/latent.expand@1", { seed = "dungeon", domain_hint = "fantasy rpg" }, cb)dg:status()
-- {
-- status = "connecting"|"connected"|"credits_exhausted"|"disconnected",
-- credits = { charged, estimated, remaining, terms }, -- from the last call
-- last_meta = { execution_id, ctc, cache_ref, ... }, -- the full _dg block
-- mcp_url = "...", rpc_url = "...", platform = "roblox", version = "0.4.0"
-- }Tether.connect(opts) also accepts request_timeout (seconds, default 30), credits_cooldown (default 30), low_credits_threshold (default 20) and idempotency = false to omit idempotency keys.
Every call carries an idempotency key that survives retries, so a dg:assert that times out and is retried cannot write twice. An expired bearer is refreshed once and the call replayed. Transport failures and 5xx retry three times with jittered backoff; 429 waits the server's Retry-After. A tool that rejects its input inside a successful envelope (error + status_code) reaches your callback as an error, not as data.
MIT — see LICENSE.